Zero-downtime rolling updates for Docker Compose — deploy new versions of your services without dropping a single request. No Docker Swarm, no Kubernetes required.
Docker Compose does not have a built-in rolling update command. Running docker compose up recreates containers one by one with a brief downtime gap. docker compose pull && docker compose up -d replaces all containers simultaneously, causing a full restart.
This project fills that gap with a simple bash script approach:
- No orchestrator required — works with plain Docker Compose on any machine or VM
- No external dependencies — pure bash + Docker CLI
- Production-tested — handles graceful shutdown, Nginx DNS staleness, and health-check gating
- Drop-in — copy two scripts into any existing Docker Compose project
For each service, the update follows three steps:
- Scale Up — start a new container alongside the existing one (
--no-recreatekeeps the old one alive) - Health Check — wait until the new container passes Docker's health check
- Remove Old — stop and remove the old container only after the new one is confirmed healthy
At no point is the service fully down. Traffic flows to the old container until the new one is ready.
Initial State:
:8182 → nginx → service1 (v1)
service2 (v1) [independent service, not behind nginx]
Updating service1:
:8182 → nginx → service1 (v1, old) + service1 (v2, new)
service2 (v1)
↓ (new container passes health check)
:8182 → nginx → service1 (v2)
service2 (v1)
Updating service2:
:8182 → nginx → service1 (v2)
service2 (v1, old) + service2 (v2, new)
↓ (new container passes health check)
:8182 → nginx → service1 (v2)
service2 (v2)
| This project | Docker Swarm | Kubernetes | |
|---|---|---|---|
| Rolling updates | ✅ | ✅ native | ✅ native |
| Requires orchestrator | ❌ | ✅ Swarm mode | ✅ K8s cluster |
Works with docker-compose.yaml |
✅ | ❌ needs stack file | ❌ needs manifests |
| Setup complexity | Low | Medium | High |
| Best for | Single host / small teams | Multi-host clusters | Large-scale infra |
If you are already on Docker Swarm or Kubernetes, use their native rolling update features. If you are on a single VM or a small setup using Docker Compose, this project is for you.
mkdir -p scripts
cp scripts/update.sh scripts/check_status.sh ./scripts/
chmod +x ./scripts/*.shServices must have a healthcheck so the script knows when the new container is ready:
services:
api:
image: your-api:v1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 5s
timeout: 2s
retries: 3
start_period: 10s
worker:
image: your-worker:v1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 5s
timeout: 2s
retries: 3
start_period: 10sdocker stop sends SIGTERM before killing a container. Your application must finish in-flight requests before exiting — otherwise the one request being handled at the moment of shutdown is dropped.
Example for Node.js:
process.on('SIGTERM', () => {
server.closeIdleConnections(); // drop Nginx keep-alive pool immediately
server.close(() => process.exit(0)); // wait for active request to finish
});For other languages: Python (signal.signal(signal.SIGTERM, ...)), Go (signal.NotifyContext), Java (shutdown hooks).
A static upstream {} block caches backend IPs at startup and never re-resolves, causing Nginx to keep routing to removed containers. Use Docker's embedded DNS resolver with a variable in proxy_pass instead:
resolver 127.0.0.11 valid=1s ipv6=off;
location / {
set $upstream your-service:3000;
proxy_pass http://$upstream;
proxy_connect_timeout 500ms;
proxy_next_upstream error timeout;
proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 2s;
}See example/nginx/nginx.conf for a complete, ready-to-use configuration.
# Update multiple services in sequence
./scripts/update.sh api worker
# Update a single service
./scripts/update.sh api.
├── scripts/
│ ├── update.sh # Main orchestration script
│ └── check_status.sh # Health check and cleanup
│
├── example/ # Complete working example: Node.js + Nginx
│ ├── docker-compose.yaml
│ ├── Dockerfile
│ ├── server.js
│ ├── nginx/
│ │ └── nginx.conf
│ ├── monitor.sh # Real-time request monitoring
│ └── README.md
│
└── README.md # This file
- Docker Engine 20.10+ and Docker Compose v3.8+
- Services must define a
healthcheck - The health check command must be available inside the container (e.g.,
curl,wget) - Applications must handle
SIGTERMgracefully
- Health Checks: Must be configured both in
Dockerfileanddocker-compose.yaml - Startup Time: Adjust
start_periodfor slower-starting services to avoid false unhealthy reports during boot - Graceful Shutdown: Services must handle
SIGTERMand drain in-flight requests before exiting.docker stopsends SIGTERM and waits 10 seconds before force-killing with SIGKILL. - Nginx DNS: Use Docker's embedded resolver (
127.0.0.11) with a variable inproxy_pass. Do not usedocker network disconnectbefore stopping — it makes the container's IP silently unreachable (packets dropped), causing Nginx to hang on connection rather than fail fast and retry.
Orchestrates the rolling update for one or more services.
Usage: ./scripts/update.sh <service1> [service2] [...]
- Scales each service to 2 replicas with
--no-recreate - Calls
check_status.shto wait for health and clean up - Exits with code 1 on first failure, leaving the stack in a safe state
Waits for new containers to become healthy, then removes the old one.
Usage: ./scripts/check_status.sh <service_name>
- Waits up to 30 seconds for containers to leave the
startingstate - Removes any
unhealthycontainers and returns exit code 1 - Removes the oldest container once all remaining containers are healthy
Health check fails immediately
- Confirm the health check binary exists:
docker exec <id> which curl - Try the check manually:
docker exec <id> curl -f http://localhost:3000/ - Increase
start_periodif the app takes time to boot
Old container is not removed
- Confirm containers have Docker Compose labels:
docker inspect <id> | grep com.docker.compose - Check Docker socket permissions
Requests fail during update (HTTP 000 or 502)
- Ensure Nginx uses
resolver 127.0.0.11 valid=1sand a variable inproxy_pass - Add
proxy_next_upstream error timeoutso Nginx retries on the healthy container - Confirm the application handles SIGTERM and does not exit while a request is in flight
See example/ for a complete, runnable demonstration with a Node.js server, Nginx, and a monitoring script that verifies zero-downtime behavior in real time.