A friendly guide to how the web actually works. For Real This Time. Generated on October 25, 2026
Introduction
In March 1989, Tim Berners-Lee, a computer scientist at CERN, handed his boss a proposal called Information Management: A Proposal. His boss, Mike Sendall, read it and wrote three words across the top: “Vague but exciting”. Then he let him build it anyway, which may be the best-value management decision ever made.
The problem Berners-Lee wanted to solve was not glamorous. CERN was full of brilliant people from dozens of countries, using every kind of computer ever sold, each with its own way of storing files. People joined a project, wrote things down, and left. Their notes stayed behind on machines nobody could find, in formats nobody could read. The laboratory was forgetting things faster than it learned them.
The modest idea
He didn’t propose to replace everyone’s computers or make them agree on a filing system. He proposed two small things.
First, documents should be able to point at other documents, anywhere, with a link. Second, there should be a protocol so simple that any computer could implement it in an afternoon: one machine asks for a document by name, the other sends it back. The very first version of that protocol was a single line of text, GET followed by the document you wanted. The server replied with the document and hung up.
By the end of 1990 the first web server was running on a NeXT computer in his office, with a handwritten label stuck to it: “This machine is a server. DO NOT POWER IT DOWN!!”. In 1993 CERN put the software in the public domain. No licence fee, no permission needed, no company to ask. That turned out to matter more than anything technical. Anyone could run a server, anyone could write a browser, and so, within a few years, everyone did.
How it got far away
The web succeeded, and success brought crowds. Crowds brought new problems, and every problem got a sensible fix. Conversations needed privacy, so encryption was added. Popular sites needed to serve millions of people, so caches and load balancers moved in between you and them. Some doors needed locks, some needed guards, and some needed a guard standing in front of a guard.
Each layer earned its place. But layers hide the thing they were built around. Today “put a website online” sounds like a job for a specialist, and the vocabulary has drifted from pages and links to edge, origin, ingress and zero-trust posture. The original exchange is still in there, one machine asking and another answering. You can’t see it anymore.
What this book does
This book peels the layers back one at a time and has you poke each one with your own hands. You’ll ask a server for a page and read its reply line by line. You’ll learn what the server’s numbers mean, follow a name like example.com down a tree of servers to the number behind it, run a web server on your own laptop, put a guard at its door, give it a lock and key, and watch what happens when a lot of people show up at once. At the end, you’ll put a real site on the real internet.
Every chapter has you run something within the first screen or two, and then explains what you just saw. The explanations are short. The commands are real, and the output you’ll see in this book came from running them.
You need two things: a terminal and a web browser. That’s all. The tools we use (curl, dig, Python, a few others) either ship with your computer already or are one install command away, and all of them are free. You don’t need to know how to program. You do need to be willing to type things and see what happens.
Underneath every layer we’re about to take apart, the web still does what it did in 1990 on that NeXT box with the sticker: one computer asks another for something, and gets an answer. So let’s ask.
Chapter 1
Asking for a Page
Open a terminal. On macOS it’s called Terminal, on Windows it’s PowerShell, and if you’re on Linux you already have six of them open. Now type this and press Enter:
$ curl -v http://example.com
A wall of text just scrolled past. Some of it was HTML, some of it looked like gibberish with angle brackets in front. Don’t close the window. That wall of text is the web. Not a metaphor for the web, not a simplified model of the web: that is literally what your browser does every time you visit a page, minus the part where it makes things pretty.
By the end of this chapter you’ll be able to read all of it.
Reading the wall
Here’s what came back for me, trimmed a little. Yours will differ in the details.
* Host example.com:80 was resolved.
* IPv4: 104.20.23.154, 172.66.147.243
* Trying 104.20.23.154:80...
* Connected to example.com (104.20.23.154) port 80
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
< Date: Sun, 18 Oct 2026 18:15:23 GMT
< Server: cloudflare
<
<!doctype html><html lang=en><head>...<title>Example Domain</title>...
curl -v (“verbose”) marks every line with a little symbol so you know who’s talking:
Lines starting with * are curl muttering to itself about what it’s doing.
Lines starting with > are what your computer sent.
Lines starting with < are what the other computer sent back.
So the whole exchange, with the muttering removed, is this. Your computer said:
GET / HTTP/1.1
Host: example.com
User-Agent: curl/8.7.1
Accept: */*
And the other computer, a few hundred kilometres away, said:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
<!doctype html><html>...
That’s it. That’s the trick. One machine sends a few lines of text asking for something, another machine sends a few lines of text back, followed by the thing that was asked for. Everything else in this book (domain names, servers, proxies, encryption, scaling) exists to make that one exchange happen faster, safer, or for more people at once.
The request, line by line
Look at the first line your computer sent:
GET / HTTP/1.1
Three words. GET is the verb: I would like to get something. / is the thing you want: the path, in this case the very top of the site, the front door. HTTP/1.1 is which version of the rules you’re following, the way you might open a letter with “Dear Sir or Madam” so the reader knows what kind of letter it is.
The lines after that are headers, little Name: value notes stapled to the request.
Host: example.com says which website you want. This matters more than you’d think, because one computer often hosts hundreds of websites, and it needs to know which one you’re knocking for.
User-Agent: curl/8.7.1 is your program introducing itself. Browsers put a long, rambling string here that claims to be about four different browsers at once, for historical reasons that are more embarrassing than technical.
Accept: */* means “I’ll take whatever you’ve got”. A browser would be fussier and say it prefers HTML.
Then an empty line, which means “I’m done talking”. That blank line is load-bearing. Without it, the server would sit there waiting for you to finish your sentence.
The response, line by line
The answer starts the same way, with one important line:
HTTP/1.1 200 OK
Same rules version, then a number and a phrase. 200 OK means it worked; here’s your thing. You’ve almost certainly met its evil twin, 404 Not Found. Numbers like these are called status codes, and the whole next chapter is about them, because they turn out to be the most expressive part of the conversation.
Then come the response’s own headers. Content-Type: text/html tells your computer what kind of thing is coming, so it knows whether to draw it as a web page, play it as a video, or save it as a PDF. Server: cloudflare is the answering computer introducing itself, which is a small preview of Chapter 6: the machine that answered you was not, strictly speaking, the machine that owns example.com. Someone was standing at the door.
Then another blank line, and then the body: the actual HTML of the page. That’s what your browser would take and turn into the page you see.
What the address bar was hiding
When you type http://example.com/about into a browser, the browser takes that address apart before it does anything else:
http://example.com/about
└┬─┘ └────┬────┘└─┬──┘
│ │ └── path: what you want → goes on the GET line
│ └────────── host: who to ask → goes in the Host header
└───────────────────── scheme: which rules to use → HTTP
Every piece of the address ends up somewhere in that little text message. The scheme picks the language. The host picks the computer, and also goes in the Host: header. The path goes right after GET. When there’s no path at all, as in http://example.com, the browser asks for /, the front door. (What the server hands you when you ask for the front door is up to the server. Many hand you a file called index.html, which is why you’ll see that name everywhere. More on that in Chapter 4.)
The full name for an address like this is a URL, a Uniform Resource Locator, and that is the last time this book will spell it out.
A browser asks a lot of questions
curl asked one question and got one answer. A browser asks one question, reads the answer, and finds out it needs to ask forty more. The HTML says “there’s an image here, a stylesheet there, a font, a script, another script, a tracking pixel, a second tracking pixel to keep the first one company”, and the browser dutifully sends a separate request for each.
You can watch this happen. In Chrome, Firefox, Edge or Safari:
Each of those rows is the same little conversation: a verb and a path go out, a status and a body come back. The web doesn’t get more complicated than that. It just gets more.
Two questions we skipped
If you were paying attention, two things in that curl output should be bothering you.
First, the line * IPv4: 104.20.23.154. You typed a name, example.com, and before a single word of HTTP was sent, curl had somehow turned it into a number. Who told it that number? (Chapter 3. It’s a better story than you’d expect.)
Second, the answer said 200 OK, and we waved our hands and moved on. But that number is doing a lot of quiet work, and when things go wrong it’s the first thing you’ll look at. So that’s where we go next: what the other computer says back, and all the different ways it can say “no”.
Chapter 2
Getting an Answer
In Chapter 1 the server said 200 OK and we moved on. That’s what it says nearly all the time, which is exactly why it’s boring. The interesting answers are the other ones. Try this:
That’s the entire reply. Three lines. No page, no HTML, not even an apology. GitHub received your request, decided it was asking in the wrong place, and told you where to go instead.
The -I flag makes curl send a HEAD request instead of GET. HEAD means “give me the status line and headers, but not the thing itself”, which makes it the fastest way to see what a server thinks of a URL without downloading anything. (-i, lowercase, sends a normal GET and shows the headers and the body. You’ll use both in this chapter.)
Three digits, five families
That 301 is a status code, and the words after it (Moved Permanently) are the reason phrase. The phrase is there for humans reading logs; programs ignore it and look only at the number. A server could send 301 Bananas and every browser on earth would still follow the redirect.
The number is three digits, and the first one carries most of the meaning:
1xx Hold on informational, the real answer is still coming
2xx Here you go it worked
3xx Try over there what you want is somewhere else
4xx You messed up the problem is with the request
5xx I messed up the problem is with the server
This is the most useful thing in the chapter. There are dozens of official codes, and you’ll meet ones you’ve never seen before, but you’ll always know which family they belong to. If a client receives a code it doesn’t recognise, the rules say to treat it like the x00 of its family: an unknown 471 is handled as a 400, an unknown 599 as a 500. So the first digit is not a convention, it’s a promise.
The 1xx family is the one you’ll see least. Those are interim messages sent before the real answer: 100 Continue (“go ahead, send the rest of your upload”), 101 Switching Protocols (used when a page upgrades its connection to a WebSocket), and 103 Early Hints (“while I think, you could start fetching this stylesheet”). They never stand alone: a real answer from one of the other families follows, except after 101, where the conversation stops being HTTP altogether.
2xx: here you go
200 OK is the workhorse. It means the request worked and the thing you asked for is in the body. Browsers never show it to you, which is how you know it’s doing its job.
Its quieter sibling is 204 No Content: “it worked, and I have nothing to say about it.” Google runs a URL that does nothing else:
$ curl -i http://www.google.com/generate_204
HTTP/1.1 204 No Content
Content-Length: 0
Cross-Origin-Resource-Policy: cross-origin
Date: Sun, 18 Oct 2026 18:25:31 GMT
That looks pointless until you learn who uses it. Android phones request a URL like this when they join a Wi-Fi network. If a 204 comes back, the internet is really there. If something else comes back, usually a 200 with a login page, the phone knows it’s stuck behind a hotel or airport “please accept our terms” screen and pops that page up for you. A reply with nothing in it turns out to be very informative.
Not every answer has a body
You might have read somewhere that every HTTP response comes with a body. It doesn’t, and you’ve already seen three counterexamples:
A reply to a HEAD request never has a body. That’s the point of HEAD.
A 204 No Content never has a body. It says so in the name.
A 304 Not Modified never has a body, for reasons coming up in a moment.
Plenty of others, like the 301 from GitHub, could have one and choose not to. GitHub’s said Content-Length: 0: here are the headers, and the body is zero bytes long. When a server does include a body with a redirect or an error, it’s usually a small HTML page for any human who happens to be looking. The status code is for the machine; the body is for you.
3xx: try over there
A redirect is an answer that says “not here” and, in the Location header, says where. GitHub’s 301 pointed from http:// to https://, which is one of the most common redirects on the web: the site only wants to talk over an encrypted connection (Chapter 7), so it bounces anyone who knocks on the old, unencrypted door.
301 Moved Permanently means update your bookmarks. Search engines transfer the old address’s reputation to the new one, and browsers remember it, sometimes for a very long time. The next time you type the old address, the browser may skip the server entirely and go straight to the new one. (This makes a mistaken 301 surprisingly hard to take back. People who run websites learn this once.)
302 Found means it’s over there, for now. Keep using the old address. Wikipedia has a page that shows a different random article every time, and it can’t possibly be a permanent redirect:
Same question, same status, different location each time. (The // at the start means “same scheme you’re already using”, so the browser fills in https:.)
There are two more you’ll meet: 307 Temporary Redirect and 308 Permanent Redirect. They mean the same as 302 and 301, with one difference that matters when you’re submitting a form: they promise the browser will repeat the request exactly as it was, instead of quietly turning it into a plain GET. Chapter 5 is where requests stop being only GET.
304: you already have it
The strangest-sounding code in the 3xx family isn’t a redirect at all. It points you somewhere you’ve already been: your own cache.
When a browser downloads a page, it keeps a copy. The server’s headers said when the page last changed. Here’s example.com’s:
Next time, instead of asking for the page again, the browser can ask a conditional question: “send it to me only if it has changed since then.” You can ask that too, with an extra header:
$ curl -i http://example.com -H 'If-Modified-Since: Tue, 13 Oct 2026 20:08:28 GMT'
HTTP/1.1 304 Not Modified
Date: Sun, 18 Oct 2026 18:25:16 GMT
...
Last-Modified: Tue, 13 Oct 2026 20:08:28 GMT
ETag: "6ace8fbc-241"
...
Nothing has changed, so the server says 304 and sends no body at all. The browser uses the copy it already has. On a page this small the saving is a few hundred bytes. On a site with a 2 MB image and a million visitors, it’s the difference between sending the image once per visitor and once per visit. (The ETag is the server’s fingerprint for this exact version; browsers can ask “has it changed since this fingerprint?” instead of using a date. Chapter 8 is all about this.)
4xx: you messed up
A 4xx means the server is fine and understood you perfectly; the problem is with what you asked. You met the most famous one in Chapter 1:
$ curl -sI http://example.com/this-page-does-not-exist | head -1
HTTP/1.1 404 Not Found
404 Not Found means there’s nothing at that path. Wrong address, typo, a page someone deleted, a link that was right in 2009. It’s not the server being broken; it’s the server correctly reporting a fact.
403 Forbidden is different: “I know what you’re asking for, and the answer is no.” The Apache web server, by default, refuses to hand out any file whose name starts with .ht, because that’s where it keeps its own configuration and passwords. Apache’s own website follows its own rule:
This is the shisa’s code. It stands at the door, it has heard your request, and it is not moving.
Its close cousin is 401 Unauthorized, which despite its name means unauthenticated: “I don’t know who you are; log in and try again.” A 403 means “I know who you are, and it doesn’t help.” There’s also 400 Bad Request (the request itself was garbled), 405 Method Not Allowed (that path exists but doesn’t accept that kind of request), and 429 Too Many Requests (slow down; you’ll see this one in Chapter 8).
When 404 means 403
Here’s a subtlety that matters if you ever run a website. A 403 tells the world that something is there. Sometimes that alone is a leak.
So servers sometimes lie on purpose. Ask GitHub for a private repository you don’t have access to, and it answers 404, exactly as if the repository didn’t exist. If it said 403, strangers could probe names (acme/secret-merger-plans, anyone?) and learn which ones are real. GitHub’s API documentation says so plainly: it sends a 404 instead of a 403 “to avoid confirming the existence of private repositories”.
It works in the other direction too. Apache’s .ht rule from above is about the name, not the file, so it refuses names that don’t exist either:
$ curl -sI https://httpd.apache.org/.htsomethingmadeup | head -1
HTTP/2 403
There is no such file. The 403 proves nothing. The lesson for reading status codes: they’re the server’s claim about what happened, and the server gets to decide what to claim.
418: I’m a teapot
On 1 April 1998, the IETF, the body that publishes internet standards, published RFC 2324: the Hyper Text Coffee Pot Control Protocol. It describes how to brew coffee over the network, and it defines one status code for a server that is asked to brew coffee but happens to be a teapot: 418 I'm a teapot.
It was a joke. Programmers loved it, and it ended up in real software. When someone proposed removing it from popular libraries in 2017, people objected loudly enough that it stayed, and the current HTTP standard reserves the number so it can never be given a serious meaning. Google, naturally, has one:
$ curl -si https://www.google.com/teapot | head -1
HTTP/2 418
(Ask with -I instead and it answers 200. Apparently the teapot only objects when you actually ask it for something.) Open it in a browser for the full experience. The body is short and stout.
5xx: I messed up
A 5xx means your request was fine and the server failed to answer it. You can’t fix these by changing your request; you can only wait, retry, or tell someone. The common ones:
500 Internal Server Error: something broke while handling your request. A bug, a crash, a missing file the code expected. It’s the server’s equivalent of a shrug, and the useful details are in the server’s logs, not in the reply.
502 Bad Gateway: the server you talked to was a go-between, and the server behind it sent back garbage or nothing at all. When a big site goes down, this is often the code you see, because the front door is still standing and the building behind it isn’t.
503 Service Unavailable: “too busy” or “closed for maintenance”. It may come with a Retry-After header saying when to try again.
504 Gateway Timeout: the go-between waited for the server behind it and eventually gave up.
502 and 504 only make sense once you know there can be more than one server between you and the answer. That’s Chapter 6. 503 is what happens when too many people show up at once, which is Chapter 8.
The number can be wrong
One last thing about status codes: they’re only as honest as the server sending them. A common mistake is a site that shows a “Sorry, page not found” message with a status of 200 OK. A human sees an error page; a search engine sees a perfectly good page that happens to say “not found”, and may file it as real content. These are called soft 404s, and they’re why it’s worth checking the status of your own error pages with curl -I rather than with your eyes.
The reverse happens too: a 500 page that contains a cheerful illustration and a “we’re on it!” message is still a 500. The machines read the number, the people read the body, and a well-behaved server makes them agree.
So when something on the web goes wrong, read the first line before anything else. The first digit tells you whose problem it is. The next two tell you what kind. Everything below that is detail.
Chapter 3
What's in a Name?
Back in Chapter 1, before curl sent a single word of HTTP, it printed this:
* Host example.com:80 was resolved.
* IPv4: 104.20.23.154, 172.66.147.243
You typed a name. Curl went off and came back with a number. Nobody asked you to approve this, nothing flashed on screen, and yet without that one step the request would never have left your computer, because the internet doesn’t actually know what example.com is. The internet knows numbers.
This chapter is about how names turn into numbers. It’s one of the oldest, most quietly heroic systems on the internet, and you can poke at every part of it from your terminal.
Computers call each other by number
Every machine on the internet has an IP address, which is to computers roughly what a phone number is to you. The classic kind (IPv4) is four numbers from 0 to 255 separated by dots, like 104.20.23.154. There are only about four billion of those, which seemed like plenty in 1981 and turned out to be roughly one per person, minus everyone’s phone, laptop, fridge and doorbell. So there’s a newer kind (IPv6) that looks like 2606:4700:10::6814:179a and comes in quantities large enough that we could give every grain of sand its own address and still have some left for the beach.
When your computer wants to talk to another one, it needs that number. Humans are bad at remembering numbers and good at remembering words, so we put a translation layer in between. That layer is the Domain Name System, or DNS.
Here’s the interesting part of what came back for me:
;; ANSWER SECTION:
example.com. 300 IN A 172.66.147.243
example.com. 300 IN A 104.20.23.154
;; Query time: 8 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
Read the answer lines left to right: the name you asked about, then 300 (how many seconds you’re allowed to remember this answer; hold that thought), then IN (for “Internet”, a relic from when other networks were a possibility), then A (the type of record: an Address), and finally the number itself. There are two, so your computer can pick either one. Those are the same numbers curl found.
Below that: the question took 8 milliseconds, and it was answered by 8.8.8.8. That server is not the one that owns example.com. It’s a middleman who went and found out. We’ll get to who it is.
Once upon a time, a text file
In the 1970s, the internet’s ancestor (ARPANET) solved the name problem the obvious way: a single text file called HOSTS.TXT listed every computer’s name next to its address. One office, the Network Information Center at SRI in California, maintained it. If you added a machine, you contacted them, they updated the file, and everyone downloaded the new copy every so often.
This worked beautifully for a few hundred computers and became a comedy for a few thousand. The file grew, copies went stale, two people would want the same name, and that one office became the bottleneck for the entire network. In 1983 Paul Mockapetris designed DNS to replace it, and the key idea was simple: don’t keep one big list; split it up and let everyone keep their own part.
The old file never fully went away, though. Your computer still has one.
Reading a name backwards
The trick that makes DNS scale is that names are a hierarchy, and you read them right to left.
www.example.com.
└── the root (that invisible final dot)
└────── .com: a "top-level domain"
└────────────── example: one name under .com
└────────────────── www: one name under example.com
Yes, there’s a dot at the end. Every fully written domain name ends with one, standing for the root of the whole tree. Everyone leaves it off because it’s always there, the way nobody bothers typing the full stop at the end of a text message.
Each level only needs to know about the level directly below it. The root knows who’s in charge of .com, .org, .ch, .dev and the rest, and nothing else. The .com servers know who’s in charge of example.com, and nothing about what’s inside it. The servers for example.com know the actual addresses. Nobody holds the whole map, which means nobody is the bottleneck.
Doing it by hand
Normally that whole walk down the tree happens for you, invisibly. But dig can be told to ask one specific server and not to cheat by asking around (+norecurse). So let’s be the resolver.
First, ask a root server. There are thirteen root server names, a.root-servers.net through m.root-servers.net, and the first one lives at 198.41.0.4:
$ dig +norecurse @198.41.0.4 example.com
;; AUTHORITY SECTION:
com. 172800 IN NS l.gtld-servers.net.
com. 172800 IN NS j.gtld-servers.net.
com. 172800 IN NS h.gtld-servers.net.
...
No answer section. The root doesn’t know where example.com is and doesn’t pretend to. What it gives you is a referral: “I don’t know, but these are the servers for .com, ask them.” NS means Name Server.
So we ask one of those (a.gtld-servers.net lives at 192.5.6.30):
$ dig +norecurse @192.5.6.30 example.com
;; AUTHORITY SECTION:
example.com. 172800 IN NS hera.ns.cloudflare.com.
example.com. 172800 IN NS elliott.ns.cloudflare.com.
Another referral, one level down: “.com doesn’t know the address either, but example.com is handled by these two servers.” So we ask one of those:
$ dig +norecurse @hera.ns.cloudflare.com example.com
;; ANSWER SECTION:
example.com. 300 IN A 172.66.147.243
example.com. 300 IN A 104.20.23.154
There it is. Three questions, three servers, each one more specific than the last, and the final one is authoritative: it’s the server the owner of example.com actually configured, so its word is final.
You don’t usually do the walking
Your laptop doesn’t do all of that every time you open a page. It hands the question to a resolver, a server whose whole job is walking the tree on behalf of other people. Your home router usually points you at one run by your internet provider; some people choose a public one instead, like Google’s 8.8.8.8 or Cloudflare’s 1.1.1.1. That’s who answered my dig earlier.
What makes resolvers fast is that they remember. Every answer comes with a TTL, a “time to live” in seconds: the root said .com’s servers were good for 172,800 seconds (two days), and example.com’s address was good for 300 (five minutes). Within that window, a resolver that gets asked again answers from memory without asking anyone. Since millions of people share the same few resolvers, the root servers hardly ever hear about .com. Its answer is almost always already sitting in someone’s cache.
This also explains one of the internet’s great folk myths. When you change your domain’s address and someone tells you to “wait for DNS to propagate”, nothing is actually propagating. Resolvers around the world are holding the old answer until its TTL runs out, and then they ask again. If your TTL was a day, the world can take a day to notice. If you know a change is coming, lower the TTL first.
Not just addresses
An A record is the most common kind of DNS record, but a name can carry other kinds of information too. You’ll meet these the moment you own a domain:
Type
Means
Example use
A
IPv4 address
example.com → 104.20.23.154
AAAA
IPv6 address (four times the A’s)
example.com → 2606:4700:10::6814:179a
CNAME
“This name is an alias for that other name”
www.example.com → example.com
MX
Where to deliver email for this domain
example.com → mail.example.com
TXT
Free-form text, mostly for proving things
“Yes, I own this domain, signed, me”
NS
Which servers are authoritative for the name
what we followed in the walk above
Where your own name fits
When you “buy” a domain, you’re really renting a line in the .com (or .org, or .ch) zone for a year at a time, through a company called a registrar. What that line says is: “for this name, ask these name servers.” Then, on those name servers, you write the records yourself: this name points to that address, mail goes here, www is an alias for the bare name.
That’s all a domain is. It’s not a website, and it’s not a server. It’s a signpost in a very large, very old, surprisingly polite tree, pointing at a number. The computer living at that number is the subject of the next chapter.
Chapter 4
The Place That Answers
So far, every answer in this book came from somebody else’s computer. Let’s fix that. Make a folder, put one file in it, and start a web server:
$ mkdir my-site
$ cd my-site
$ echo '<h1>Hello from my laptop</h1>' > index.html
$ python3 -m http.server 8000
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
The terminal now looks stuck. It isn’t. It’s waiting, which is the entire job description.
That middle bit in quotes is the request line from Chapter 1, word for word: GET, a path, a version. After it comes the status code the server chose to send back. The ::1 at the start is who was asking (you, more on that in a moment), and the two dashes are blank slots in a log format older than most websites.
The second request (most browsers make it) is your browser, unprompted, asking for /favicon.ico: the little icon it would put in the tab. You don’t have one, so the server said 404 and the browser shrugged. Browsers have been asking websites for this file, uninvited, since 1999.
Congratulations are not in order, because nothing difficult happened. That’s the point. You are now running a web server, and it came free with Python.
A server is a program, not a box
People say “server” and picture a humming black cabinet. The cabinet is optional. A server is a program that opens a connection point, waits for someone to connect, reads a request, and writes a response. The computer it runs on gets called a server too, by association, the way a building gets called a bakery.
Ask your own server something with curl, from a second terminal:
$ curl -v http://localhost:8000/
...
* Connected to localhost (::1) port 8000
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.7.1
> Accept: */*
>
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.9.6
< Date: Sun, 18 Oct 2026 18:25:13 GMT
< Content-type: text/html
< Content-Length: 30
< Last-Modified: Sun, 18 Oct 2026 18:25:12 GMT
<
<h1>Hello from my laptop</h1>
It’s the same shape as the exchange with example.com in Chapter 1. The Server: header is your Python introducing itself. Content-Length: 30 is the size of your file in bytes (29 characters plus the newline echo added). Last-Modified is when you created it. Every header there came from looking at a file on your disk.
The server that answers example.com is a different program, quite possibly on a different continent, but it does the same four things in the same order. The machines at big companies are faster and more numerous, not different in kind.
Doors with numbers
Look at the address you typed: localhost:8000. That :8000 is a port.
An IP address gets a request to the right computer. But one computer can run many programs that want to receive connections: a web server, a database, a game, the thing your printer uses to talk to your laptop. So each program claims a numbered door, from 1 to 65535, and a connection names both. If the IP address is the building, the port is the door number.
Some doors have conventional numbers, so nobody has to ask:
Port
Usually
80
HTTP
443
HTTPS (Chapter 7)
22
SSH, for logging in remotely
53
DNS, from Chapter 3
When an address leaves the port out, the scheme fills it in. http://example.com means port 80, and https://example.com means 443. That’s why curl said Connected to example.com ... port 80 in Chapter 1 without being asked, and why http://example.com:80/ gets you exactly the same page.
We picked 8000 because it’s out of the way. On Linux, ports below 1024 traditionally need administrator rights, and port 80 may already be taken by something else on your machine anyway. Speaking of which:
Knocking on a door where nobody’s listening goes one of two ways. On your own machine, the answer comes back instantly: curl says Couldn't connect to server, because your operating system knows nobody is there and says so. Out on the internet, a firewall often throws such knocks away without a word, and curl stands outside trying different addresses until it gives up:
$ curl -v --max-time 4 http://example.com:8201/
...
* Trying 104.20.23.154:8201...
* ipv4 connect timeout after 1898ms, move on!
...
* Connection timed out after 4006 milliseconds
A refusal is fast and a silence is slow, which is a useful thing to know at 2 a.m. when something hangs.
The name for yourself
localhost is the name every computer has for itself. You saw it in /etc/hosts in Chapter 3, mapped to 127.0.0.1, and your computer checks that file before asking DNS. So this request never touched the network: it went out of your browser, turned around inside your operating system, and came back in to Python.
The ::1 in the log is the IPv6 spelling of the same idea. Your machine tried IPv6 first, and Python was listening there too.
That raises a question worth asking about any server: listening where? The startup line said Serving HTTP on ::, and :: means “every address this machine has”. That includes the one your Wi-Fi router gave it. Unless a firewall stops them, anyone on the same network who knows your laptop’s address can read your folder right now. For experiments, keep the door on the inside:
$ python3 -m http.server 8000 --bind 127.0.0.1
Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...
Now only programs on your own machine can reach it.
Asking for the front door
You asked for / and got a file called index.html. You never mentioned index.html. The server decided.
This is a convention, not a law. When a request’s path points at a folder, many servers look inside for a file named index.html and send that. If there isn’t one, they either refuse or, like Python’s, generate a page listing what’s in the folder.
This is why you’ll see index.html everywhere, and also why “the URL path is a file path” is only true when the server feels like it. A path is a question. The server is free to answer it with a file, a folder listing, a redirect, a page it builds from a database, or a 404. Your browser can’t tell which, and doesn’t need to.
Files, or made to order
That distinction has names. A static server hands out files that already exist: what’s on disk is what you get, and everyone asking for / gets the same bytes. Your Python server is static, apart from those folder listings. So are a lot of real websites, including this book: it’s a folder of HTML files that a program generated once, before you ever arrived.
A dynamic server runs code for each request and builds the answer then and there. A search results page, your inbox, a shopping basket: none of these exist as files anywhere. The server receives GET /search?q=tanuki, asks a database, assembles some HTML, and sends it. To the browser, the response looks identical to a static one. Same status line, same headers, same body. The difference is entirely on the server’s side of the door.
Static is fast, cheap, and hard to break, because there’s nothing to run. Dynamic is how anything personal or up-to-the-second works. Most big sites are a mix: dynamic pages that pull in static images, scripts and stylesheets.
Where servers live
Anywhere a computer can stay switched on and connected.
Yours lives on your laptop and dies when you close the lid. In the early web, plenty of sites lived on a desktop machine in a university closet, and some still live under someone’s desk, in what hobbyists call a home lab. Most of the web lives in data centres: buildings full of racks of computers, with backup power, several internet connections, and enough air conditioning to chill a small town.
“The cloud” is those same data centres, rented by the hour instead of owned. When a site “runs in the cloud”, it runs on someone else’s computer, which you can rent for an afternoon or a decade. The program inside behaves exactly like the one in your terminal. It waits, it reads, it answers.
Every request is a stranger
Load your page a few times and watch the log. Each line is complete in itself. The server read a request, answered it, and forgot about it. It has no idea that the request for /favicon.ico came from the same browser that asked for / a millisecond earlier, and it doesn’t care.
This is called being stateless: each request carries everything the server needs to answer it, and nothing carries over to the next one. It sounds like a limitation, and it is one of the best decisions in the web’s design. Because no request depends on the last, any copy of a server can answer any request. That’s how a big site spreads its traffic across hundreds of identical machines (Chapter 8), and it’s why a server can crash, restart, and carry on without anyone noticing.
So how does a website remember that you’re logged in? The server hands your browser a small token called a cookie, and your browser attaches it to every later request, so each request still carries everything needed, including who you are. You’ll see the headers that do this in the next chapter.
The other thing a plain HTTP server never does is start a conversation. It can’t push a page at you or wonder where you went. It sits by its door and waits for a knock. Everything happens because a client asked.
What it was listening for
Look at that log line one more time: "GET / HTTP/1.1" 200. Your browser and a Python module, written by strangers who never met, agreed on every character of that exchange. That’s because both were speaking the same language, and it’s a language simple enough that you can type it with your own fingers. Next chapter, you will.
Chapter 5
Speaking Clearly
You’ve read HTTP. Now you’re going to write it, by hand, to a real server on the internet, with no browser and no curl to do the talking for you.
The tool is nc (netcat), which opens a connection to a port and then gets out of the way: whatever you type goes down the wire, and whatever comes back gets printed. It doesn’t know anything about HTTP. That part is your job.
Here’s what came back for me, trimmed:
HTTP/1.1 200 OK
Date: Sun, 18 Oct 2026 18:25:40 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: close
Server: cloudflare
Last-Modified: Tue, 13 Oct 2026 20:08:28 GMT
...
alt-svc: h3=":443"; ma=86400
241
<!doctype html><html lang=en><head><meta charset=utf-8>...</html>
0
You spoke HTTP to a machine you’ve never met, and it understood you. Apart from the connection nc opened, nothing was sent that you didn’t type. Those three lines were the whole request.
Two odd things in the answer. Transfer-Encoding: chunked means the server is sending the body in pieces, each one preceded by its size in hexadecimal: 241 is 577 bytes, which is exactly the size of the page, and the 0 at the end means “no more chunks”. And Connection: close is the server agreeing to your request to hang up afterwards. Without it, HTTP/1.1 keeps the connection open in case you want something else, and nc would sit there with you until the server got bored.
The fine print about line endings
HTTP’s rulebook says every line ends with two invisible characters: a carriage return and a line feed, written \r\n. It’s a habit inherited from teletypes, where one character moved the print head back to the left and another moved the paper up. Your Enter key only sends \n. Most servers forgive this, which is why your typed request worked, but a strict one is within its rights not to.
To send the proper endings, give nc a flag (-c on macOS, -C on most Linux versions), or write the whole request with printf, which turns \r\n into the real characters:
The sleep 2 is there because some versions of nc hang up the moment their input runs out, before the answer has had time to arrive. The parentheses keep the input open for two more seconds.
Leaving things out
Since you’re the one typing, you can also get it wrong on purpose. Drop the Host line:
400 means “I couldn’t make sense of that”, and here the server is right: HTTP/1.1 makes Host mandatory, because the machine answering for example.com answers for an enormous number of other names too. Without Host, it has no idea which site you’re knocking for.
Your own server from Chapter 4 is more forgiving. Start it again (python3 -m http.server 8000 in your my-site folder) and talk to it directly:
$ printf 'GET / HTTP/1.1\r\n\r\n' | nc localhost 8000
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.9.6
Date: Sun, 18 Oct 2026 18:25:28 GMT
Content-type: text/html
Content-Length: 30
Last-Modified: Sun, 18 Oct 2026 18:25:12 GMT
<h1>Hello from my laptop</h1>
No Host, no complaint. It only serves one site, so it never needed to ask. (Notice it answers in HTTP/1.0, an older version that closes the connection after every response. That’s also why the sleep wasn’t needed this time.)
Why text
A protocol is an agreement about what bytes to send, in what order, and what they mean. HTTP is the agreement the web runs on, and you’ve now seen almost all of its grammar: a request line, some headers, a blank line, maybe a body; then a status line, some headers, a blank line, a body.
It’s plain text because Tim Berners-Lee made it that way from the start, and that choice has paid off ever since. Anyone can read it, as you have. Anyone can write a program that speaks it, in any language, on any machine. When something breaks, you can look at the actual bytes and see what went wrong. Your browser, your Python server and Cloudflare’s machines were all written by different people who never talked to each other, and they interoperate because they all read the same few documents.
Verbs
The first word of a request is the method. You’ve used GET. There are a handful more, and each one promises something about what it does:
Method
Means
Safe
Idempotent
GET
Give me this
yes
yes
HEAD
Same as GET, but headers only
yes
yes
POST
Here’s some data, do something with it
no
no
PUT
Store this, replacing what’s there
no
yes
PATCH
Change part of this
no
not necessarily
DELETE
Remove this
no
yes
Safe means asking doesn’t change anything on the server. You can GET a page a thousand times and the page is no different afterwards (the server may count your visits, but it’s not supposed to change anything that matters to you). Idempotent means doing it twice has the same effect as doing it once. Deleting a thing twice still leaves you with one deleted thing. PUT the same file twice and you’ve stored that file. POST an order twice and you may have two orders.
These promises are why your browser asks “Confirm form resubmission?” when you reload a page you got by submitting a form: it was a POST, and the browser has no way to know whether repeating it is harmless. They’re also why a server should never delete anything in response to a GET. Programs assume GET is safe and follow links freely. In 2005, Google’s Web Accelerator started prefetching every link on the pages its users visited, and web apps whose “delete” buttons were plain links found out what that means.
HEAD is what curl -I sends, which you met in Chapter 2. It’s how you ask “how big is it, and when did it change?” without downloading the thing.
Sending something
GET carries nothing but the request. When you submit a form, the data goes in a body after the blank line, usually with POST. curl -d sends one. httpbin.org, the test service from Chapter 2, will echo back whatever it receives:
Giving curl a body made it switch to POST on its own, and add two headers. Content-Type says what format the body is in: application/x-www-form-urlencoded is the format HTML forms use, name=value pairs joined by &. Content-Length: 27 is the body’s size in bytes. Count the characters in name=tanuki&snack=dumplings if you don’t believe it.
Content-Length is how the other side knows where the body ends. There’s no closing marker; the reader counts bytes (or, with chunked, reads one counted piece at a time). Get the number wrong and the server either waits forever for bytes that aren’t coming or cuts your message short.
Your Python server, by the way, only knows GET and HEAD. Send it a POST with curl -d hello http://localhost:8000/ and it answers 501 Unsupported method ('POST'). Accepting data means deciding what to do with it, and a file server has no opinion.
Headers worth knowing
There are dozens of standard headers and countless unofficial ones. These are the ones you’ll meet every day:
Header
Sent by
Says
Host
client
Which site you want (required in HTTP/1.1)
User-Agent
client
Which program is asking
Accept
client
Which formats it would like back
Cookie
client
Tokens the server asked it to keep
Content-Type
both
What format the body is in
Content-Length
both
How many bytes the body has
Cache-Control
mostly server
How long the answer may be reused
Set-Cookie
server
“Keep this and send it back next time”
Location
server
Where to go instead (with a redirect)
Header names are case-insensitive: Content-type from Python and Content-Type from Cloudflare mean the same thing.
The last chapter promised you the mechanism that lets a stateless server remember you. Here it is in one exchange:
The server says two things at once. Set-Cookie asks the client to remember flavour=matcha and send it back with every request to this site. Location with a 302 says “now go to /cookies”. A browser would do both without asking you. Curl does neither unless told to.
Cache-Control decides whether a response can be reused without asking again. Cache-Control: max-age=60 means “this is good for 60 seconds”. It’s the HTTP cousin of DNS’s TTL from Chapter 3, and it’ll matter a lot in Chapter 8.
HTTP/2 and HTTP/3
Ask for the same page over HTTPS and look at the first lines:
Same method, same path, same headers, same status code, though the header names are now all lowercase. What curl printed is a translation. On the wire, HTTP/2 isn’t text at all: it’s binary, chopped into small labelled frames, with headers compressed. That makes it impossible to type into nc, and much better at one thing: carrying many requests and responses over one connection at the same time, interleaved, instead of one after another. For a page that needs forty files, that’s a big deal.
HTTP/3 keeps the same idea and moves it onto a new transport called QUIC, which runs over UDP instead of TCP and copes better with lost packets and flaky mobile connections. Remember the alt-svc: h3=":443" header in the reply to your hand-typed request? That was the server mentioning, in passing, that it also speaks HTTP/3 on port 443, should your client care to switch.
The versions differ in packaging, not in meaning. Methods, headers, status codes, cookies and caching all work the same in all three. Everything you typed by hand in this chapter is still what’s being said; it’s only the envelope that changed.
Out in the open
One more thing about your hand-typed request: every byte of it crossed the internet as readable text. Your Wi-Fi, your provider, and every network in between could read it as easily as you did. That’s fine for example.com. It’s less fine for a password.
There’s also an assumption we’ve been making all along: that the program answering is the server itself. Often it isn’t. Remember Server: cloudflare. Someone usually stands at the door, and the next chapter is about who.
Chapter 6
Standing at the Door
Back in Chapter 1, example.com answered your curl with a header that didn’t quite fit:
< Server: cloudflare
Cloudflare doesn’t own example.com. It wasn’t the machine that wrote the page, and it isn’t where the page lives. It was standing between you and that machine, taking your request, getting the answer and handing it back as if it had written it itself. You never noticed, because noticing isn’t the point.
That thing in the middle is a reverse proxy, and a large share of the web’s traffic goes through one. The quickest way to understand it is to put one in front of your own server.
Hiring a doorman
You’ll need the little Python server from Chapter 4 and a program called Caddy, a web server that is also a handy reverse proxy. (brew install caddy on a Mac, apt install caddy on Debian and Ubuntu, or a single download from caddyserver.com. It’s one file.)
--from :8080 means “listen on port 8080”, and --to :8000 means “pass everything to port 8000”. Because --from names only a port and no hostname, Caddy stays on plain HTTP. (Give it a real domain name instead and it will try to get an HTTPS certificate for it. That’s Chapter 9.)
Spot the difference
Here’s what the Python server said when asked directly, trimmed to the response:
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.9.6
< Date: Sun, 18 Oct 2026 18:25:19 GMT
< Content-type: text/html
< Content-Length: 30
< Last-Modified: Sun, 18 Oct 2026 18:25:17 GMT
<
<h1>Hello from my laptop</h1>
And here’s the same page, fetched through Caddy on port 8080:
< HTTP/1.1 200 OK
< Content-Length: 30
< Content-Type: text/html
< Date: Sun, 18 Oct 2026 18:25:19 GMT
< Last-Modified: Sun, 18 Oct 2026 18:25:17 GMT
< Server: SimpleHTTP/0.6 Python/3.9.6
< Via: 1.0 Caddy
<
<h1>Hello from my laptop</h1>
Same body, byte for byte. But look at the edges. The first line went from HTTP/1.0 to HTTP/1.1: Python speaks the older version, Caddy speaks the newer one, and you only ever talked to Caddy. The headers came back in a different order, and Content-type got a capital T. There’s a new header, Via: 1.0 Caddy, which is the proxy signing the guest book: “this message came via me, and the hop behind me spoke HTTP/1.0.”
The lesson is that a proxy doesn’t pass your bytes along like a pipe. It reads the whole request, makes a new request of its own to the server behind it, reads that answer, and writes a new answer to you. Two complete HTTP conversations, glued together in the middle.
Python’s log in the first terminal agrees. It shows two requests: the first from ::1 (that was curl), the second from ::ffff:127.0.0.1 (IPv6’s way of writing 127.0.0.1), which was Caddy. As far as Python is concerned, its second visitor was the proxy.
What the server behind the door sees
If the server only ever meets the proxy, how does it know who the real visitor was? The proxy tells it, with headers. You can watch this with nc from Chapter 5, playing the part of the server.
The first four lines are curl’s, passed along unchanged. The rest Caddy added. X-Forwarded-For is the visitor’s real address (::1 is IPv6 for “this same machine”). X-Forwarded-Host and X-Forwarded-Proto say which name and which scheme the visitor originally used, which matters once the proxy is speaking HTTPS to the world and plain HTTP to the server. Accept-Encoding: gzip is Caddy offering to take a compressed answer from the server, on its own behalf.
Notice that these headers are claims, not facts. Anyone can send an X-Forwarded-For header with any address in it. A server should only believe them when they come from a proxy it trusts.
Forward and reverse
A forward proxy works for the client: your company or your VPN app sits in front of you, and every site you visit sees the proxy instead of you. A reverse proxy works for the server: it sits in front of the site, and every visitor sees the proxy instead of the server.
Same trick, opposite side of the conversation. When people say “proxy” in the context of running a website, they almost always mean the reverse kind, and so does the rest of this book.
The shisa at the gate
On Okinawa, roofs and gateposts are guarded by shisa, the curly-maned lion-dogs you might recognise from this book’s cover. They come in pairs, they don’t own the house, and they don’t cook dinner. Their whole job is the doorway: everyone who comes in passes them first, and some people don’t get past.
A reverse proxy has exactly that job description. The server behind it owns the content and does the real work. The proxy owns the doorway. Because every single request has to walk past it, the doorway turns out to be the best place to do a surprising number of chores, each one taken off the server’s plate.
One chore is telling you, honestly, when the house is empty.
There’s the 502 from Chapter 2, and now you know exactly who says it. The door was open, the proxy answered, and the proxy reported that the server behind it didn’t. This time Server: Caddy is the only name on the reply, because there was no upstream answer to pass along. Caddy’s log, meanwhile, says dial tcp :8000: connect: connection refused, which is the same news from the inside.
Whenever you see a 502 or 504 on a real website, picture this: a doorman, standing in a perfectly working doorway, shrugging at an empty hallway.
What a doorman is for
Here are the chores that make proxies worth the extra hop. Most real setups use several at once.
TLS termination. The proxy holds the site’s certificate and does the encryption with visitors, then talks plain HTTP to the server over a short, private link. One program to configure, one place where certificates get renewed. That’s what X-Forwarded-Proto was hinting at, and it’s the subject of the next chapter.
Caching. If a thousand people ask for the same logo, the proxy can fetch it from the server once, keep a copy, and hand out the other 999 itself. The server sets the rules with headers like Cache-Control, saying what may be kept and for how long. Chapter 8 is largely about this.
Load balancing. Repeat the --to flag (--to :8000 --to :8001) and Caddy spreads requests across both servers. Visitors still see one address. When one server falls over, a proxy with health checks stops sending people to it.
Routing by path or host. One doorway can serve several houses. A few lines of Caddy’s config file (the Caddyfile) send /api/ to one program and everything else to another:
From outside it looks like one website. Behind the door it’s two programs that have never met. Routing by Host header works the same way, which is how one machine with one IP address serves many domains, the Chapter 1 Host header earning its keep.
Rate limiting and blocking. The doorway is where you notice that one address has asked for /wp-login.php four hundred times in a minute (on a site that has never run WordPress). A proxy can slow down or refuse clients that ask too often, drop requests that don’t parse as proper HTTP, and turn away known-bad patterns before they reach code that might be fooled by them. A web application firewall is this idea taken seriously, with a rulebook of attacks.
None of this is impossible for the server itself. But a program that’s good at building your pages is rarely also good at fending off a botnet, and keeping the chores in one well-tested program in front means you don’t have to teach every server behind it the same tricks.
Who stands at doors
You’ll keep running into the same handful of names. They overlap a lot; the differences are mostly emphasis.
Proxy
Known for
Nginx
Web server and reverse proxy in front of a large part of the web. Fast, everywhere, terse configuration.
Caddy
Automatic HTTPS by default and a short config file. What this book uses.
HAProxy
Load balancing specialist, with very fine control over how traffic is spread and checked.
Traefik
Built for containers; discovers services in Docker or Kubernetes and routes to them as they come and go.
Envoy
Programmable proxy common inside large service meshes.
CDNs (Cloudflare, Fastly, Akamai, CloudFront…)
Reverse proxies as a service, run on thousands of machines around the world.
That last row is the one you met in Chapter 1. When a site sits behind a CDN, its DNS points at the CDN’s machines (the 104.20.23.154 you got back in Chapter 3 is one of Cloudflare’s addresses, not example.com’s own), so your request lands at whichever of the CDN’s doorways is nearest to you. That doorway serves from its cache when it can and fetches from the site’s real server, the origin, when it can’t. It’s the same pattern you just built on your laptop, scaled up until it covers the planet.
Stop the doorman
Press Ctrl+C in the Caddy terminal and in any other server you still have running. Port 8080 is a plain empty port again.
On your laptop, the trip from curl to the doorman was a few centimetres of memory. On the real internet it’s a long stretch of cables, routers and other people’s Wi-Fi, and so far everything you’ve sent down it has been plain text. Every request in this book, Host header and all, could have been read by anyone along the way. The next chapter is about how the web learned to seal the envelope, and why the doorman is usually the one holding the key.
Chapter 7
Locks, Keys, and Quiet Conversations
Every request in this book so far has gone out as plain text. You’ve read them yourself, Host header and all, and so could the café Wi-Fi, your internet provider and every router in between. Time to add one letter:
$ curl -v https://example.com -o /dev/null
The -o /dev/null throws the page away, because this time the interesting part comes before it. A whole new block of lines appears between “Connected” and GET. That block is two computers that have never met agreeing on a secret, in public, in a few milliseconds, with anyone listening welcome to listen.
First, the port is 443, not 80. That’s the default home of HTTPS, the same way 80 is home for HTTP. Then come seven messages, marked OUT when your computer sent them and IN when the server did. This is the TLS handshake. When it’s done, curl reports TLSv1.3 and a cipher name, prints the server’s certificate, and only then sends its ordinary GET.
(The numbers in brackets are message types from the TLS specification. Unknown (8) is “Encrypted Extensions”; the curl that ships with macOS doesn’t have a name for it, and other builds may print one. The ALPN lines are a side negotiation about which HTTP version to speak afterwards: curl offered HTTP/2, the server accepted, and that’s why the response headers come back in lowercase.)
TLS stands for Transport Layer Security. HTTPS is ordinary HTTP sent inside a TLS connection, nothing more: the same GET, the same headers, the same 200. (You’ll also hear “SSL”, TLS’s predecessor. The name outlived the protocol, which is why curl still says SSL connection.)
Postcards
Plain HTTP is a postcard. It gets where it’s going, but everyone who handles it on the way can flip it over and read it, and nothing stops them from writing on it either. For a page about cats, that’s fine. For POST /login with your password in the body, or a response with your bank balance in it, it’s not.
HTTPS puts the postcard in a sealed envelope. Same postal system, same routes, same handlers, but they only see the outside. The envelope does three jobs at once: nobody along the way can read it (confidentiality), nobody can change it without the recipient noticing (integrity), and you can be sure who it’s addressed to (authentication). The handshake is where all three get set up.
A secret agreed in public
Here’s the puzzle. Your laptop and Cloudflare’s server have never met. Every message between them passes through networks neither of them controls. And yet by the end of the handshake, they share a secret key that nobody listening knows. How?
The trick is a piece of mathematics called key agreement, descended from an idea by Whitfield Diffie and Martin Hellman in 1976. Each side picks a random private number that never leaves the machine and computes a public “share” from it. They swap shares in the open. Each side then combines its own private number with the other’s public share, and the maths guarantees both arrive at the same result. Someone who saw both public shares can’t work it out, because doing so would mean undoing a calculation that is easy in one direction and impractically hard in the other.
Notice what never happened: nobody sent the secret. It was computed separately on both ends. Here’s how that maps onto curl’s lines:
laptop server
│── Client hello ───────────────────▶│ my key share, ciphers,
│ │ "I want example.com"
│◀─────────────────── Server hello ──│ my key share
│ │
│ both compute the same secret │
│┈┈┈┈┈┈ encrypted from here on ┈┈┈┈┈┈│
│◀─────────── Encrypted Extensions ──│
│◀──────────────────── Certificate ──│ "I am example.com"
│◀──────────────────── CERT verify ──│ proof I hold its key
│◀─────────────────────── Finished ──│
│── Finished ───────────────────────▶│
│── GET / ──────────────────────────▶│
In TLS 1.3, the version curl used, this takes a single round trip: one message out, one batch back, and the laptop’s Finished can travel together with the first request. The Finished messages are each side’s checksum over the whole conversation so far, so if anyone tampered with a hello along the way, the handshake fails.
But key agreement alone has a hole. You’ve agreed a secret with someone. If an attacker sat in the middle and swapped in their own key shares, you’d have a perfectly private conversation with the attacker. That’s what the next two messages are for. Certificate says who the server claims to be, and CERT verify is a signature over the handshake made with the private key that matches the certificate. Only the real owner of that key can produce it.
Reading the certificate
A certificate is a small signed document that says: this public key belongs to this name, from this date to that date, and here’s who vouches for it. You can ask any HTTPS server to show you its own.
Certificate chain
0 s:CN=example.com
i:C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
v:NotBefore: Sep 26 22:49:11 2026 GMT; NotAfter: Dec 25 22:56:35 2026 GMT
1 s:C=US, O=SSL Corporation, CN=Cloudflare TLS Issuing ECC CA 3
i:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
v:NotBefore: May 29 19:49:45 2025 GMT; NotAfter: May 27 19:49:44 2035 GMT
2 s:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
i:C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
v:NotBefore: Oct 21 17:02:23 2022 GMT; NotAfter: Oct 17 17:02:22 2037 GMT
3 s:C=US, O=SSL Corporation, CN=SSL.com TLS ECC Root CA 2022
i:C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
...
Each entry has an s: (subject, who this certificate is about) and an i: (issuer, who signed it). Read it top to bottom and the issuer of each line is the subject of the next. Certificate 0 is example.com’s own, signed by an intermediate called “Cloudflare TLS Issuing ECC CA 3”. That one is signed by another intermediate, which is signed by “SSL.com TLS ECC Root CA 2022”. The last entry is that root again, this time vouched for by an older root, so that devices which only know the older one can still follow along.
The v: line is the validity window. example.com’s certificate runs from 26 September to 25 December: ninety days. The intermediates last a decade or more, which is why they’re kept locked away and only ever used to sign other certificates.
Why trust any of this? Because the chain has to end somewhere you already trust. Your operating system and your browser ship with a list of well over a hundred root certificates belonging to certificate authorities (CAs), organisations that are audited and allowed to vouch for names. Curl told you where it keeps its list: CAfile: /etc/ssl/cert.pem. If the chain leads back to a root on that list, every signature checks out, the name matches and the dates are current, you get SSL certificate verify ok. If not, you get a warning page.
One thing the padlock never promised: that the site is nice. A certificate for paypa1-login-secure.com proves you’re talking to paypa1-login-secure.com, with total cryptographic confidence. Whether you should be is up to you.
Free certificates, on a timer
For years, certificates cost money and a renewal meant someone remembering, every year or two, to click through a CA’s website and copy files onto a server. Plenty of small sites didn’t bother, which is how most of the web stayed on postcards well into the 2010s.
Let’s Encrypt changed the economics. It’s a non-profit CA that began issuing certificates to the public at the end of 2015, charges nothing, and has no website to click through. Instead, your server talks to it with a protocol called ACME: it asks for a certificate, proves it controls the domain (typically by serving a token Let’s Encrypt asks for, over HTTP, at a well-known path), and receives one. A program can do that unattended forever.
Its certificates have been valid for 90 days, short on purpose: if renewing is automatic, a short life costs nothing, and a stolen key is useful for less time. They’re getting shorter. Let’s Encrypt has offered 45-day certificates as an opt-in since May 2026, and its plan moves the default to 64 days in February 2027 and to 45 days in February 2028. (There’s also a six-day option for the truly committed.) Nobody renews by hand on that schedule. That’s the point.
Caddy, the doorman from the last chapter, does all of this by default: give it a domain name instead of a bare port, and it fetches a certificate, renews it, and redirects HTTP to HTTPS without being asked. You’ll watch it happen in Chapter 9.
What the envelope hides, and what it doesn’t
TLS protects everything inside the connection. It does not hide the fact that the connection exists.
An observer on the path can’t see
An observer on the path can see
The path (/account/statement) and query string
The IP address you connected to
All headers, including cookies and Authorization
Usually the hostname, in the Client hello
Request and response bodies: passwords, pages, forms
Your DNS lookups, unless you use DoH or DoT
The status code
When you connected, for how long, and roughly how much data moved
The hostname leaks because the Client hello has to say which site you want before any encryption exists, so a server hosting many sites knows which certificate to present. That’s the -servername flag you gave openssl, a field called SNI (Server Name Indication). A newer extension, Encrypted Client Hello, hides the real name where both sides support it. And as Chapter 3 warned, the DNS question you asked a moment earlier may have gone out as a postcard of its own.
Sizes and timing say more than you’d think. An observer who can’t read the envelope can still see that it’s the size of a particular page, or that it arrives every thirty seconds like a chat app checking in.
And once the envelope arrives, TLS is done. What the server does with your password after decrypting it is between you and the server’s owners.
Who’s holding the key
Look at the certificate issuer one more time: Cloudflare TLS Issuing ECC CA 3. The machine that completed your handshake wasn’t example.com’s own server. It was the doorman from Chapter 6, holding a certificate for example.com and the private key that goes with it.
This is TLS termination, and it’s the most common way HTTPS is deployed. The encrypted connection from the visitor ends at the proxy. The proxy decrypts the request, makes its decisions (cache, route, block), and forwards it to the origin server, often over plain HTTP on a private network, or over a second, separate TLS connection when the path is less private. The X-Forwarded-Proto header you saw Caddy add in Chapter 6 is how the server behind the door finds out: on your laptop it said http, and behind a real certificate it says https.
It’s a reasonable trade, but it is a trade. The envelope is opened at the door, so whoever runs the door can read everything: every password, every cookie. When you put a site behind a CDN, you are handing them that key. That’s often the right choice, and it should always be a deliberate one.
On your own laptop, none of this matters. Curl talking to Python over localhost never leaves the machine, so there is nobody in between to keep secrets from. The envelope is for the long trip.
With the conversation sealed, the remaining problem is volume: what happens to the doorman, the server and everyone’s patience when ten thousand visitors show up in the same minute.
Chapter 8
When Things Get Busy
Your requests so far have arrived one at a time and been answered at leisure. Real servers don’t get that luxury. A link to your page turns up somewhere popular, and a few thousand people click it in the same minute, each of them sending the same little GET you met in Chapter 1.
You don’t have to wait for fame to find out what happens next. You can send the crowd yourself.
A crowd of ten
Here’s what came back on my laptop, trimmed. Your numbers will differ, possibly by a lot: they depend on your machine and on what else it’s doing at the time.
Three thousand requests a second, from a server that Python includes as a convenience and whose documentation says not to use in production. Every one got a 200, and the average answer took three milliseconds. At that rate you could serve 265 million requests a day, which is more than most websites see in their lifetime.
Now be less kind. Five hundred at a time, ten thousand in total:
More traffic, and the server got slower at handling it: about 600 requests a second instead of 3,000. The big bar says most people were still answered in a blink. But a tail of them waited one, two, four, six seconds, and 368 never got in at all.
Those clumps aren’t random. Your operating system keeps a short waiting line for new connections that a program hasn’t picked up yet, and Python’s server asks for a line five places long. Five hundred crabs at a door with room for five: when the line is full, new knocks are ignored, and the knocking side waits a second, then longer, then longer again, before trying once more. Some gave up.
That’s what overload looks like from the outside. Not a crash, not a red error page: a server that is up, answering most people quickly and some people never. It’s also why “works for me” is the least useful sentence anyone can say during an outage.
Where the ceiling is
A server under load runs out of something. It might be CPU, memory, network bandwidth or disk speed, or, as here, the program’s own ability to accept and juggle connections. My laptop wasn’t anywhere near out of CPU. The bottleneck was the software.
Same files, same laptop, same crowd: sixty-five times the throughput, no errors, and the slowest answer took an eighth of a second. (This is flattering. hey and the server shared one machine and never touched a real network, so a server across the internet will do worse. Not 600-requests-a-second worse.)
So the first rule of scaling is unglamorous: before you buy more machines, check that you aren’t running the wrong program on the one you have.
Bigger, or more?
When the program is fine and the machine really is full, you have two ways out.
Vertical scaling means a bigger machine: more cores, more memory, a faster network card. Nothing about your setup changes, which is why it’s always the first thing people try. But there’s a biggest machine you can buy, its price climbs faster than its size, and it’s still one machine. One failed disk, one botched update, one reboot, and the whole site is gone.
Horizontal scaling means more machines of the same size, each with a copy of the site. There’s no hard ceiling, and losing one machine leaves the others running. The price is that something now has to decide which machine answers which request, and the machines have to agree about anything they share, like who is logged in or what’s in a shopping cart.
That second part is where HTTP’s design pays off. Every request carries everything needed to answer it (the path, the Host header, any cookies), so any copy of the site can answer any request. The hard part is the data behind the site. Ten web servers are easy; ten copies of one database that all agree with each other are a career. Most real setups end up in the middle: a few sensible machines rather than one giant or a thousand tiny ones.
Spreading the load
The something that decides is a load balancer, and you’ve already run one. In Chapter 6 you stood Caddy at the door with caddy reverse-proxy --from :8080 --to :8000. The --to flag can be repeated.
hello from server A
hello from server B
hello from server A
hello from server B
hello from server A
hello from server A
One address, two servers, and curl has no way of knowing. The shisa at the door is now also deciding which room to send each visitor to. Caddy picks at random by default, which evens out over many requests. Other strategies are a config line away: strict turns (round_robin), whoever is least busy (least_conn), or always the same server for the same visitor, for applications that keep state in one place.
When a server falls over
Now stop server B with Ctrl+C and ask the proxy twenty more times, printing only the status codes:
$ for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} " http://localhost:8410/; done
200 502 502 200 502 502 502 502 200 502 502 502 502 502 502 200 200 502 200 200
More than half the visitors just got 502 Bad Gateway, the status from Chapter 2 that means “the server behind me didn’t answer”. Server A is alive and well, and the proxy keeps sending people to B anyway, because nobody told it B is gone.
The fix is a health check: the proxy asks each server a small question every few seconds and stops sending traffic to any that don’t answer. Stop the proxy (Ctrl+C), restart server B, and save this as a file called Caddyfile:
Start it with caddy run --config Caddyfile, then stop server B again and give the proxy a few seconds to notice:
$ for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} " http://localhost:8410/; done
200 200 200 200 200 200 200 200 200 200
Every visitor lands on A. Caddy’s log fills up with complaints that localhost:8412 refused the connection, and when you start B again, it’s back in rotation within a couple of seconds. Nobody outside the building noticed a thing. (Caddy can also do passive health checks, judging servers by how real requests go instead of by sending test questions. Most load balancers do both.)
You can stop everything now: Ctrl+C in each terminal.
Health checks handle a server that’s gone. Sometimes the whole back end is in trouble, and then the goal is graceful degradation: fail in pieces rather than all at once. A shop can hide its “customers also bought” box and keep the checkout working. A news site can serve a slightly stale cached front page. And when there’s nothing left to give, an honest 503 Service Unavailable with a Retry-After header beats a request that hangs for a minute.
The fastest request is the one you don’t answer
Everything so far adds servers. Caching removes work instead: answer once, keep the answer, and hand out copies until it goes stale.
When I ran curl in Chapter 1, the answer had a few more headers than I showed you. Here they are:
(-I sends a HEAD request, which asks for the headers without the body.) age: 11267 says this copy has been sitting in a cache for 11,267 seconds, a little over three hours. cf-cache-status: HIT says the cache had it, so the computer that actually owns example.com wasn’t asked at all. My question was answered by a copy.
The interesting thing missing is a Cache-Control header, the server’s instructions to caches. Without one, caches fall back on their own rules and guesses (an old page that hasn’t changed in a while is probably fine to keep for a bit). Sites that care say what they mean:
max-age=3600 tells your browser it may reuse this page for an hour without asking. s-maxage=86400 gives shared caches, the ones that serve many people, a full day. must-revalidate means that once a copy is past its date, it has to be checked before anyone gets it. The date is seven hours older than my request, which matches the age: I was handed a page made that morning.
x-cache shows two layers of Wikipedia’s own caches. The first one, cp6016, didn’t have the page. The second, cp6011, did, and by its counter had handed out that same copy 280,822 times.
Caches all the way down
There’s a cache at nearly every step between you and the server:
Your browser keeps copies on your own disk. That’s why a site loads faster the second time.
A CDN (more on those in a moment) keeps copies near you, shared by everyone in your region.
A reverse proxy in front of the server can keep copies, so repeated requests never reach the application.
The application can save expensive work, like a finished page or a database result, in memory and reuse it.
Even when a copy goes stale, it doesn’t have to be thrown away. The cache can ask the server whether it changed, and if it didn’t, the server says so in a few bytes instead of resending everything.
The hard part of caching is not keeping copies, it’s knowing when they’re wrong. Phil Karlton’s line that there are only two hard things in computer science, cache invalidation and naming things, has outlived most of the software written in his era. Static sites have a neat way around it: put a fingerprint in the file name (style.3f9a1c.css), tell every cache to keep it for a year, and when the file changes, it gets a new name. Nothing ever needs to be invalidated, because nothing old is ever asked for again.
Copies all over the world
Look at the end of that cf-ray header: ZRH. That’s the airport code for Zurich, and it’s where the copy of example.com that answered me lives. Run the command yourself and you’ll most likely see a code for an airport near you.
That’s a content delivery network: a company with servers in hundreds of cities, each keeping cached copies of its customers’ sites. It exists because light is fast but not instant. Zurich to Sydney is about 16,500 kilometres in a straight line, light in fibre covers roughly 200,000 kilometres a second, and cables don’t run straight. That’s well over 160 milliseconds for every round trip, and setting up a new HTTPS connection takes several. An edge server in your own city answers before the far one has heard the question.
How does your request end up at the nearby copy? Partly with the trick from Chapter 3’s root servers: thirteen names, more than a thousand machines. A CDN announces the same IP address from all its locations, and the internet’s routing delivers you to the closest one. The 104.20.23.154 you saw in Chapter 1 is a different building in Zurich and in Tokyo. (Some CDNs use DNS instead, handing out a different address depending on where the question came from.)
CDNs shine for things that are the same for everyone: images, stylesheets, scripts, video, whole static pages. A page that’s different for each visitor still has to come from the origin, the server where the site actually lives, but the CDN can at least hold the connection close to you. Because they sit in front of everything, CDNs also soak up floods of junk traffic that would flatten a single server.
You probably don’t need any of this
Here’s the part the cloud brochures leave out. Caddy on my laptop did 39,000 requests a second serving static files. A small rented server will do less, but take a tenth of that, 4,000 a second, and every person in Switzerland could load your page in under forty minutes. For most personal sites, the busiest day ever is a rounding error.
What actually knocks small sites over is rarely static files. It’s a page that runs a slow database query for every visitor, or a server program that, like http.server, was never meant to face a crowd. So the order of operations is: serve static files where you can, use a proper server, send sensible Cache-Control headers, maybe put a CDN in front. Load balancers and health checks are for the day you’ve measured that one machine isn’t enough, and that day may never come.
Which is good news, because one small machine is exactly what you’re about to rent.
Chapter 9
Running Your Own Corner
For eight chapters you’ve been asking other people’s computers for things, and answering only yourself, on localhost. This chapter closes the gap. By the end of it, a page you wrote will be on the public internet, at your own name, behind a padlock, served by a machine you rent for about the price of a sandwich a month.
Here’s the shopping list:
A domain name. Around ten to fifteen a year for a .com, from any registrar. As Chapter 3 put it, you’re renting a line in the .com zone.
A small virtual server, usually called a VPS. Plenty of providers rent them by the month, and the smallest plans cost a few euros or dollars. One CPU and a gigabyte or two of memory is more than enough: Chapter 8 showed what a single machine can serve.
Something to serve. An index.html will do. So will the output of a static site generator.
An hour, most of which is waiting for things to install.
The walk-through below uses Debian or Ubuntu, the most common choices, and placeholder addresses: example.org for your domain, 203.0.113.10 and 2001:db8::10 for your server. (Those addresses are reserved for documentation, so they can’t belong to anyone. Swap in your own.)
A key, not a password
You’ll talk to your server over SSH, a remote terminal that runs over an encrypted connection, much like the TLS from Chapter 7. You could log in with a password, but every server on the internet is constantly guessed at by strangers’ programs, so you’ll use a key instead. Make one before you rent anything.
Generating public/private ed25519 key pair.
...
Your public key has been saved in /Users/you/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:uU/bgwF+iSGwgcEEAviTnOJt1+orV3UMhRc9TxHuQFg you@laptop
That made two files. id_ed25519 is the private half, and it never leaves your laptop. id_ed25519.pub is the public half, a single line starting with ssh-ed25519, and you can hand it to anyone. A server that has your public key can check that you hold the private one without the private one ever crossing the network: the same trick a certificate plays in Chapter 7, with you as your own certificate authority.
Renting the machine
Every provider’s sign-up looks slightly different and asks the same things:
Where. Pick a region near the people who’ll visit. Chapter 8 explained why distance costs milliseconds.
Which operating system. A current Debian or Ubuntu LTS release.
How you’ll log in. Paste the contents of your .pub file here.
A minute later you get an IP address, and usually an IPv6 one too. Log in:
$ ssh root@203.0.113.10
The authenticity of host '203.0.113.10 (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:...
Are you sure you want to continue connecting (yes/no/[fingerprint])?
That question is SSH admitting there’s no certificate authority here: it has never met this machine and can’t vouch for it. Type yes, and it remembers the server’s fingerprint and will complain loudly if it ever changes. (Some providers log you in as root, others as a user called debian or ubuntu who can use sudo. The commands below use sudo; as root you can leave it off.)
Before anything else, install the updates that came out since the provider built its image:
$ sudo apt update && sudo apt upgrade
Then make sure passwords can’t be used to log in at all. Many images already turn them off once you’ve supplied a key, but check rather than hope:
$ sudo sshd -T | grep passwordauthentication
If that says passwordauthentication yes, set PasswordAuthentication no in /etc/ssh/sshd_config and run sudo systemctl restart ssh. If the setting won’t budge, a file in /etc/ssh/sshd_config.d/ is overriding it; sshd -T always shows the final word. Keep your current session open until you’ve confirmed, from a second terminal, that you can still log in.
Pointing your name at it
Now Chapter 3 becomes your problem. At your registrar (or whoever hosts your domain’s DNS), add two records:
example.org. 300 IN A 203.0.113.10
example.org. 300 IN AAAA 2001:db8::10
The control panel will show these as form fields rather than a line of text, but the parts are the same: name, TTL, type, address. If you want www.example.org to work too, add the same pair for www, or a CNAME pointing www at example.org.
Set the TTL low, 300 seconds, while you’re setting things up. If you mistype an address, resolvers around the world will only hold on to your mistake for five minutes. Once everything works you can raise it.
Do this step early. Caddy will ask for a certificate as soon as it starts, and Let’s Encrypt will only issue one once your name points at your server.
Three doors open
A fresh server answers on whatever ports its programs happen to open. You want exactly three: 22 for SSH, 80 for plain HTTP, and 443 for HTTPS. Port 80 stays open even though you’ll serve everything over HTTPS, because that’s where visitors who type http:// arrive to be redirected, and one of the ways Let’s Encrypt checks that you control the name.
This is the shisa’s oldest job: three doors, and a wall everywhere else. Ubuntu comes with a simple firewall called ufw; on Debian, install it with sudo apt install ufw.
Order matters: allow SSH before you enable the firewall, or the first thing it blocks is you. If you’d like HTTP/3 (Chapter 5), which Caddy speaks and which travels over UDP, add sudo ufw allow 443/udp as well. (Many providers also offer a firewall in their control panel. Using one is fine; using both means you have two places to forget about.)
Installing Caddy
Caddy’s install page, caddyserver.com/docs/install, has a short block of commands for Debian and Ubuntu that adds Caddy’s own package repository. Paste those, and they finish with:
$ sudo apt install caddy
The package sets Caddy up as a system service: it starts now, starts again after every reboot, and reads its configuration from /etc/caddy/Caddyfile. Out of the box it serves a welcome page on port 80, so visit http://203.0.113.10 in your browser. If you see Caddy’s welcome page, your machine is on the web. It doesn’t have your name or your page yet.
Your page, your name
First, a home for your files. Create the directory and make it yours, so you can copy things in without sudo:
Then, from your laptop, copy your page across. scp is cp over SSH, using the same key:
$ scp index.html root@203.0.113.10:/var/www/site/
Back on the server, replace the contents of /etc/caddy/Caddyfile (sudo nano /etc/caddy/Caddyfile works) with this:
example.org {
root * /var/www/site
file_server
}
Four lines. The first is the site’s address. root says where its files live, and the * means “for every request”. file_server says to answer requests with those files, handing out index.html when someone asks for /, as in Chapter 4. Check your typing, then tell Caddy to reload:
What you didn’t write is the interesting part. Because the address is a domain name, Caddy assumes you want HTTPS. It asks Let’s Encrypt for a certificate, proves it controls example.org (that’s why the DNS and the open ports had to come first), installs the certificate, starts redirecting http:// to https://, and renews the certificate on its own, well before it expires. All of Chapter 7, from four lines. (caddy adapt prints the full configuration Caddy builds from your Caddyfile, if you’d like to see what those four lines expand into: there’s a server listening on :443 in there that you never asked for.)
If something goes wrong, the logs say why:
$ sudo journalctl -u caddy --no-pager | tail -20
The usual suspects are a DNS record that hasn’t reached Let’s Encrypt yet or a closed port 80 or 443. Look for a line saying certificate obtained successfully. Then open https://example.org in your browser. That’s your page, at your name, with a padlock.
Keeping it alive
A server is a bit like a houseplant: it doesn’t need much, but it does need something, and the something is mostly the same every month.
Updates. Turn on unattended-upgrades, which installs your distribution’s security fixes on its own (Ubuntu ships with it switched on; on Debian, sudo apt install unattended-upgrades, then sudo dpkg-reconfigure unattended-upgrades to enable it). It leaves other repositories alone, so still run sudo apt update && sudo apt upgrade now and then: that’s what brings new Caddy releases from the repository you added. Some updates, like a new kernel, only take effect after sudo reboot; on Ubuntu, a file appears at /var/run/reboot-required to tell you.
Backups. Your site’s real home is your laptop, ideally in version control, and the server only holds a copy. The things that exist only on the server are the Caddyfile and anything your site collects there. Copy those somewhere else, or use your provider’s snapshot feature. Restore from a backup once, on purpose, so the first time isn’t an emergency.
A look at the logs. Run sudo journalctl -u ssh a day after setting up and you’ll see strangers’ programs trying passwords for users called admin and test. That’s not personal. Every address on the internet gets this, and it’s why you turned passwords off. (Caddy doesn’t keep an access log unless you ask: add a log line inside your site block.)
Someone to notice. Plenty of services will request your page every few minutes and email you when it stops answering. You want to hear about an outage before your friends tell you.
That’s the whole job. Minutes a month, and most of those spent reading what an update changed.
Other ways in
Caddy isn’t the only path. Nginx or Apache with Certbot fetching the certificates is the classic setup, documented in a thousand tutorials. If your corner grows an application behind it, you’ll want a reverse proxy in front, and Caddy, Nginx, HAProxy and Zentinel all do that job, as you saw in Chapter 6. And if you’d rather not run a machine at all, static hosting services will take a folder of files and run the servers for you. That’s a fine choice, and you now know what they’re doing on your behalf.
One more time
Back on your laptop, ask your own server for your own page, the same way you asked example.com at the start of this book:
$ curl -v https://example.org
I can’t show you yours, but here’s the same request to caddyserver.com, which is served by Caddy too, trimmed:
It’s the wall of text from Chapter 1, and you can read every line of it now. A name turned into a number. A connection to a port. A certificate, signed by someone your computer trusts, for exactly the name you asked for. Then a GET, a Host header, a blank line, and a 200 coming back.
Somewhere, sometime soon, a person you’ve never met will type your name. Their computer will send those same few lines across the world to the machine you rented, and that machine will answer. It’s still one request and one response. One of the two computers is now yours.