Skip to main content

Stacks and deployment


Chapter objectives

  • Understand Docker stacks
  • Write stack files (docker-compose)
  • Deploy and manage stacks
  • Use deployment options

1 - What is a Stack?

Definition

A stack is a group of related services, defined in a YAML file and deployed together.

┌─────────────────────────────────────────────────────────────┐
│ Stack "myapp" │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service: web (3 replicas) │ │
│ │ Service: api (2 replicas) │ │
│ │ Service: db (1 replica) │ │
│ │ Service: redis (1 replica) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Network: frontend │ │
│ │ Network: backend │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Volume: db-data │ │
│ │ Config: nginx-config │ │
│ │ Secret: db-password │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Stack vs Docker Compose

AspectDocker ComposeDocker Stack
ModeStandaloneSwarm
Commanddocker compose updocker stack deploy
BuildYesNo (pre-built images)
depends_onYesNo
deployIgnoredUsed

2 - Stack file

Basic structure

version: "3.9"

services:
web:
image: nginx:alpine
ports:
- "80:80"
deploy:
replicas: 3

api:
image: myapi:latest
deploy:
replicas: 2

networks:
default:
driver: overlay

volumes:
data:

The deploy section

services:
web:
image: nginx
deploy:
# Nombre de réplicas
replicas: 3

# Mode de déploiement
mode: replicated # ou global

# Placement
placement:
constraints:
- node.role == worker
- node.labels.zone == eu-west
preferences:
- spread: node.labels.zone

# Ressources
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M

# Stratégie de mise à jour
update_config:
parallelism: 2
delay: 10s
failure_action: rollback
order: start-first

# Stratégie de rollback
rollback_config:
parallelism: 1
delay: 5s

# Politique de redémarrage
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s

# Labels
labels:
com.example.description: "Web frontend"

3 - Deploy a Stack

Basic command

# Déployer une stack
docker stack deploy -c docker-compose.yml myapp

# Avec plusieurs fichiers
docker stack deploy \
-c docker-compose.yml \
-c docker-compose.prod.yml \
myapp

Check the deployment

# Lister les stacks
docker stack ls

# Services d'une stack
docker stack services myapp

# Tâches d'une stack
docker stack ps myapp

# Tâches avec filtres
docker stack ps myapp --filter "desired-state=running"

Update a stack

# Re-déployer avec les changements
docker stack deploy -c docker-compose.yml myapp

# Swarm compare l'état désiré et applique les différences

Remove a stack

# Supprimer
docker stack rm myapp

# Attention : supprime tous les services, réseaux (pas les volumes)

4 - Complete example: Web application

docker-compose.yml

version: "3.9"

services:
# Reverse proxy / Load balancer
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
configs:
- source: nginx_config
target: /etc/nginx/conf.d/default.conf
networks:
- frontend
deploy:
replicas: 2
placement:
constraints:
- node.role == manager
update_config:
parallelism: 1
delay: 10s

# Application API
api:
image: myregistry/api:${VERSION:-latest}
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db:5432/app
- REDIS_URL=redis://redis:6379
networks:
- frontend
- backend
secrets:
- db_password
- jwt_secret
deploy:
replicas: 3
resources:
limits:
cpus: '1'
memory: 512M
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3

# Base de données
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=app
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
secrets:
- db_password
deploy:
replicas: 1
placement:
constraints:
- node.labels.db == true
resources:
limits:
cpus: '2'
memory: 2G

# Cache Redis
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
networks:
- backend
deploy:
replicas: 1

# Worker pour les jobs asynchrones
worker:
image: myregistry/worker:${VERSION:-latest}
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db:5432/app
- REDIS_URL=redis://redis:6379
networks:
- backend
secrets:
- db_password
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 256M

networks:
frontend:
driver: overlay
backend:
driver: overlay
internal: true

volumes:
db-data:
redis-data:

configs:
nginx_config:
file: ./nginx-default.conf

secrets:
db_password:
external: true
jwt_secret:
external: true

Deployment

# Créer les secrets d'abord
echo "supersecretpassword" | docker secret create db_password -
echo "jwtsecretkey" | docker secret create jwt_secret -

# Déployer
VERSION=1.0.0 docker stack deploy -c docker-compose.yml myapp

# Vérifier
docker stack services myapp

5 - Variables and environment files

Use variables

services:
api:
image: myapi:${VERSION:-latest}
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
# Avec variables d'environnement
VERSION=2.0.0 LOG_LEVEL=debug docker stack deploy -c stack.yml myapp

# Avec fichier .env
export $(cat .env | xargs) && docker stack deploy -c stack.yml myapp

The .env file

# .env
VERSION=1.2.3
LOG_LEVEL=info
DB_PASSWORD=secret
DOMAIN=example.com

6 - Multiple files

Base file + overrides

# docker-compose.yml (base)
version: "3.9"
services:
api:
image: myapi:latest
networks:
- backend

# docker-compose.prod.yml (override production)
version: "3.9"
services:
api:
deploy:
replicas: 5
resources:
limits:
memory: 1G
# Combiner les fichiers
docker stack deploy \
-c docker-compose.yml \
-c docker-compose.prod.yml \
myapp
project/
├── docker-compose.yml # Base
├── docker-compose.prod.yml # Production overrides
├── docker-compose.dev.yml # Development overrides
├── configs/
│ └── nginx.conf
├── secrets/
│ └── .gitkeep
└── scripts/
├── deploy-prod.sh
└── deploy-staging.sh

7 - Healthchecks and dependencies

Healthcheck in the stack

services:
api:
image: myapi
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
replicas: 3

Manage dependencies (without depends_on)

services:
api:
image: myapi
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 10
# L'API redémarre jusqu'à ce que db soit prête

db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5

8 - Best practices

Images

services:
api:
# ✅ Toujours utiliser des tags versionnés
image: myregistry/api:1.2.3

# ❌ Éviter latest en production
# image: myregistry/api:latest

Resources

services:
api:
deploy:
resources:
# ✅ Toujours définir des limites
limits:
cpus: '1'
memory: 512M
# ✅ Définir des réservations pour le scheduling
reservations:
cpus: '0.25'
memory: 256M

Networks

networks:
frontend:
driver: overlay

backend:
driver: overlay
internal: true # ✅ Isoler le backend

# ❌ Éviter un seul réseau pour tout

Updates

services:
api:
deploy:
update_config:
parallelism: 1 # Un à la fois
delay: 30s # Attendre entre chaque
failure_action: rollback # Rollback si échec
order: start-first # Nouveau avant de tuer l'ancien

Command summary

CommandDescription
docker stack deploy -c file.yml nameDeploy a stack
docker stack lsList stacks
docker stack services nameServices of a stack
docker stack ps nameTasks of a stack
docker stack rm nameRemove a stack
Key points
  • Stacks use the deploy section of the docker-compose
  • depends_on is not supported, use healthchecks + restart
  • Use versioned tags, not latest
  • Always define resource limits

Practical exercises

  1. Create a stack with nginx + api + redis
  2. Configure the rolling update for zero downtime
  3. Add healthchecks to all services
  4. Test updating a service in the stack

← Swarm networks | Secrets and Configs →