Zero-downtime deploys on modest infrastructure
Seamless releases are usually presented as a property of large platforms. The mechanism is simple enough to implement on two servers, or even one.
Deploying without interrupting anyone is often treated as something you get by adopting a platform. The underlying mechanism is much simpler than that, and it works at any size.
The requirement is only this: at every moment during a release, something that works is serving traffic, and requests already in flight are allowed to finish.
The mechanism, in general terms
Every zero-downtime strategy is a variation on the same four steps.
- Start the new version alongside the old one.
- Confirm the new version is genuinely ready, not merely started.
- Move traffic to it.
- Stop the old version, after giving in-flight requests time to complete.
If any step fails, the old version is still serving and you have a failed deployment rather than an outage. That property is the whole point, and it is what distinguishes this from stopping the service and starting it again quickly.
With two or more servers
The straightforward case. A load balancer sits in front, and you take instances out of rotation one at a time.
For each instance: mark it unhealthy or deregister it, wait for connection draining to complete, deploy the new version, wait for the health check to pass, return it to rotation. Repeat.
Three details determine whether this actually works.
The drain timeout must exceed your longest normal request. Set too short, in-flight requests are cut and users see errors during every deployment. This is the most common reason a supposedly seamless deployment is not.
The health check must verify readiness, not just liveness. An application that has started but not yet connected to its database will fail requests if returned to rotation too early.
Remaining capacity must handle the traffic. Taking one of two instances out means the other carries everything. If they were both at seventy percent, you now have a problem. Deploy during lower traffic, or run enough instances that losing one is comfortable.
With one server
Frequently assumed impossible. It is not, if a reverse proxy sits in front of the application.
Start the new version on a different internal port. Wait for it to become healthy. Reconfigure the proxy to point at the new port and reload it, which most proxies do without dropping connections. Then stop the old version after a drain period.
The proxy is doing the load balancer’s job on the same machine. The requirement is enough memory to run both versions briefly, which is usually the only real constraint.
Container tooling makes this easier still, since running two versions side by side on internal ports is the natural arrangement.
Blue-green and canary
Two named variations worth knowing.
Blue-green runs two complete environments and switches all traffic between them at once. Rollback is switching back, which is fast and complete. The cost is running double infrastructure during the changeover, and the risk is that a problem affects everyone at the moment of the switch.
Canary sends a small share of traffic to the new version, watches error rates and latency, and increases the share if things look healthy. Problems affect a small proportion of users rather than all of them. It requires the ability to split traffic by percentage and enough traffic for a small share to be statistically meaningful, which rules it out for low-traffic services.
For most small deployments, rolling instance-by-instance is the right default. Blue-green suits releases that are risky or hard to reverse. Canary suits high-traffic services where a subtle regression would otherwise reach everyone.
The database is the hard part
Application deployment is straightforward. Schema changes are where zero-downtime attempts fail, because for a period both versions of the application are running against one database.
The rule that makes it work: every schema change must be compatible with the currently deployed application version as well as the new one.
That forces destructive changes to be split across releases.
Adding a column: safe, provided it is nullable or has a default, and provided the old code ignores it.
Removing a column: two releases. First deploy code that no longer references it. Then, in a later release, drop it.
Renaming a column: four steps across releases. Add the new one, write to both, backfill, switch reads to the new one, and only then remove the old one.
Adding an index: use the concurrent or online variant where the database offers one, because a plain index build can lock the table for a long time on a large one.
Changing a column type: usually add a new column, migrate, switch, remove.
This is more work than a single migration, and it is the difference between a deployment and a maintenance window.
Requests that outlive the deployment
Long-running requests, file uploads, streaming responses and websocket connections do not fit neatly into a drain window.
For websockets, the practical approach is to have clients reconnect automatically and to accept a brief reconnection. For long uploads, either extend the drain timeout to accommodate them or move them to a separate service deployed less often. For background jobs, ensure they are idempotent and can be retried, so a worker stopping mid-job is recoverable.
Verifying it works
The failure mode of this whole exercise is believing you have zero-downtime deployment when you do not, because nobody has checked.
Run a simple load generator against the service continuously, deploy, and count errors. Zero errors during a deployment is the pass condition. Anything else means the drain timeout, the health check or the readiness definition needs work.
Do this once when you set it up, and again whenever the deployment process changes. It takes ten minutes and it is the only way to know.
Where you run this matters less than how, but if the provider is still open, the cloud account catalogue compares the options side by side.
For the vendor’s own reference on the services involved here, see the UpCloud documentation.
Reading about storage behaviour only goes so far; measuring it on a live server goes further. We stock UpCloud accounts in a single configuration, and the cloud account catalogue covers nine other providers.
Questions people ask
Can I do zero-downtime deployment on a single server?
Yes, with a reverse proxy in front. Start the new version on a different internal port, wait for it to become healthy, reconfigure and reload the proxy, then stop the old version after draining. The only real constraint is enough memory to run both briefly.
Why do my deployments still produce errors?
Most often a drain timeout shorter than your longest normal request, so in-flight requests are cut when an instance is removed. The second most common cause is a health check that reports ready before dependencies are connected.
How do I change a database schema without downtime?
Make every change compatible with both the old and new application versions, which means splitting destructive changes across releases. Removing a column takes two releases; renaming one takes several.
How do I know my deployment is genuinely seamless?
Run a continuous load generator against the service, deploy, and count errors. Zero errors is the pass condition; anything else means the drain timeout or readiness check needs work.


