Before this:Scaling & load balancing
Zero-downtime deploys
Key takeaways Replacing a running version without dropping requests has three moving parts: bring the new version up and gate it on a health check before it takes traffic; drain connections from the old version so its in-flight requests finish; and keep the ability to roll back instantly if the new version misbehaves. Multi-instance setups do this seamlessly; a single instance uses a brief graceful restart.
Release strategies named the patterns — rolling, blue-green, canary. This lesson is the mechanics that make any of them lose zero requests: health gating, draining, and rollback. They lean directly on the health checks and load balancer you already have.
Gate the rollout on health
The cardinal rule: never send traffic to a version that hasn’t proven it’s ready. A new instance starts, but it might still be loading config, opening its database, or warming caches. So the load balancer waits until the instance passes its health check — GopherTrunk exposes exactly this endpoint — before routing anything to it:
# poll the new instance until it's healthy, then it joins rotation
until curl -fsS http://127.0.0.1:8081/api/v1/health >/dev/null; do sleep 1; done
echo "new instance healthy — adding to the load balancer"
If the new version never goes healthy, it never gets traffic, and users stay on the good old version. The health check is the gate that makes a rollout safe.
Drain the old version
Once the new version is serving, retire the old one gracefully. Draining tells the load balancer to stop sending the old instance new requests while letting its current ones finish. The app cooperates by handling the shutdown signal — finishing in-flight work before exiting rather than dying mid-request:
A well-behaved server catches SIGTERM, stops accepting new connections, finishes what’s
in flight, then exits — “graceful shutdown.” Orchestrators and load balancers give it a
grace period to do so.
Always be able to roll back
Even a health-gated, drained rollout can ship a subtle bug that only shows under real traffic. So keep the previous versioned artifact one command away:
# roll straight back to the known-good version
docker compose pull && docker tag ghcr.io/example/app:1.4.1 ghcr.io/example/app:current
docker compose up -d # recreate on the previous image
This is why you version and keep images — rollback is just deploying the last good one. Blue-green makes rollback instant (flip back to blue); rolling makes it a reverse rollout. Either way, “how do I undo this?” must have an answer before you deploy.
The single-instance reality
All of the above assumes multiple instances. One GopherTrunk instance bound to one USB
radio can’t run two copies of itself — the radio is a single physical resource. So the
honest answer for a single instance is a brief graceful restart: docker compose up -d
recreates the container on the new image, and systemctl restart does the same for the
systemd unit, each a moment’s gap while the process swaps. That’s a perfectly fine
tradeoff for a single-user scanner — recreate
with a short, honest downtime beats pretending you have a fleet you don’t.
Quick check: what does connection draining accomplish during a deploy?
Recap
- Gate the rollout on a health check — no traffic until the new version proves it’s ready.
- Drain the old version: stop new requests to it, let in-flight ones finish, then
exit gracefully on
SIGTERM. - Keep rollback one command away by versioning and retaining images.
- A single instance can’t be truly zero-downtime; a fast graceful restart (
compose up -d,systemctl restart) is the honest, fine choice.
Next up: keeping a deployment healthy over time — monitoring and updates.
Frequently asked questions
How do you deploy without downtime?
Bring the new version up alongside the old, wait until it reports healthy before sending it traffic, then drain the old version — let its in-flight requests finish while the load balancer stops sending it new ones — and only then stop it. Rolling and blue-green strategies both do this. On a single instance you can’t fully avoid a gap, but a fast graceful restart keeps it to a moment.
What is connection draining?
Draining means telling the load balancer to stop sending an instance new requests while letting the requests it’s already handling finish. Without draining, stopping an old instance kills its in-flight requests and users see errors. With draining, the old instance quietly finishes its work and then exits cleanly, so no request is dropped during the swap.