The first deploy is a sequence of small manual steps you half remember. The tenth is the same sequence, plus whatever you forgot on the fourth.
The part people skip is the other direction. Nearly every deploy guide ends at "your site is live" and says nothing about the ten minutes after a bad one.
The idea
A deploy is a repeatable procedure with a check at the end and a known way back. Repeatable means written down as commands, so it runs the same way at 9am and at 11pm. The check is one request that proves the new version is serving. The way back is a previous version you can restore without rebuilding anything.
git push
|
v
+---------+ +-------+ +---------+ +--------------+
| build | --> | tests | --> | release | --> | health check |
+---------+ +-------+ +---------+ +--------------+
| |
keep the old 200 OK ? --> done
release on disk |
^ not OK ?
| |
+----- roll back <+
(point at the
previous build)How it works
- Managed platforms already do all of this. Vercel, Netlify, Railway and Render build every push and keep every previous build. On Vercel you open the project's Deployments list, pick an older one, and use Instant Rollback from its menu. It takes seconds and no terminal, with two things worth knowing first: on the free plan you can only go back one deployment, and after a rollback Vercel stops sending new pushes to your live domain until you undo it, so a fix you ship while rolled back will not appear.
- The
testsbox in the diagram is the one part nobody sets up for you. It is a GitHub Actions workflow that runs your checks on each push, and until you write one, that box is aspirational. - On your own server you build the same shape by hand. A
deploy.shthat pulls, builds into a new dated folder, moves a symlink, restarts the service, then curls the site. Keeping the previous folder is what makes rollback possible, and it is the step people drop to save disk space. - A
A URL your app answers only when it is genuinely working, used by scripts and monitors to tell running from broken.
is a route that touches something real. A homepage that was pre-rendered at build time answers
fine while the database is down.
/api/healththat runsSELECT 1does not. You have to add that route yourself, and it is about ten lines: run the query, return 200 when it succeeds and a 500 when it throws. - When a deploy fails, the answer is in the logs. In production Next.js replaces a server-side
error message with a generic one and an identifier, so what a visitor sees is not what happened.
Managed platforms keep two separate streams: a build log for the deploy itself and a runtime log
for requests after it, and telling them apart is most of the diagnosis. On your own server it is
journalctl -u yourapp -n 100. Running it is the section on reading them. - Database migrations do not roll back with the code. Restoring the previous build undoes your application. A column you dropped is still dropped. This asymmetry is the whole reason schema changes get treated more carefully than code changes.
What to do
- Write the deploy down the first time you do it. Even as a numbered list in your project's
README. Turning that list into
scripts/deploy.shcomes later, once you have done it by hand often enough to recognise a wrong step. A generated deploy script is a privileged program, so have your assistant explain it back line by line and run it against a throwaway server first. "Read it before you run it" is not a safeguard if you cannot yet read shell. - Practise the rollback while nothing is wrong. Deploy, roll back, undo the rollback, deploy again. On a managed platform the whole drill is a few clicks. On your own server it is different: you have to build the way back first, by keeping the previous release folder on disk, and the practice run is what proves it works. Nobody learns a rollback for the first time under pressure and does it well.
- End every deploy with the same check.
curl -o /dev/null -w '%{http_code}\n' -sS https://myapp.com/api/healthprints the status code, which is the thing you are judging. Plaincurlprints the body and stays silent about a 500, so a broken app can look like a passing check. A deploy can finish cleanly and still leave a broken app behind it. - Split a destructive schema change across separate deploys, and back up first. Add the new column and ship. Backfill it and ship. Only once the old version is no longer running anywhere, and only after you have a backup you have restored somewhere else, drop the old column. Dropping is the step with no rollback, so Backups is a prerequisite for it.
Where it breaks
Deploying on a Friday is a real argument with a boring answer: it depends on how fast you can undo it. With one-button rollback and a health check, Friday is an ordinary day. With a hand-typed deploy you have never reversed, you are betting the weekend on a build you have not watched.
Two things a rollback will not save you from. Anything that already left the building, like emails sent or payments taken, cannot be recalled by restoring old code. And a rollback restores the previous version of your app, not the previous state of the world: caches, queues and unfinished background jobs stay where they were.