1. What is Docker Swarm & When to Use It
- Built-in orchestration in Docker Engine — no extra install
- Best for: small-medium deployments, already using Docker Compose, want simplicity over features
- Key concepts: Manager nodes, Worker nodes, Services, Tasks, Stacks, Overlay networks
Swarm vs Kubernetes
| Aspect | Docker Swarm | Kubernetes |
|---|---|---|
| Complexity | Simple, native Docker CLI | Complex, steep learning curve |
| Setup time | Minutes | Hours/days |
| Scaling | Good for small/medium | Built for massive scale |
| Learning curve | Low (if you know Docker) | High |
| Load balancing | Built-in (routing mesh) | Ingress controller needed |
| Service discovery | Built-in DNS | CoreDNS + Services |
| Secrets management | Built-in | Built-in |
| Best for | Small teams, < 100 nodes | Large orgs, cloud-native |
| Community | Smaller | Massive ecosystem |
Architecture
Manager Node 1 (Leader) ←── Raft Consensus ──→ Manager Node 2
↕ ↕
Worker Node 1 Worker Node 2 Worker Node 3
↕ ↕ ↕
[containers] [containers] [containers]
Routing Mesh: Any node can receive traffic for any service2. Cluster Setup & Node Management
# Initialize swarm (on first manager)
docker swarm init --advertise-addr 10.0.0.1
# Get join tokens
docker swarm join-token worker
docker swarm join-token manager
# Join as worker
docker swarm join --token SWMTKN-1-xxx 10.0.0.1:2377
# Join as manager
docker swarm join --token SWMTKN-1-yyy 10.0.0.1:2377
# List nodes
docker node ls
# Promote worker to manager
docker node promote node-2
# Demote manager to worker
docker node demote node-2
# Remove node (on the node itself)
docker swarm leave
docker swarm leave --force # for managers
# Remove node (from manager)
docker node rm node-3
# Update node availability
docker node update --availability drain node-3 # maintenance mode
docker node update --availability active node-3 # back online
docker node update --availability pause node-3 # no new tasks
# Add labels to nodes
docker node update --label-add env=production node-1
docker node update --label-add disk=ssd node-2
docker node update --label-add region=eu-west node-3
# Inspect node
docker node inspect --pretty node-13. Service Management
# Create service
docker service create \
--name web \
--replicas 3 \
--publish 80:80 \
--network my-overlay \
--env NODE_ENV=production \
--mount type=bind,source=/data,target=/app/data \
--constraint 'node.labels.env==production' \
--limit-cpu 0.5 \
--limit-memory 512M \
--reserve-cpu 0.25 \
--reserve-memory 256M \
nginx:latest
# List services
docker service ls
# Inspect service
docker service inspect --pretty web
# Service logs
docker service logs web
docker service logs -f --tail 100 web
docker service logs --since 1h web
# Scale service
docker service scale web=5
docker service scale web=5 api=3 worker=2 # multiple at once
# Update service image (rolling update)
docker service update --image nginx:1.25 web
# Update with rollback config
docker service update \
--image nginx:1.25 \
--update-parallelism 1 \
--update-delay 30s \
--update-failure-action rollback \
--update-max-failure-ratio 0.25 \
--rollback-parallelism 1 \
--rollback-delay 10s \
web
# Rollback
docker service rollback web
# Remove service
docker service rm web
# List tasks (containers) of a service
docker service ps web
docker service ps web --no-trunc # full error messages
# Force rebalance (redeploy all tasks)
docker service update --force web4. Docker Stack (Compose for Swarm)
# docker-stack.yml
version: "3.8"
services:
web:
image: myregistry.com/myapp:latest
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
order: start-first # start new before stopping old
rollback_config:
parallelism: 1
delay: 10s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
placement:
constraints:
- node.role == worker
- node.labels.env == production
preferences:
- spread: node.labels.zone # spread across zones
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
labels:
- "traefik.enable=true"
- "traefik.http.routers.web.rule=Host(`app.example.com`)"
ports:
- "80:80"
- "443:443"
networks:
- frontend
- backend
secrets:
- db_password
- ssl_cert
configs:
- source: nginx_conf
target: /etc/nginx/nginx.conf
volumes:
- app_data:/app/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
db:
image: postgres:16
deploy:
replicas: 1
placement:
constraints:
- node.labels.disk == ssd
volumes:
- db_data:/var/lib/postgresql/data
secrets:
- db_password
networks:
- backend
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
redis:
image: redis:7-alpine
deploy:
replicas: 1
networks:
- backend
networks:
frontend:
driver: overlay
attachable: true
backend:
driver: overlay
internal: true # no external access
volumes:
app_data:
driver: local
db_data:
driver: local
secrets:
db_password:
external: true
ssl_cert:
file: ./certs/ssl.pem
configs:
nginx_conf:
file: ./nginx.confStack Commands
# Deploy stack
docker stack deploy -c docker-stack.yml myapp
# List stacks
docker stack ls
# List services in stack
docker stack services myapp
# List tasks in stack
docker stack ps myapp
# Remove stack
docker stack rm myapp
# Update stack (redeploy with changes)
docker stack deploy -c docker-stack.yml myapp # same command, idempotent5. Networking
# Create overlay network
docker network create --driver overlay --attachable my-network
# Create encrypted overlay
docker network create --driver overlay --opt encrypted my-secure-net
# Create with subnet
docker network create --driver overlay --subnet 10.10.0.0/24 my-network
# List networks
docker network ls
# Inspect network
docker network inspect my-network
# Service discovery: containers resolve service names via DNS
# Inside container: curl http://web:80 → resolves to service VIP
# Routing mesh: published ports available on ALL nodes
# External LB → any node:80 → routes to correct container
# Bypass routing mesh (host mode):
docker service create --publish mode=host,target=80,published=80 webNetwork Architecture
External Traffic
↓
[Load Balancer] → Any Swarm Node:published_port
↓
[Routing Mesh / Ingress Network]
↓
[Overlay Network]
↓
[Service VIP] → Task 1, Task 2, Task 3 (round-robin)6. Secrets & Configs
# Create secret from file
echo "mypassword" | docker secret create db_password -
docker secret create ssl_cert ./cert.pem
# List secrets
docker secret ls
# Inspect secret (metadata only, never shows value)
docker secret inspect db_password
# Remove secret
docker secret rm db_password
# Use secret in service
docker service create --secret db_password --name web myapp
# Secret available at /run/secrets/db_password inside container
# Rotate secret (create new, update service, remove old)
echo "newpassword" | docker secret create db_password_v2 -
docker service update \
--secret-rm db_password \
--secret-add db_password_v2 \
web
# Configs (non-sensitive configuration)
docker config create nginx_conf ./nginx.conf
docker config ls
docker service create --config nginx_conf --name web nginx
# Config available at /nginx_conf inside container (or custom target)7. Registry & Image Distribution
# Private registry in swarm
docker service create \
--name registry \
--publish 5000:5000 \
--mount type=bind,source=/registry-data,target=/var/lib/registry \
registry:2
# Tag and push
docker tag myapp:latest 10.0.0.1:5000/myapp:latest
docker push 10.0.0.1:5000/myapp:latest
# Deploy from private registry
docker service create --name web 10.0.0.1:5000/myapp:latest
# With registry auth
docker login myregistry.com
# Docker stores auth in ~/.docker/config.json
# For swarm: use --with-registry-auth
docker service create --with-registry-auth --name web myregistry.com/myapp:latest
docker stack deploy --with-registry-auth -c stack.yml myapp
# GitLab Container Registry example
docker login registry.gitlab.com
docker tag myapp registry.gitlab.com/mygroup/myproject:latest
docker push registry.gitlab.com/mygroup/myproject:latest8. Health Checks & High Availability
# Health check in Dockerfile
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=60s \
CMD curl -f http://localhost/health || exit 1
# Health check in service create
docker service create \
--health-cmd "curl -f http://localhost/health || exit 1" \
--health-interval 30s \
--health-timeout 10s \
--health-retries 3 \
--health-start-period 60s \
--name web myapp
# Manager HA: Use 3 or 5 managers (odd number for Raft quorum)
# 3 managers → tolerates 1 failure
# 5 managers → tolerates 2 failures
# 7 managers → tolerates 3 failures (max recommended)
# Check manager status
docker node ls --filter role=manager
# Drain a manager for maintenance (stays manager but no tasks)
docker node update --availability drain manager-2Quorum Table
| Managers | Quorum | Tolerated Failures |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 0 (avoid!) |
| 3 | 2 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
9. Monitoring & Logging
# Built-in monitoring
docker node ls # Node status
docker service ls # Service status
docker service ps <service> # Task status per service
docker stats # Container resource usage (local)
docker system df # Disk usage
# Prometheus + Grafana stack for Swarm
# Enable Docker metrics endpoint in /etc/docker/daemon.json:
{
"metrics-addr": "0.0.0.0:9323",
"experimental": true
}
# Key metrics to monitor:
# - Node count and status
# - Service replica count (actual vs desired)
# - Container CPU/memory usage
# - Network I/O per service
# - Disk usage per node
# - Task failure rateCentralized Logging
# Global logspout (one per node)
docker service create \
--name logspout \
--mode global \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
gliderlabs/logspout \
syslog+tcp://logstash:5000
# Or use Docker logging drivers
docker service create \
--log-driver gelf \
--log-opt gelf-address=udp://logstash:12201 \
--name web myapp10. Troubleshooting & Best Practices
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Service stuck at 0/N replicas | Image pull failure, constraints | docker service ps --no-trunc <svc> |
| Tasks in "Rejected" state | Resource constraints too tight | Increase limits or add nodes |
| Network connectivity issues | Overlay network misconfigured | Check firewall ports 2377, 7946, 4789 |
| Manager quorum lost | Too many managers down | docker swarm init --force-new-cluster |
| Slow deployments | Large images, slow registry | Use smaller images, local registry |
| Service not reachable | Port not published | Check docker service inspect for ports |
| Secrets not available | Service not updated | Redeploy service with new secret |
Required Firewall Ports
| Port | Protocol | Purpose |
|---|---|---|
| 2377 | TCP | Cluster management |
| 7946 | TCP/UDP | Node communication |
| 4789 | UDP | Overlay network (VXLAN) |
Best Practices
- ▸Use 3 or 5 managers (odd number, never just 1 in production)
- ▸Keep managers as managers only (drain them from workloads)
- ▸Always use --with-registry-auth for private registries
- ▸Use placement constraints + labels for workload distribution
- ▸Set resource limits on all services
- ▸Use health checks on every service
- ▸Use update_config.order: start-first for zero-downtime deploys
- ▸Pin image versions (never use :latest in production stacks)
- ▸Back up Swarm state: /var/lib/docker/swarm/
- ▸Use overlay networks with internal: true for backend services
- ▸Rotate join tokens periodically: docker swarm join-token --rotate worker