How we went from "rebuild and restart every pod" to "pick a build and click deploy."
Every team ships a bad build at some point. When it happens, the only thing that matters is how quickly you can get back to the last version that worked.
The problem
A single Node server served both our backend and frontend from one domain:
example.com/api/... → Backend
example.com/... → Frontend (HTML, JS, CSS)
The frontend files were cached inside every pod. To revert a bad build, we had to revert the code, rebuild, redeploy and restart every pod. That's a full deploy cycle in the middle of an incident.
The idea: never overwrite a build
Every build goes to its own folder in S3, served through a CDN. At the root sits one special file: index.html, a copy of the index.html from the build we want live.
s3://frontend-bucket/ (https://cdn.example.com)
├── index.html ← copy of the LIVE build's index.html
├── 20261028-1015/ ← build 1
│ ├── index.html
│ ├── main.js
│ └── style.css
└── 20261029-1430/ ← build 2 (latest)
├── index.html ── copied to root on release
├── main.js
└── style.css
Old builds are never touched. Which version is live comes down to which index.html is sitting at the root.
The one detail that makes it work: each build must load its assets by the full folder path, so it still works after being copied to the root:
<script src="https://cdn.example.com/20261029-1430/main.js"></script>
Set this with your bundler's publicPath or base option. As a bonus, View Source shows which build is live.
Serving the HTML from the CDN
The pods no longer hold frontend files. Requests to /api/* go to Node, and everything else gets the root index.html from the CDN. You can route this in two ways:
-
At the load balancer: non-
/apirequests go straight to the CDN. The frontend stays up even if Node is down. -
Through Node: Node fetches
index.htmlfrom the CDN and streams it. This is a smaller change, but the frontend depends on Node.
Either way, a frontend change never needs a pod restart again.
Release and rollback
Release:
Build → Upload to new folder → Verify → Copy its index.html to root → Clear CDN cache
You can test any build at cdn.example.com/<timestamp>/index.html before users see it. Only the copy-to-root step changes what users get.
The root index.html acts as a switch between builds. We automate release and rollback through Jenkins: select a build and run the job. Jenkins replaces the root HTML, clears the CDN cache and verifies the live version, with no manual file changes.
It takes about a minute. There's no code revert, no rebuild and no pod restart. You revert the bad commit later, without pressure.
One assumption: backend changes stay backward compatible with the previous frontend build. The frontend and backend used to ship together. Now the frontend can roll back on its own, which is what makes it fast.
Two rules you can't skip
-
Caching: the root
index.htmlusesno-cache, because it's the switch. Timestamped JS/CSS usemax-age=31536000, immutable. Ifindex.htmlis cached for an hour, your rollback takes an hour to reach users. - Retention: keep the last ~10 builds. Never delete the live build or the one before it, because users with the page open still load from it.
Serving straight from a CDN? This works for you too
Many teams serve the frontend directly from S3 behind a CDN, with no Node server. The weak spot is that every deploy overwrites the previous build. A bad release means reverting, merging to main and waiting for the pipeline: 10 minutes at the very least.
Versioned folders fit this setup even better, because there's no routing to change. Upload each build to a new folder, use full asset paths, and treat the root index.html as the switch.
This is especially worth it if you deploy once or twice a week. Ten builds give you over a month of rollback points for almost no storage cost, and recovery becomes a one-minute switch instead of a 10-minute scramble.
Wrapping up
Builds are immutable, and going live is just pointing at one of them. Once releases work that way, a rollback is no longer an emergency rebuild. It's a one-minute switch.
Top comments (0)