← Back to Wiki
Self-Hosting / Web

You Deployed the New Page and the Browser Kept the Old One

You rewrite a page. You copy it to the server. You load it and get the old one. Half an hour later you still get the old one. Nothing is wrong with the server. Your web server never told the browser how long to keep that page, so the browser decided for itself.

The tell is that half the page still works

The page I rewrote was a dashboard. It read two data files. One of those files got retired along with the rewrite.

So the old page printed an error where that data goes.

The other panel read a file that still exists. That panel kept working. Live numbers, updating on its own timer, right underneath the error.

A dead server does not do that. A dead server gives you nothing.

Part live and part broken is the signature of a cached shell. The HTML is old. The data it can still reach is new. Suspect the page before you suspect the data.

Prove the server is right before you touch anything

Ask the server what it is actually sending with command curl -s https://example.com/ | grep -o "<title>.*</title>"

Then read the headers with command curl -sI https://example.com/

You want three things to agree.

Check the size on the server with command stat -c %s /srv/yoursite/index.html

If all three agree, stop looking at the server. It is doing its job. The problem is between the server and the reader.

Caddy sends no Cache-Control at all

file_server sends an ETag. It sends Last-Modified. It does not send Cache-Control.

No Cache-Control does not mean do not cache.

It means the browser gets to guess.

Browsers guess with heuristic caching. They look at how long ago the file was last modified, and keep it for a fraction of that age. A file untouched for a month gets held onto for a good while. That is sensible for a logo. It is wrong for a page whose entire job is being current.

Set the header on the site block

One line, inside the block for that site only.

dashboard.example.com {
	header Cache-Control "no-cache"
	root * /srv/dashboard
	file_server
}

Do not put it on the whole server. Your static pages and images want to be cached. Only the pages that must be current need this.

no-cache does not mean no-store

This is the part that gets remembered backwards, and the names are the reason.

no-store means never keep a copy. Every single load pulls the whole file down again.

no-cache means keep a copy, but ask before you use it.

Think of a library book you are allowed to keep on your desk. no-store makes you walk back to the shelf and carry the book over every time you want to read a page. no-cache lets the book live on your desk, but you have to phone the librarian first and ask whether a new edition came out. Most of the time the answer is no, and you read the copy already sitting in front of you.

So no-cache costs you a round trip. It does not cost you the file.

Confirm that is what you got. Read the ETag with command curl -sI https://example.com/

Then hand it back with command curl -s -o /dev/null -w "%{http_code}\n" -H 'If-None-Match: "your-etag-here"' https://example.com/

A 304 is the answer you want. The browser asked, the server said nothing changed, and no page body crossed the wire.

BE WARNED: the copy already in your browser will not fix itself. It was stored before the header existed, so it is still living under the old rules. Load the page once with a hard reload. Ctrl+Shift+R on Windows and Linux. Cmd+Shift+R on Mac. Anyone else who already loaded the page has to do it once too. The header only governs copies fetched after you set it.

Validate before you restart a shared reverse proxy

One Caddy usually fronts a lot of sites. A broken config file takes every one of them down together.

Test the new file in a throwaway container first with command docker run --rm -v /tmp/Caddyfile.new:/etc/caddy/Caddyfile:ro caddy:2 caddy validate --config /etc/caddy/Caddyfile

You are looking for Valid configuration on the last line. Everything above it is startup noise.

Keep the old one with command cp /opt/caddy/Caddyfile /opt/caddy/Caddyfile.bak-$(date +%s)

Swap the new file in. Then restart the container with command docker restart caddy

Restart, not reload. A reload can report success and quietly leave the old config running, which is the exact failure you are already trying to get out of.

Then curl every other site on that proxy and confirm they still answer. A login redirect is a 302 and is fine. A 000 or a timeout is not.

Test the real URL, not a copy of it

This is the mistake that let it through, and it was mine.

I checked the rewrite by copying the files to my own machine, serving them there, and taking a screenshot. The screenshot was perfect. Every row correct, every number live.

It proved the page was written properly. It could not have caught this. Nothing about a local copy involves the reverse proxy, the response headers, or a cache.

Load the real URL. In a real browser. Best in one that has never seen the site before.

A private window is the cheap version of that, and it takes about four seconds.

Resources

https://caddyserver.com/docs/caddyfile/directives/header

https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control

Share on X