Filling the Swarm cluster with services: the Service-Task-Container relationship, deployment via docker service and docker stack, cross-node overlay networks, Swarm secrets & configs, and zero-downtime rolling updates and rollbacks with control over parallelism and delay.

After successfully joining several servers into one Swarm cluster in episode 16 — managers with Raft consensus, workers, quorum, and TLS between nodes — in this episode we start using it: defining what may run in the cluster, how many, where, and how to update it without taking users offline. A cluster without services is just a collection of servers greeting each other; services are what turn it into a platform.
The most important mental shift in this episode: for 15 episodes you've been thinking in containers — "I'm running a web container". In Swarm, you think in services — "I want 3 web replicas that always exist". Containers are merely a byproduct; what you declare is the desired state, and Swarm's job is to make it real continuously. This isn't just a writing style — it's the orchestration philosophy that separates "running an application" from "operating an application". If one container dies, docker run doesn't care; a Swarm service will immediately create a replacement task.
In this episode we'll dissect the Service-Task-Container relationship, deploy with docker service and docker stack, connect cross-node containers with overlay networks, store secrets via Swarm secrets, and close with what's most valuable in production: zero-downtime rolling updates and rollbacks. Get your cluster from episode 16 ready — every example here runs on it.
Before writing commands, lock in these three levels, because this entire episode stands on them:
New → Assigned → Preparing → Running → Complete/Shutdown/Failed.The right analogy: a service is a housing blueprint (3 houses, type A), a task is the worksheet for building one house, and a container is the standing house. If one house collapses, the contractor (Swarm) grabs that worksheet and rebuilds it — no new blueprint needed.
The first command to fill the cluster — and note its similarity to docker run, only ending with a wish about the replica count:
docker service create \
--name web \
--replicas 3 \
-p 80:80 \
nginx:1.27-alpineNow inspect its status from three sides — service, tasks, and logs:
docker service ls
docker service ps webID NAME IMAGE NODE DESIRED STATE CURRENT STATE
v9... web.1 nginx:1.27-alpine node-1 Running Running about a minute ago
p2... web.2 nginx:1.27-alpine node-2 Running Running about a minute ago
q7... web.3 nginx:1.27-alpine node-3 Running Running about a minute agoLook at the NODE column: all three tasks are spread across three different nodes — Swarm automatically balances the load. Now let's test the power of orchestration: force-kill one container, then watch Swarm replace it:
docker ps --filter name=web -q | head -1 | xargs docker kill
docker service ps webWithin seconds, a new task web.4 appears on another node — and the replica count is back to 3. This is self-healing: because you declared --replicas 3, Swarm makes it reality without your intervention. Imagine having to do this manually with docker run in the middle of the night — this is why orchestration was built.
Other commands you'll use often: docker service logs web (aggregated logs of all tasks), docker service scale web=5 (change replica count anytime), and docker service rm web (delete entirely):
docker service scale web=5
docker service logs --tail 50 web
docker service rm webA web task on node-1, a web task on node-2, a web task on node-3 — how do they and other services talk to each other? The answer is the overlay network: a virtual network stretched across all nodes, where cross-node containers can call each other as if they were on one switch. Remember the glimpse of the overlay driver in episode 9 — now it becomes real.
docker network create -d overlay my-net
docker service create \
--name api \
--network my-net \
--replicas 2 \
my-api:1.0Two important facts about overlays: (1) they work only within Swarm scope — non-swarm services can't use them; (2) DNS runs across nodes, so api can be resolved from any task on any node. Inter-node traffic is encapsulated with VXLAN over port 4789/udp (which we opened in episode 16). The combination of overlay + embedded DNS is why multi-host microservice architectures feel "as easy as localhost".
Writing services one by one with docker service create feels verbose for an application with web, api, and a database. The solution: a stack — deploy a Compose file to Swarm, and Swarm translates each service into a Swarm service. Compose files for Swarm use the deploy key, which doesn't apply in regular Compose:
version: "3.9"
services:
web:
image: ghcr.io/arman/my-web:1.0.0
ports:
- "80:3000"
networks:
- app-net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 20s
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
failure_action: rollback
rollback_config:
parallelism: 1
delay: 5s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
api:
image: ghcr.io/arman/my-api:1.0.0
networks:
- app-net
deploy:
replicas: 2
resources:
limits:
cpus: "0.50"
memory: 256M
reservations:
cpus: "0.25"
memory: 128M
placement:
constraints:
- node.role == worker
restart_policy:
condition: on-failure
db:
image: postgres:16-alpine
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-net
environment:
POSTGRES_DB: appdb
POSTGRES_USER: app
secrets:
- db_password
deploy:
placement:
constraints:
- node.labels.db == true
restart_policy:
condition: on-failure
networks:
app-net:
driver: overlay
volumes:
db-data:
secrets:
db_password:
external: trueDissecting the deploy key — this is what distinguishes a stack from regular compose:
replicas — the desired number of tasks.update_config — how the service updates: parallelism: 1 (one task at a time), delay: 10s (pause between batches), order: start-first (new tasks become healthy first, then old ones are stopped — the key to zero-downtime), failure_action: rollback (automatically return to the old version if the update fails).rollback_config — how rollback runs (similar to update, but backwards).restart_policy — when and how many times a task is restarted after failure (on-failure, max_attempts).resources — CPU/memory limits and reservations per task (equivalent to --memory/--cpus from episode 25).placement — constraints on which node a task may be scheduled: node.role == worker or custom labels (e.g. node.labels.db == true, set with docker node update --label-add db=true node-3).Deploy and manage the stack:
docker stack deploy -c docker-stack.yml my-stack
docker stack ls
docker stack ps my-stack
docker stack services my-stack
docker stack rm my-stackThe stack name becomes a service prefix: my-stack_web, my-stack_api, my-stack_db. To update a service in the stack, just update the file and run docker stack deploy again — Swarm detects the difference and only changes what changed.
Storing passwords in the environment of a compose file is a leak waiting to happen. Swarm offers secrets — secret data stored encrypted in the state store, sent only to nodes that receive the task, and mounted as files at /run/secrets/<name> inside the container. Two creation approaches:
printf "P@ssw0rd-super-rahasia" | docker secret create db_password -
docker secret create tls_cert tls_cert.pemdocker service create \
--name api \
--secret db_password \
--secret source=tls_cert,target=cert.pem,mode=0400 \
my-api:1.0Inside the container, the application reads the password from the file /run/secrets/db_password — not from an environment variable. Secret files are mounted in RAM, not container disk, and are never visible in docker inspect. Note the target option to rename the file and mode to control its permissions.
Configs are secrets' counterpart for non-secret data — for example nginx configuration or service discovery lists:
docker config create nginx_conf nginx.conf
docker service create --name web --config nginx_conf nginx:1.27-alpineOnce again, the file is mounted at /run/secrets/nginx_conf inside the container. The difference: configs aren't encrypted in the state store, because their content isn't secret. Rule of thumb: secret → secrets, not secret → configs.
This is the climax. You have my-stack_web running 3 replicas at version 1.0.0, and you want to go up to 2.0.0 without users noticing. With docker service update, Swarm replaces tasks one by one according to update_config — new tasks wait for a green healthcheck before old tasks are stopped (order: start-first):
docker service update \
--image ghcr.io/arman/my-web:2.0.0 \
--update-parallelism 1 \
--update-delay 10s \
my-stack_webReading it: change the image to 2.0.0, one task per batch, with a 10-second pause between batches. The Swarm load balancer automatically only routes traffic to healthy tasks — during the rotation, there's always an old replica serving until a new one is ready. If the new task's healthcheck fails repeatedly (remember episode 15: HEALTHCHECK determines health), failure_action: rollback automatically returns the service to the previous image:
docker service rollback my-stack_webUse docker service ps my-stack_web to monitor each rotation: every failed or rolled-back task leaves a trace in the ERROR column — a goldmine of information when an update goes wrong.
Tip
A habit that saves many teams: make a habit of deploying with a strategy already proven in the stack file (order: start-first, failure_action: rollback, parallelism: 1) rather than typing manual docker service update flags in production. Manual commands are prone to typos and hard to reproduce; a stack file is both documentation and a consistent executor.
Using depends_on in a Swarm stack. depends_on (episode 10) isn't properly supported in Swarm mode — Swarm handles ordering via healthchecks and restart policies, not depends_on. Remove depends_on from your stack files.
docker run inside the cluster. docker run containers aren't scheduled by Swarm and can't use overlay networks — they only live on one node. All workloads that "must spread" have to go through a service.
Updating straight to a nonexistent image. Swarm doesn't pull the image before draining old tasks. The new task fails, the healthcheck fails, and failure_action: rollback saves the day — but you won't see this warning if update_config isn't set. Always define update config in the stack.
Secrets read via env. Secrets mounted at /run/secrets don't automatically become environment variables. The application must read the file, not read process.env. Make sure the application code is adjusted when migrating from env to secrets.
Forgetting docker stack rm when the demo is done. A stack that isn't removed leaves services, networks, and secrets running. Clean up with docker stack rm <name> so the cluster doesn't fill up with resources.
In this episode 17 we filled the cluster with something valuable: understanding the Service → Task → Container hierarchy and the shift from a docker run mentality to declaring desired state, deploying with docker service create/ls/ps/logs/scale/rm, connecting cross-node services via overlay networks with cross-node DNS, deploying a whole application via docker stack deploy -c docker-stack.yml with the deploy key (replicas, resources, placement, update_config, restart_policy), storing secrets with Swarm secrets and configuration with configs (mounted at /run/secrets), and releasing new versions without downtime via rolling updates (--update-parallelism, --update-delay, failure_action) and automatic or manual rollback.
Core takeaways:
deploy: in a stack file is the production key: update order, failure action, resources, placement.start-first + healthcheck = zero-downtime; failure_action: rollback = an automatic safety net.The cluster is now alive, populated with services, and updatable without downtime. But there's one arm we haven't deployed yet: image building itself still uses the old, slow, and limited builder. In the next episode, episode 18, we'll replace the machine: Modern Build Engine: Docker Buildx & BuildKit — parallel builds, advanced caching, build secrets with no trace in the image, and multi-platform images in a single command. See you in episode 18!