Zero-downtime deploys get talked about as if they require a cluster and a load balancer. On a single VPS, with plain Docker, you can get most of the way there with three ideas: build before you switch, health-check before you route, and keep the old version around until the new one proves itself.
The naive deploy (and why it has downtime)
git pull && docker compose up -d --build stops the running container, builds the new image, and starts it — in that order, more or less. Between "stop" and "new container ready to serve traffic," requests fail. For a low-traffic app that might be a blip nobody notices; for anything with real usage, it's a visible outage on every deploy.
The fix: build first, switch second
- Build the new image while the old container is still serving traffic. Nothing about building an image requires stopping the running app.
- Start the new container alongside the old one, on a different internal port or container name, both attached to the same network.
- Health-check the new container — hit its health endpoint directly (container-to-container, not through the public route) until it responds successfully, with a retry budget and a timeout.
- Only then flip the route. If you're using Traefik or Caddy, this is updating which container the router's labels point to, which takes effect in milliseconds.
- Stop the old container once the new one has been serving successfully for a bit — not immediately, so you have an instant rollback target if something looks wrong.
Handling database migrations
The genuinely hard part. Additive migrations (new columns, new tables) are safe to run before the switch. Destructive ones (dropping a column the old code still reads) need a two-deploy pattern: stop reading/writing the old column in this deploy, drop it in the next one. There's no way around this with a single-VPS setup — or any setup — other than expand-and-contract migrations.
Rollback
If step 5 above is done right, rollback is just "route back to the previous container," which is why keeping the last few images tagged by commit hash matters — instant rollback beats git-revert-and-rebuild every time, especially under pressure.
Automating this
This exact sequence — build, start alongside, health-check, atomic route switch, keep-old-for-rollback — is what DeployOS's deploy pipeline does on every push, with one-click rollback to any previous deployment if a health check fails or something looks wrong after the fact. If you're currently doing the naive stop-and-restart deploy, this is the single highest-leverage change you can make to your deploy process.