← Retour aux tutoriels

Déployer des applications avec Docker Compose : guide avancé

docker-composedockerdeploiementdevopscontainersmicroservicesorchestrationproductionci-cdobservabilite

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

Vérification :

docker version
docker compose version

Objectifs

À la fin, vous saurez :


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 :


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 :

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 :

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 :

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 :

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 :

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 :

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

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 :

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 :

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 :

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

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é :


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 :

  1. Exécuter en utilisateur non-root (si l’image le permet).
  2. Système de fichiers en lecture seule (read_only: true).
  3. Capacités Linux minimales (avancé, dépend des besoins).
  4. Pas de ports inutiles exposés.
  5. Réseau interne pour les services sensibles.
  6. 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

  1. Récupérer les nouvelles images (si image:) :
docker compose pull
  1. Reconstruire (si build:) :
docker compose build
  1. 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

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 :


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

docker compose logs --tail=200 app
docker inspect monprojet-app-1 --format '{{.State.ExitCode}}'
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)


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

docker compose up -d
docker compose ps
curl -f "http://localhost:${APP_PORT:-8080}/health"

Checklist production


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.