Docker & ContainersXXXVII Β· Orchestration TransitionThe orchestrators
Docker Swarm at a glance β what you get and what it costs
What you'll learn
- Describe the Swarm object model and how it maps onto a Compose file
- Reason about manager quorum and what happens when you lose it
- Identify the rolling-update defaults that reduce capacity or hide failure
- Weigh the ecosystem risk of choosing Swarm against its operational simplicity
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
Swarm mode is built into the Docker Engine you already run. There is no
separate control plane to install, no new CLI, and no new file format β
docker swarm init turns a host into a one-node cluster and
docker stack deploy takes a Compose file.
That makes it the cheapest possible step from single-host Compose to multi-host, and the cheapness is the entire argument for it. This lesson covers what you actually get, the defaults worth changing on day one, and the risk you take on by choosing it.
The object model
# On the first manager
docker swarm init --advertise-addr 192.0.2.11
# Read the join tokens on the manager
docker swarm join-token manager
docker swarm join-token worker
# On each additional node, with the token printed above
TOKEN=REPLACE_ME
docker swarm join --token "$TOKEN" 192.0.2.11:2377$ docker node lsID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
p2x9k1v... node01 Ready Active Leader 28.3.2
q7m4b8n... node02 Ready Active Reachable 28.3.2
r5t1w6z... node03 Ready Active Reachable 28.3.2
s9c3h2j... node04 Ready Active 28.3.2Illustrative output
Quorum is the thing to understand first
Managers keep the desired state in a Raft log. Writes require a majority:
with N managers, quorum is (N / 2) + 1.
| Managers | Quorum | Failures tolerated |
|---|---|---|
| 1 | 1 | 0 |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
Use an odd number. Going from 3 to 4 managers does not improve fault tolerance β both tolerate one failure β and it adds a node to every consensus round. Beyond 7, the write latency of consensus starts to cost more than the extra redundancy is worth.
Compose is nearly, but not entirely, the same file
docker stack deploy -c compose.yml myapp accepts a Compose file, and the
differences are the source of most first-week frustration:
| Compose key | In a stack |
|---|---|
deploy: | Ignored by docker compose, used by docker stack deploy |
build: | Ignored β the image must already be in a registry every node can reach |
depends_on: | Accepted, but the condition: forms are ignored; there is no startup ordering |
restart: | Ignored β use deploy.restart_policy |
container_name: | Ignored β task names are generated |
ports: | Published through the routing mesh unless you set mode: host |
env_file: | Read on the machine running the deploy, not on the nodes |
The second difference nobody expects: there is no startup ordering. Swarm starts every service at once and relies on applications retrying their dependencies. An application that exits when its database is not yet reachable will crash-loop through the first minute of every deploy. If you cannot fix the application, its entrypoint needs to wait and retry.
Defaults worth changing on day one
services:
web:
image: registry.example.com/myorg/myapp:1.4.2
deploy:
replicas: 4
update_config:
parallelism: 1
delay: 60s
order: start-first
failure_action: rollback
rollback_config:
parallelism: 0
order: stop-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Note reservations versus limits: the scheduler places tasks using
reservations, and the kernel enforces limits. A service with limits
and no reservations can be scheduled onto a node that cannot actually satisfy
it, and you find out when the OOM killer does.
The routing mesh, and the source IP
By default a published port is reachable on every node in the swarm, even nodes not running a task for that service. The ingress network forwards the request to a node that is.
That is convenient β any nodeβs address works as a load balancer target β and it has a cost: by the time the request reaches your container, the source address is the ingress networkβs, not the clientβs. Access logs show internal addresses. Rate limiting by IP does not work. Geo-lookup does not work.
Two ways out, both with trade-offs:
mode: hostpublishing. The port is bound only on nodes running a task, and the real source IP is preserved. You lose the βany node worksβ property, so your external load balancer must know which nodes are running the service.- A reverse proxy at the edge that sets
X-Forwarded-For, with the application configured to trust it. Standard practice, and the usual answer.
The honest costs
Against those: a Swarm cluster is three Docker hosts and a docker swarm init. There is no separate control plane to patch, no CNI plugin to choose,
no ingress controller to operate, and no quarterly upgrade treadmill. For a
team of two running a dozen services, that difference is not small.
Knowledge check
Knowledge check Β· 4 questions
Q1. A five-manager swarm loses three managers. What happens?
Q2. The `deploy:` section of a Compose file is ignored by `docker compose up` but used by `docker stack deploy`.
Q3. Which Swarm defaults are worth changing for a production service? Select all that apply.
Q4. Why do access logs from a service published through the Swarm routing mesh show internal addresses instead of client IPs?
Passing score: 75%. Answers are checked in this browser.