Zero-Downtime Deploys on a Single VPS
You do not need a cluster for zero-downtime deploys. Here is how to do blue-green and rolling deploys on one VPS so users never see a dropped request.
You can deploy new code to a single VPS without dropping a single request, no cluster required. The trick is to start the new version alongside the old one, wait until it is healthy, then flip the reverse proxy to it and retire the old container. That is a blue-green deploy, and it runs fine on one box. The naive approach, stop the old container and start the new one, gives users a few seconds of errors on every deploy. A few extra steps removes that gap entirely.
Why the simple deploy drops requests
The default self-host deploy is docker compose up -d --build, which stops the running container and starts the new one in its place. For the seconds between stop and healthy-start, there is nothing serving traffic. Requests in that window get connection errors or a proxy 502. On a low-traffic side project you might not notice. On anything real, every deploy is a small visible outage, and you end up deploying at 2am to avoid it. That is a self-inflicted constraint you do not need to live with.
The fix is to never have a moment where zero healthy containers exist. You always keep the old version serving until the new version is proven ready, then switch. The old code overlaps the new code for a few seconds, and users never see the seam.
How do I do a blue-green deploy on one box?
Blue-green means running two versions side by side and flipping between them. On a single VPS with a reverse proxy in front, the flow is:
- Build and start the new version as a second container, on a different internal port, while the old one keeps serving.
- Hit the new container's health check until it reports ready. Do not flip until it is genuinely up, migrations applied and app responding.
- Update the reverse proxy to point at the new container and reload the proxy. A reload is graceful: existing connections finish, new ones go to the new version.
- Stop and remove the old container once traffic has drained.
Caddy and nginx both reload without dropping in-flight connections, which is what makes the flip seamless. This layers cleanly onto one reverse proxy for every app on the box: the proxy is the switch. Wrap the whole sequence in a script so a deploy is one command, and fold it into your git-push deploy on your own box flow so pushing a branch runs it automatically.
The health check is the whole thing
Zero-downtime lives or dies on the health check. If you flip to the new container before it is actually ready, you have just moved the outage, not removed it. The new version must expose an endpoint that returns healthy only when it can truly serve: database connection open, migrations applied, dependencies reachable. Your deploy script polls that endpoint and refuses to flip until it passes, with a timeout that aborts and leaves the old version running if the new one never gets healthy.
That last part is the safety net. A bad build should fail the deploy and leave production untouched, not take the site down. Because the old container is still running until the flip, a failed health check just means you roll back by doing nothing. Keep the previous image tagged so an instant revert is re-pointing the proxy at the old container, which is the reversibility principle behind owning your deploy pipeline end to end.
Handle database migrations carefully
The one thing zero-downtime deploys cannot paper over is a breaking database migration. If the new code needs a schema the old code cannot read, the overlap period breaks the old version. The discipline is backward-compatible migrations: add columns before you use them, deploy code that writes both old and new shapes, then remove the old shape in a later deploy. Each step is compatible with the version running next to it.
This is more thought than a stop-the-world deploy, but it is what lets you ship during the day without a maintenance window. Test the whole sequence against staging on the same VPS first, so the blue-green flip and the migration are both proven before they touch real users.
None of this needs a managed platform or a Kubernetes rollout controller. It is a reverse proxy, a health check, and a short script, running on the HostSSH box you already own. The result is that deploys stop being scary events you schedule around and become something you do casually in the middle of the afternoon, because no user will ever notice.