Skip to Content
Zero-Downtime Restart With Compose on One Host
Advanced
0 likes
Tutorials

Zero-Downtime Restart With Compose on One Host

Deploy a new version with blue and green copies, a proxy reload and a switch script, and prove no request failed. After this, a deployment on one host does not drop users.

docker compose up -d with a new image stops the old container before the new one is ready, so users get errors for a few seconds. On one host you can avoid that with two copies of the service and a proxy: start the new copy, wait until it is healthy, switch the proxy, stop the old copy. This page turns that into a script you can run for every deployment. It is the short version of the rolling deployment lesson in the Docker Advanced course. Run on Docker Desktop 4.85.0, Engine 29.6.2 and Compose v5.3.1.

The setup

The app is nginx serving its version, with a health check. In your project this is your own image.

FROM nginx:1.30-alpine
ARG VERSION=1
RUN echo "version $VERSION" > /usr/share/nginx/html/index.html
HEALTHCHECK --interval=1s --timeout=2s --retries=3 CMD wget -qO- http://127.0.0.1/ > /dev/null || exit 1
services:
  blue:
    image: zd-app:${BLUE_VERSION:-1}
  green:
    image: zd-app:${GREEN_VERSION:-1}
  proxy:
    image: nginx:1.30-alpine
    ports:
      - "127.0.0.1:8472:80"
    volumes:
      - ./proxy.conf:/etc/nginx/conf.d/default.conf:ro
      - ./active:/etc/nginx/active:ro
networks:
  default:
    name: zd-net
server {
    listen 80;
    resolver 127.0.0.11 valid=1s ipv6=off;
    include /etc/nginx/active/upstream.conf;
    location / {
        proxy_pass $upstream;
    }
}
set $upstream http://blue;

The switch script

param([Parameter(Mandatory)][ValidateSet("blue", "green")][string]$To, [Parameter(Mandatory)][string]$Version)
$from = if ($To -eq "blue") { "green" } else { "blue" }
Set-Item "Env:$($To.ToUpper())_VERSION" $Version
docker compose --progress quiet up -d --wait $To
if ($LASTEXITCODE -ne 0) { throw "$To did not become healthy, nothing was switched" }
Set-Content -Encoding ascii active\upstream.conf "set `$upstream http://$To;"
docker compose exec proxy nginx -t
if ($LASTEXITCODE -ne 0) { throw "proxy config test failed" }
docker compose exec proxy nginx -s reload
docker compose --progress quiet stop $from
"switched to $To ($Version)"

It refuses to switch if the new copy never becomes healthy, and it tests the proxy config before reloading.

Run a deployment

docker build -q -t zd-app:1 --build-arg VERSION=1 zd-app | Out-Null
docker build -q -t zd-app:2 --build-arg VERSION=2 zd-app | Out-Null
Set-Location zd-stack
docker compose up -d --wait blue proxy 2>$null
"before: " + (Invoke-RestMethod http://127.0.0.1:8472/)
.\switch.ps1 -To green -Version 2
"after: " + (Invoke-RestMethod http://127.0.0.1:8472/)
before: version 1

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
2026/10/20 17:23:52 [notice] 59#59: signal process started
switched to green (2)
after: version 2

Prove nothing was dropped

Switch back to version 1 while a client container asks for the page 150 times:

docker run -d --name zd-client --network zd-net alpine:3.24 sh -c 'for i in $(seq 1 150); do wget -qO- -T 1 http://proxy/ 2>/dev/null || echo failed; sleep 0.04; done | sort | uniq -c' | Out-Null
Start-Sleep -Seconds 1
.\switch.ps1 -To blue -Version 1 | Out-Null
docker wait zd-client | Out-Null
docker logs zd-client
docker rm zd-client | Out-Null
docker compose down 2>$null
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
2026/10/20 17:23:57 [notice] 103#103: signal process started
     64 version 1
     86 version 2

No "failed" lines. The rollback is the same script with the other colour.

Before you use it for real

  • Database changes must work with both versions at once, because for a few seconds both run.
  • Keep sessions outside the app containers, or users on the old copy lose them at the switch.
  • docker compose stop gives the old copy 10 seconds to finish by default. Use -t for longer requests.

⚠ Warning: Without a health check in the image, --wait only waits for the container to start, not for the app to be ready, and the switch can send traffic to an app that is still loading.

Related course: Docker Advanced.

About

Deploy a new version with blue and green copies, a proxy reload and a switch script, and prove no request failed. After this, a deployment on one host does not drop users.

Responsible
Christopher R
Last Update
08/14/2026
Reading Time
4 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.