CertGrid CertGrid
Hands-on Lab·Docker

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

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.

Three hosts, because a swarm needs more than one. DOCKER01 is the single manager and Leader; the other two are workers. One manager means no fault tolerance, which several of these guides make a point of - three would tolerate one failure.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSSwarm Manager (Leader)2 Core4 GB50 GB
DOCKER02192.168.0.22Ubuntu 26.04 LTSSwarm Worker2 Core4 GB50 GB
DOCKER03192.168.0.23Ubuntu 26.04 LTSSwarm Worker2 Core4 GB50 GB

Before you start

  1. 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.4

    Expected 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.

  2. Roll the update out gradually

    service update changes the declaration and the cluster reconciles. Three flags govern the pace. --update-parallelism is how many tasks change at once, --update-delay is the pause between batches, and --update-order start-first brings 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 converged

    Expected 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.

  3. Read the task history

    service ps shows 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 ago

    Expected 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.

  4. 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.5

    Expected 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.

  5. Watch the update state machine

    The update is not instantaneous and the service records where it is. updating means batches are still being worked through, completed means done, paused means 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_completed

    Expected resultA state and a human-readable message.

    Success conditionYou know where to look after a deploy. A paused service is the failure mode that quietly leaves two versions running.

  6. Roll back

    service rollback returns 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_completed

    Expected 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.4

    Success 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.

  7. Make failure roll back by itself

    Better than reacting is not needing to. --update-failure-action rollback reverses a bad deploy automatically, and --update-monitor sets 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 automatically

    Expected 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

Official sources