CertGrid CertGrid
Hands-on Lab·Docker

Docker Swarm Services and Scheduling

A service is a desired state, not a container. Create one with six replicas, watch the scheduler spread them two per node across three machines, scale it, and see what a task actually is.

Swarm and Multi-Host (optional) Guide 42 of 46 Intermediate

Tested on the versions above. Captured on a real three-node cluster. Node IDs, IP addresses and join tokens are specific to that cluster - tokens shown here are truncated deliberately and were rotated afterwards. 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. A service is a declaration

    docker run starts one container and you own its fate. docker service create states what should be true - six copies of this image, this port published - and the cluster keeps making it true. If a task dies or a node vanishes, the scheduler replaces it without you.

    bash Example session
    docker service create --name cg-web --replicas 6 --publish 8080:80 nginx:alpineverify: Waiting 1 seconds to verify that tasks are stable...verify: Service 2hx8sy5eax46d8lwst076dvp8 converged

    Expected resultThe command blocking until the service converges, then a service ID.

    Success conditionconverged means the actual state matches what you asked for. That word is the whole model in one line.

  2. Replicas are spread across the cluster

    The scheduler places tasks itself. With six replicas on three nodes it produced an exactly even spread - two each - because its default strategy prefers the node with the fewest tasks for the service. You did not choose where anything runs.

    bash Example session
    docker service ps cg-web --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}"NAME       NODE           CURRENT STATEcg-web.1   ahm-docker03   Running 28 seconds agocg-web.2   ahm-docker02   Running 20 seconds agocg-web.3   ahm-docker01   Running 18 seconds agocg-web.4   ahm-docker03   Running 28 seconds agocg-web.5   ahm-docker02   Running 19 seconds agocg-web.6   ahm-docker01   Running 17 seconds ago

    Expected resultSix tasks, evenly distributed.

    Verify it worked

    bash Example session
    docker service ps cg-web --format "{{.Node}}" | sort | uniq -c      2 ahm-docker01      2 ahm-docker02      2 ahm-docker03

    Success conditionTwo per node. Note the manager is running tasks too - by default it is a worker as well.

  3. Task, service, container

    Three words that get used loosely. The SERVICE is the declaration. A TASK is one slot in it - cg-web.3 - and the scheduler assigns it to a node. A CONTAINER is what actually runs that task. A task is never moved: if its node fails, the task is marked shutdown and a NEW task takes its place, which is why numbering stays stable while placement changes.

    bash Example session
    docker service lsID             NAME      MODE         REPLICAS   IMAGE          PORTS2hx8sy5eax46   cg-web    replicated   6/6          nginx:alpine   *:8080->80/tcp# 6/6 = running/desired. Anything less means the cluster has not converged.

    Expected resultThe replica count reported as running over desired.

    Success conditionYou read 6/6 as a health signal. 4/6 means two tasks cannot be placed or keep failing.

  4. Swarm pins the image to a digest

    A useful detail people miss: the service records the digest the tag pointed to at creation time, not the tag. Every node therefore runs exactly the same image even if the tag is repointed upstream mid-deploy. It is the digest pinning from guide 20, applied automatically.

    bash Example session
    docker service inspect cg-web --format "image={{.Spec.TaskTemplate.ContainerSpec.Image}}"image=nginx:alpine@sha256:db35bfc6b2951e7f8a72db5db120288c127ffaeeb4a6d4b95a26fead017d5913

    Expected resultThe tag you typed, plus the digest it resolved to.

    Success conditionYou understand why re-running service update --image nginx:alpine can still change the running image - the tag may now point somewhere else.

  5. Scaling is a one-word change to the declaration

    Scaling down removes tasks; scaling up creates them, and the scheduler rebalances as evenly as it can. Note it does NOT redistribute existing tasks to make room - placement decisions are made per task as it is created.

    bash Example session
    docker service scale cg-web=3verify: Service cg-web convergeddocker service ps cg-web --filter desired-state=running --format "{{.Node}}" | sort | uniq -c      1 ahm-docker01      1 ahm-docker02      1 ahm-docker03

    Expected resultThree tasks, still one per node.

    Success conditionThe spread held. --filter desired-state=running matters here - without it you also see the shutdown tasks from the scale-down.

  6. Constraining where tasks may run

    When placement does matter - a database that must stay off managers, a service that needs specific hardware - constraints express it declaratively. Built-in labels cover role, hostname and platform; you can add your own to nodes.

    bash
    docker service create --name cg-probe --constraint node.role==manager alpine:3.22 sleep 900verify: Waiting 1 seconds to verify that tasks are stable...verify: Waiting 1 seconds to verify that tasks are stable...verify: Service uif8lntww1b4ky19e3hm8lqah converged

    Expected resultA service that only ever runs where you allowed.

    Success conditionThe task lands on a permitted node. An unsatisfiable constraint leaves the service at 0/1 forever rather than erroring - see the troubleshooting below.

  7. Global services: one per node, automatically

    The other mode. A global service runs exactly one task on every eligible node, and a node joining later gets one without you scaling anything. This is the right shape for agents - log shippers, metrics collectors, node exporters.

    bash
    docker service create --mode global --name cg-agent alpine:3.22 sleep 900# no --replicas: the count is "however many nodes there are"docker service ls --format "{{.Name}} {{.Mode}} {{.Replicas}}"# global services report as N/N where N is the eligible node count

    Expected resultA service whose replica count tracks the cluster size.

    Success conditionYou can choose the mode from the requirement: replicated for capacity, global for per-node agents.

  8. Clean up

    Removing a service removes all of its tasks across every node - you do not visit the machines.

    bash
    docker service rm cg-web cg-probe cg-agentcg-webcg-probecg-agent

    Expected resultEach name echoed back.

    Success conditiondocker service ls is empty, and docker ps on every node is clear.

Troubleshooting

Official sources