Docker Swarm Rolling Updates and Rollback
Change the image on a live service and watch the cluster replace tasks a few at a time. Proven end to end by the HTTP Server header changing under real traffic, then changing back on rollback.
Swarm and Multi-Host (optional) Guide 44 of 46 Advanced
- Cluster1 manager, 2 workers
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2 on every node
- Architectureamd64
- TimeAbout 14 min
- Reviewed22 August 2026
Tested on the versions above. Captured on a real three-node cluster. Node names, IDs and addresses are specific to it - match the shape of the output, not the values.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Swarm Manager (Leader) | 2 Core | 4 GB | 50 GB |
| DOCKER02 | 192.168.0.22 | Ubuntu 26.04 LTS | Swarm Worker | 2 Core | 4 GB | 50 GB |
| DOCKER03 | 192.168.0.23 | Ubuntu 26.04 LTS | Swarm Worker | 2 Core | 4 GB | 50 GB |
Before you start
- A published service on a cluster - guide 42 in this path.
- The routing mesh, so you can watch traffic during the update - guide 43 in this path.
-
What you are starting from
The running service is pinned to the digest its tag resolved to at creation. Record it - you want to be able to prove the update happened and that the rollback undid it.
bash Example session docker service inspect cg-web --format "image={{.Spec.TaskTemplate.ContainerSpec.Image}}"image=nginx:alpine@sha256:db35bfc6b2951e7f8a72db5db120288c12curl -s -m5 -D- -o /dev/null http://192.168.0.22:8080 | grep -i "^server:"Server: nginx/1.31.4Expected resultThe current digest, and the version actually being served.
Success conditionYou have a before value from OUTSIDE the cluster. The Server header is what real users get, which is a better witness than any inspect field.
-
Roll the update out gradually
service updatechanges the declaration and the cluster reconciles. Three flags govern the pace.--update-parallelismis how many tasks change at once,--update-delayis the pause between batches, and--update-order start-firstbrings the new task up BEFORE stopping the old one - which is what actually gives you zero downtime, at the cost of briefly running more replicas than you asked for.bash Example session docker service update --image nginx:1.27-alpine --update-parallelism 2 --update-delay 4s --update-order start-first cg-webverify: Waiting 1 seconds to verify that tasks are stable...verify: Service cg-web convergedExpected resultThe command working through the tasks in batches, then converging.
Success conditionIt converged. The default order is
stop-first, which briefly reduces capacity - fine for a background worker, not for a web service. -
Read the task history
service psshows the replacement chain. Each numbered task has the new image running and the previous one indented beneath as Shutdown. That history is per task slot, which is why you can see exactly what was replaced and where the new one landed - note task 1 moved node in the process.bash Example session docker service ps cg-web --format "table {{.Name}}\t{{.Image}}\t{{.Node}}\t{{.CurrentState}}" | head -7NAME IMAGE NODE CURRENT STATEcg-web.1 nginx:1.27-alpine ahm-docker03 Running 21 seconds ago \_ cg-web.1 nginx:alpine ahm-docker02 Shutdown 18 seconds agocg-web.2 nginx:1.27-alpine ahm-docker03 Running 21 seconds ago \_ cg-web.2 nginx:alpine ahm-docker02 Shutdown 18 seconds agocg-web.3 nginx:1.27-alpine ahm-docker03 Running 13 seconds agoExpected resultNew tasks Running with the old ones indented as Shutdown.
Success conditionYou can trace each slot's history. The
\_lines are not failures - they are the record of what was replaced. -
Prove it reached real traffic
The inspect field says what the cluster intends. The response header says what a user gets. This is the check worth building into a deploy script.
bash Example session docker service inspect cg-web --format "image={{.Spec.TaskTemplate.ContainerSpec.Image}}"image=nginx:1.27-alpinecurl -s -m5 -D- -o /dev/null http://192.168.0.22:8080 | grep -i "^server:"Server: nginx/1.27.5Expected resultThe served version changed from 1.31.4 to 1.27.5.
Success conditionReal traffic is being answered by the new image. Note the new image is a bare tag - only the ORIGINAL create resolved to a digest.
-
Watch the update state machine
The update is not instantaneous and the service records where it is.
updatingmeans batches are still being worked through,completedmeans done,pausedmeans a task failed and the update stopped. That last one is the important one - by default a failed update PAUSES and leaves you half-deployed.bash Example session docker service inspect cg-web --format "state={{.UpdateStatus.State}} msg={{.UpdateStatus.Message}}"state=updating msg=update in progress# other values: completed, paused, rollback_started, rollback_completedExpected resultA state and a human-readable message.
Success conditionYou know where to look after a deploy. A
pausedservice is the failure mode that quietly leaves two versions running. -
Roll back
service rollbackreturns the service to its previous specification - not to a tag you name, but to the exact spec it had before the last update, digest included. It rolls back using the same batching rules.bash Example session docker service rollback cg-webverify: Service cg-web convergeddocker service inspect cg-web --format "image={{.Spec.TaskTemplate.ContainerSpec.Image}}"image=nginx:alpine@sha256:db35bfc6b2951e7f8a72db5db120288c12docker service inspect cg-web --format "{{.UpdateStatus.State}}" -> rollback_completedExpected resultThe original digest restored, and 1.31.4 served again.
Verify it worked
bash Example session curl -s -m5 -D- -o /dev/null http://192.168.0.22:8080 | grep -i "^server:"Server: nginx/1.31.4Success conditionThe header went back. There is only ONE step of history - rolling back twice returns you to where you started, it does not walk further back.
-
Make failure roll back by itself
Better than reacting is not needing to.
--update-failure-action rollbackreverses a bad deploy automatically, and--update-monitorsets how long a new task must survive before it counts as healthy. Combine with a HEALTHCHECK and the cluster judges the deploy on readiness rather than on the process merely starting.bash docker service update --update-failure-action rollback --update-monitor 30s --update-max-failure-ratio 0.2 cg-web# a task that dies within 30s counts as a failure# more than 20% failing reverses the whole update automaticallyExpected resultThe policy stored on the service for every future update.
Success conditionDeploys become self-correcting. Without a healthcheck this only detects crashes, not a service that starts and cannot serve - see guide 26.
Troubleshooting
The update paused part way and two versions are running
Why: A task failed and the default failure action is
pause. The cluster stops rather than continuing to roll out something broken.Fix:Read why the task failed, then either fix forward or roll back. Set
--update-failure-action rollbackso this reverses itself next time.bash docker service ps cg-web --no-trunc --format "{{.Name}} {{.CurrentState}} {{.Error}}" | headThere was a gap in service during the update
Why: The default
stop-firstorder stops the old task before starting the new one, so capacity dips - and with a single replica it goes to zero.Fix:Use
--update-order start-firstand run more than one replica.bash docker service inspect cg-web --format "{{.Spec.UpdateConfig.Order}} parallelism={{.Spec.UpdateConfig.Parallelism}}"Re-running update with the same tag changes nothing
Why: The service is pinned to a digest. Pushing a new image to the same tag does not move the running service on its own.
Fix:Force resolution with
--imageagain, or--forceto make the cluster redeploy tasks regardless.bash docker service update --image myapp:latest --force myserviceRollback did not go back far enough
Why: Swarm keeps exactly one previous spec. Two updates and the older one is gone.
Fix:Deploy from a stack file in version control so any past state is reproducible - see guide 46.
bash docker service inspect cg-web --format "{{json .PreviousSpec.TaskTemplate.ContainerSpec.Image}}"