Déployer des applications avec Docker Compose : guide avancé
Ce tutoriel propose une approche avancée et pragmatique de Docker Compose pour déployer des applications multi-conteneurs en environnement de développement, de préproduction et de production. Il met l’accent sur les bonnes pratiques, la reproductibilité, l’observabilité, la sécurité, la gestion des secrets, les stratégies de mise à jour et les pièges courants.
1) Prérequis et objectifs
Prérequis techniques
- Un système Linux, macOS ou Windows (avec WSL2 recommandé).
- Docker Engine et Docker Compose v2 (plugin
docker compose). - Connaissances de base : images, conteneurs, volumes, réseaux.
Vérification :
docker version
docker compose version
Objectifs
À la fin, vous saurez :
- Structurer un projet Compose propre et maintenable.
- Gérer environnements et surcharges (dev/staging/prod).
- Construire des images avec cache, multi-étapes et paramètres.
- Concevoir des réseaux, volumes, politiques de redémarrage.
- Mettre en place des healthchecks et des dépendances robustes.
- Utiliser secrets et variables d’environnement correctement.
- Appliquer des stratégies de déploiement et de mise à jour.
- Diagnostiquer les problèmes (logs, événements, inspection, ressources).
2) Structure recommandée d’un projet Compose
Une structure réaliste et évolutive :
mon-projet/
├─ compose.yaml
├─ compose.override.yaml
├─ compose.prod.yaml
├─ .env
├─ .env.prod
├─ docker/
│ ├─ nginx/
│ │ ├─ Dockerfile
│ │ └─ default.conf
│ └─ app/
│ ├─ Dockerfile
│ └─ entrypoint.sh
├─ app/
│ ├─ src/...
│ └─ package.json
└─ scripts/
├─ up.sh
├─ down.sh
└─ backup-db.sh
Principes :
compose.yaml: base commune (services, réseaux, volumes).compose.override.yaml: ergonomie développement (montages de code, ports, outils).compose.prod.yaml: durcissement production (ressources, logs, secrets, read-only…)..env: variables par défaut (non sensibles)..env.prod: variables de production (idéalement gérées via un gestionnaire de secrets ; sinon fichier protégé et hors dépôt).
3) Comprendre la résolution des variables et les fichiers .env
Docker Compose lit automatiquement un fichier .env situé dans le répertoire courant (celui où vous exécutez docker compose). Ce fichier sert à substituer des variables dans compose.yaml.
Exemple .env :
COMPOSE_PROJECT_NAME=monprojet
APP_PORT=8080
POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=devpassword
Points importants :
- La substitution se fait au moment où Compose interprète le fichier.
- Les variables d’environnement du shell peuvent surcharger
.env. - Les variables sensibles (mots de passe, clés) ne devraient pas être commitées.
Pour visualiser la configuration finale après substitution et fusion des fichiers :
docker compose config
Pour utiliser un autre fichier .env :
docker compose --env-file .env.prod config
4) Exemple complet : application web + base de données + proxy
Nous allons déployer :
- Une application (exemple Node.js) construite via
Dockerfile. - Une base PostgreSQL avec volume persistant.
- Un proxy Nginx en frontal.
- Un réseau interne pour isoler la base.
- Des healthchecks et dépendances.
4.1) compose.yaml (base)
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 5s
timeout: 3s
retries: 20
restart: unless-stopped
app:
build:
context: ./docker/app
dockerfile: Dockerfile
args:
NODE_VERSION: "20-alpine"
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
PORT: "3000"
depends_on:
db:
condition: service_healthy
networks:
- backend
- frontend
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
interval: 10s
timeout: 3s
retries: 10
restart: unless-stopped
nginx:
build:
context: ./docker/nginx
dockerfile: Dockerfile
ports:
- "${APP_PORT:-8080}:80"
depends_on:
app:
condition: service_healthy
networks:
- frontend
restart: unless-stopped
networks:
backend:
internal: true
frontend:
volumes:
db_data:
Explications avancées :
networks.backend.internal: trueempêche l’accès direct depuis l’extérieur vers les services de ce réseau. La base n’est accessible que depuis les services attachés àbackend.depends_onaveccondition: service_healthyrend la dépendance plus robuste qu’un simple ordre de démarrage. Sans healthcheck, un service peut être “démarré” mais non prêt.- Les healthchecks utilisent
CMD-SHELLet des variables$$(double$) pour éviter que Compose n’interprète la variable au moment du rendu ; elle doit être interprétée dans le conteneur.
4.2) Dockerfile de l’application (multi-étapes)
docker/app/Dockerfile :
ARG NODE_VERSION=20-alpine
FROM node:${NODE_VERSION} AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:${NODE_VERSION} AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
COPY ../../app ./app
WORKDIR /app/app
EXPOSE 3000
ENTRYPOINT ["/entrypoint.sh"]
CMD ["node", "server.js"]
Pourquoi multi-étapes :
- Réduit la taille finale (pas d’outils de build inutiles).
- Améliore le cache :
npm cine se relance que sipackage*.jsonchange. - Favorise la reproductibilité.
docker/app/entrypoint.sh :
#!/bin/sh
set -eu
echo "Démarrage de l'application…"
exec "$@"
4.3) Nginx en frontal
docker/nginx/Dockerfile :
FROM nginx:1.27-alpine
COPY default.conf /etc/nginx/conf.d/default.conf
docker/nginx/default.conf :
server {
listen 80;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
proxy_pass http://app:3000/health;
}
}
5) Lancer, arrêter, reconstruire : commandes essentielles (et avancées)
Démarrage :
docker compose up -d
Voir l’état :
docker compose ps
docker compose top
Logs :
docker compose logs -f
docker compose logs -f app
Reconstruire une image (sans redémarrer tout) :
docker compose build app
docker compose up -d --no-deps app
Forcer la recréation :
docker compose up -d --force-recreate --no-deps app
Arrêt et suppression (en gardant les volumes) :
docker compose down
Suppression avec volumes (attention, destructif) :
docker compose down -v
Nettoyage global (prudence) :
docker system df
docker system prune
docker volume prune
6) Overrides et profils : séparer développement et production
6.1) compose.override.yaml (développement)
Objectif : itération rapide, montage du code, outils de debug.
services:
app:
environment:
NODE_ENV: development
volumes:
- ./app:/app/app
command: ["node", "server.js"]
ports:
- "3000:3000"
db:
ports:
- "5432:5432"
Ici :
- On expose PostgreSQL au poste local pour utiliser un client SQL.
- On monte le code pour éviter de reconstruire l’image à chaque modification.
6.2) compose.prod.yaml (production)
Objectif : durcissement, limitation des droits, logs, ressources.
services:
app:
read_only: true
tmpfs:
- /tmp
environment:
NODE_ENV: production
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
nginx:
read_only: true
tmpfs:
- /var/cache/nginx
- /var/run
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Remarques :
read_only: trueréduit l’impact d’une compromission, mais exige de prévoir des emplacements en écriture (tmpfs, volumes).- La section
deployest historiquement liée à Swarm, mais certaines valeurs restent utiles comme documentation et peuvent être prises en compte selon les environnements. Pour une limitation stricte hors Swarm, on privilégie souvent les options du moteur et la supervision, mais cela dépend des versions et du contexte.
6.3) Lancer avec plusieurs fichiers
En production :
docker compose --env-file .env.prod -f compose.yaml -f compose.prod.yaml up -d
En développement (par défaut, compose.override.yaml est automatiquement fusionné) :
docker compose up -d
7) Réseaux : isolation, DNS interne et exposition contrôlée
DNS interne
Compose crée un DNS interne : chaque service est joignable par son nom (db, app, nginx) sur le réseau partagé.
Test depuis app :
docker compose exec app sh -lc 'getent hosts db && nc -zv db 5432'
Exposition minimale
- Évitez d’exposer la base de données en production (
ports:). - Préférez
expose:pour documenter un port interne sans l’exposer à l’hôte.
Exemple :
services:
app:
expose:
- "3000"
Réseau interne
internal: true empêche l’accès depuis l’extérieur, mais n’empêche pas la communication entre services du réseau. C’est idéal pour db et services internes.
8) Volumes : persistance, sauvegarde, migrations
8.1) Volume nommé pour PostgreSQL
Le volume db_data persiste les données entre redémarrages.
Inspecter :
docker volume ls
docker volume inspect monprojet_db_data
8.2) Sauvegarde et restauration (commandes réelles)
Sauvegarde :
docker compose exec -T db sh -lc 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup.sql
Restauration (attention, écrase selon vos commandes SQL) :
cat backup.sql | docker compose exec -T db sh -lc 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"'
Pour une sauvegarde compressée :
docker compose exec -T db sh -lc 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip -c' > backup.sql.gz
8.3) Migrations applicatives
Évitez de lancer des migrations “au hasard” au démarrage si vous avez plusieurs réplicas. Une approche courante :
- Un service dédié
migrateexécuté ponctuellement. - Ou une commande CI/CD avant la mise à jour.
Exécution ponctuelle :
docker compose run --rm app sh -lc 'node migrate.js'
9) Secrets et configuration sensible : pratiques solides
9.1) Variables d’environnement : limites
Les variables d’environnement sont pratiques mais :
- Elles peuvent apparaître dans des dumps, logs, outils d’inspection.
- Elles sont visibles via
docker inspectpour qui a accès au démon Docker.
9.2) Secrets via fichiers (approche Compose)
Même sans orchestrateur avancé, vous pouvez monter des secrets sous forme de fichiers.
Exemple : créer un répertoire protégé :
mkdir -p secrets
chmod 700 secrets
printf '%s' 'motdepasse-super-secret' > secrets/db_password.txt
chmod 600 secrets/db_password.txt
Puis dans Compose (exemple conceptuel) :
services:
db:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Selon les images, la variable *_FILE est souvent supportée (PostgreSQL officiel supporte POSTGRES_PASSWORD_FILE). Pour l’application, vous pouvez lire le fichier au démarrage.
Test dans le conteneur :
docker compose exec db sh -lc 'ls -l /run/secrets && cat /run/secrets/db_password | wc -c'
9.3) Ne pas commiter les secrets
Ajoutez au .gitignore :
secrets/
.env.prod
10) Healthchecks : rendre le déploiement fiable
Un conteneur “up” n’est pas forcément “prêt”. Les healthchecks permettent :
- D’attendre la disponibilité réelle (DB prête, HTTP répond).
- D’améliorer la robustesse des dépendances.
- D’aider à l’auto-réparation (redémarrages si combinés à une supervision).
Voir l’état santé :
docker compose ps
docker inspect --format='{{json .State.Health}}' monprojet-app-1 | jq
Sans jq :
docker inspect monprojet-app-1 --format '{{.State.Health.Status}}'
11) Stratégies de build et optimisation du cache
11.1) BuildKit et cache
Activez BuildKit (souvent par défaut) :
docker buildx version
Construire avec sortie détaillée :
DOCKER_BUILDKIT=1 docker compose build --progress=plain
11.2) Éviter d’invalider le cache
- Copier d’abord
package*.json, installer, puis copier le code. - Éviter de copier des fichiers inutiles via
.dockerignore.
Exemple docker/app/.dockerignore (à placer dans le contexte de build, ici docker/app ne voit pas forcément app/ selon votre contexte ; adaptez) :
node_modules
npm-debug.log
.git
Dans notre cas, le contexte est ./docker/app et on copie ../../app. Il faut être conscient que le contexte de build limite ce qui est accessible. Une alternative plus classique consiste à mettre le Dockerfile à la racine et définir context: . pour maîtriser .dockerignore global.
11.3) Épingler les versions
Pour la reproductibilité :
- Épinglez les tags d’images (
postgres:16-alpineplutôt quelatest). - Épinglez les dépendances applicatives (
package-lock.json,npm ci).
12) Observabilité : logs, métriques, inspection et événements
12.1) Logs structurés et rotation
Nous avons configuré max-size et max-file. Vérifiez la taille :
docker compose logs --tail=200 nginx
docker inspect monprojet-nginx-1 --format '{{json .HostConfig.LogConfig}}'
12.2) Événements Docker
Très utile pour diagnostiquer des redémarrages :
docker events --filter 'label=com.docker.compose.project=monprojet'
12.3) Ressources (CPU/RAM)
docker stats
Pour un service :
docker stats $(docker compose ps -q app)
13) Sécurité : durcissement des conteneurs
Mesures concrètes :
- Exécuter en utilisateur non-root (si l’image le permet).
- Système de fichiers en lecture seule (
read_only: true). - Capacités Linux minimales (avancé, dépend des besoins).
- Pas de ports inutiles exposés.
- Réseau interne pour les services sensibles.
- Limiter les secrets (fichiers, gestionnaire dédié).
Exemple d’ajustement (conceptuel) :
services:
app:
user: "10001:10001"
security_opt:
- no-new-privileges:true
Attention : changer d’utilisateur peut nécessiter d’ajuster les permissions des répertoires et volumes.
14) Déploiement : mises à jour, continuité et rollback
Docker Compose n’est pas un orchestrateur complet, mais on peut mettre en place des pratiques fiables.
14.1) Mise à jour d’un service
- Récupérer les nouvelles images (si
image:) :
docker compose pull
- Reconstruire (si
build:) :
docker compose build
- Recréer les conteneurs :
docker compose up -d
Pour ne redéployer qu’un service :
docker compose up -d --no-deps app
14.2) Stratégie de rollback simple
- Garder des tags versionnés d’images (ex.
monapp:1.4.2). - En cas de problème, revenir au tag précédent dans
compose.prod.yaml, puis :
docker compose -f compose.yaml -f compose.prod.yaml up -d --no-deps app
14.3) Zéro interruption : limites et alternatives
Avec un seul conteneur app derrière un seul nginx, la mise à jour peut provoquer une brève interruption.
Approches possibles :
- Mettre en place une stratégie “bleu/vert” à la main (deux services
app_blueetapp_green+ bascule Nginx). - Utiliser un orchestrateur (Kubernetes, Swarm) ou un reverse-proxy dynamique (Traefik) avec plusieurs réplicas.
- Ajouter de la tolérance côté client et des timeouts corrects côté proxy.
15) Dépannage avancé : méthodes et commandes
15.1) Vérifier la configuration finale
docker compose config > rendu-final.yaml
15.2) Entrer dans un conteneur
docker compose exec app sh
docker compose exec db sh
15.3) Tester la connectivité réseau
Depuis app vers db :
docker compose exec app sh -lc 'nc -zv db 5432'
Tester HTTP interne :
docker compose exec nginx sh -lc 'wget -qO- http://app:3000/health'
15.4) Diagnostiquer un crash loop
- Logs du service :
docker compose logs --tail=200 app
- Code de sortie :
docker inspect monprojet-app-1 --format '{{.State.ExitCode}}'
- Dernier état :
docker inspect monprojet-app-1 --format '{{json .State}}'
15.5) Conflits de ports
Si APP_PORT est déjà utilisé :
ss -ltnp | grep ':8080'
Puis ajustez .env :
APP_PORT=8081
Relance :
docker compose up -d
16) Bonnes pratiques de conception Compose (récapitulatif)
- Un service = un rôle : base, application, proxy, worker, cron.
- Éviter
latest: versions épinglées. - Minimiser
ports:: exposer uniquement l’entrée (souvent le proxy). - Réseaux séparés :
frontendetbackend, avecinternalpour le backend. - Volumes nommés pour la persistance, pas des chemins “magiques”.
- Healthchecks partout où c’est pertinent.
- Secrets via fichiers et permissions strictes, pas dans Git.
- Overrides pour le confort dev, fichier prod pour le durcissement.
docker compose configcomme outil de vérité.- Scripts d’exploitation (backup, restore, migrations) versionnés.
17) Exemple de scripts utiles
scripts/up.sh :
#!/usr/bin/env bash
set -euo pipefail
docker compose up -d --build
docker compose ps
scripts/down.sh :
#!/usr/bin/env bash
set -euo pipefail
docker compose down
scripts/backup-db.sh :
#!/usr/bin/env bash
set -euo pipefail
ts="$(date +%Y%m%d-%H%M%S)"
mkdir -p backups
docker compose exec -T db sh -lc 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip -c' > "backups/db-${ts}.sql.gz"
echo "Sauvegarde créée : backups/db-${ts}.sql.gz"
Rendre exécutables :
chmod +x scripts/*.sh
18) Étapes finales : validation et checklist production
Validation fonctionnelle
- Démarrer :
docker compose up -d
- Vérifier la santé :
docker compose ps
- Tester l’accès :
curl -f "http://localhost:${APP_PORT:-8080}/health"
Checklist production
- Secrets hors dépôt, permissions strictes.
- Base non exposée (
ports:supprimé en prod). - Volumes sauvegardés et procédure de restauration testée.
- Logs avec rotation et collecte centralisée si nécessaire.
-
read_only,no-new-privileges, utilisateur non-root si possible. - Healthchecks et timeouts proxy configurés.
- Stratégie de rollback documentée.
- Supervision des ressources (
docker stats, alertes).
Conclusion
Docker Compose peut servir de socle solide pour déployer des applications multi-services, à condition de traiter sérieusement l’isolation réseau, la persistance, la configuration par environnement, les healthchecks, la sécurité et les procédures d’exploitation (sauvegarde, mise à jour, rollback). En combinant une structure de projet claire, des fichiers de surcharge (override/prod), des images optimisées et des commandes de diagnostic maîtrisées, vous obtenez un déploiement fiable, reproductible et maintenable.
Si vous souhaitez, je peux adapter cet exemple à votre pile exacte (Python/FastAPI, PHP/Laravel, Java/Spring, Rails), ajouter un service de cache (Redis), un worker, ou intégrer un proxy dynamique avec certificats TLS.