Docker Compose au-delà du développement
Docker Compose n'est pas uniquement réservé au développement. Avec les bonnes pratiques, il devient un outil puissant pour orchestrer des applications en production.
Pour une application hébergée sur un seul serveur (un VPS ou une machine dédiée), Compose offre un excellent compromis : toute l'infrastructure est décrite dans quelques fichiers versionnés, le déploiement tient en deux commandes, et il n'y a ni plan de contrôle à maintenir ni cluster à superviser. Kubernetes ou Docker Swarm deviennent pertinents lorsqu'il faut répartir la charge sur plusieurs machines ou garantir une haute disponibilité au niveau de l'hôte, ce qui n'est pas le besoin de la majorité des applications métier.
Cet article détaille une organisation de fichiers, une configuration de production commentée, la gestion des logs, des secrets et des données, puis une procédure de déploiement et les pièges les plus fréquents.
Structure de fichiers recommandée
├── docker-compose.yml # Configuration de base
├── docker-compose.prod.yml # Overrides production
├── docker-compose.dev.yml # Overrides développement
├── .env.production # Variables d'environnement
└── nginx/
└── default.conf # Configuration Nginx
Le principe est celui de la surcharge : le fichier de base décrit ce qui est commun à tous les environnements (les services, leurs réseaux, leurs volumes), et chaque fichier d'environnement n'ajoute que ses différences. En développement, on monte le code source en volume et on active Xdebug ; en production, on utilise l'image construite, on limite les ressources et on active les redémarrages automatiques.
Compose fusionne les fichiers dans l'ordre où ils sont passés avec l'option -f : les valeurs scalaires du dernier fichier l'emportent, tandis que les listes comme ports ou volumes sont combinées. Pour éviter de répéter les options à chaque commande, la variable COMPOSE_FILE peut être définie sur le serveur :
# Fusion explicite des fichiers
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Ou une fois pour toutes dans l'environnement du serveur
export COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml
docker compose up -d
# Vérifier la configuration finale après fusion
docker compose config
La commande docker compose config affiche la configuration réellement appliquée, variables interpolées comprises. C'est le premier réflexe à avoir lorsqu'un service ne se comporte pas comme prévu.
Configuration de production
services:
app:
build:
context: .
target: production
restart: always
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
healthcheck:
test: ["CMD", "php-fpm-healthcheck"]
interval: 30s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
app:
condition: service_healthy
redis:
image: redis:7-alpine
restart: always
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
Chaque bloc de cette configuration a un rôle précis :
build.target: production: construit uniquement l'étape de production d'un Dockerfile multi-stage, sans les outils de développement. Voir l'article Builds multi-stage Docker.restart: always: le conteneur redémarre après un crash et au redémarrage du démon Docker. La varianteunless-stoppedse comporte de la même façon, sauf qu'un conteneur arrêté manuellement reste arrêté après un redémarrage du serveur.deploy.resources.limits: avec Docker Compose v2, ces limites sont appliquées même hors Swarm. Un conteneur qui dépasse sa limite mémoire est tué par le noyau (OOM) au lieu de faire tomber tout le serveur.healthcheck:php-fpm-healthcheckest un petit script open source à installer dans l'image ; il interroge la page de statut de PHP-FPM, qui doit donc être activée avecpm.status_pathdans la configuration du pool.depends_onaveccondition: service_healthy: Nginx ne démarre qu'une fois PHP-FPM déclaré sain, ce qui évite les erreurs 502 au démarrage.- Redis est borné à 256 Mo avec la politique
allkeys-lru: une fois la limite atteinte, les clés les moins récemment utilisées sont évincées. C'est adapté à un cache, pas à un stockage de sessions qui ne doivent pas disparaître.
Variables d'environnement et secrets
Le fichier .env.production ne doit jamais être commité : il vit uniquement sur le serveur, avec des droits restreints (chmod 600). Compose permet aussi de rendre une variable obligatoire, afin qu'un oubli fasse échouer le déploiement au lieu de démarrer une application mal configurée :
services:
app:
env_file: .env.production
environment:
APP_ENV: prod
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL doit être définie}
Pour les données les plus sensibles (mots de passe de base de données, clés d'API), préférez les secrets Compose, montés sous forme de fichiers dans /run/secrets/ plutôt qu'exposés dans l'environnement du processus. Le sujet est détaillé dans l'article Sécuriser vos conteneurs Docker.
Gestion des logs
Configurez un driver de logging centralisé pour faciliter le monitoring :
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Sans ces options, le driver json-file conserve les logs indéfiniment et un conteneur bavard finit par remplir le disque. Ici, chaque conteneur garde au maximum trois fichiers de 10 Mo. Plutôt que de répéter ce bloc dans chaque service, utilisez une extension YAML et une ancre :
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
app:
logging: *default-logging
nginx:
logging: *default-logging
Vous pouvez aussi définir ces valeurs par défaut pour tout l'hôte dans /etc/docker/daemon.json, puis redémarrer Docker. Elles ne s'appliquent qu'aux conteneurs créés après le changement :
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Pour l'agrégation (Loki, Elasticsearch, un service SaaS), faites écrire l'application sur la sortie standard et laissez un agent collecter les logs des conteneurs : c'est plus robuste que d'écrire dans des fichiers à l'intérieur du conteneur.
Volumes et données persistantes
Un conteneur est jetable, ses données ne le sont pas. Tout ce qui doit survivre à un docker compose down (base de données, fichiers envoyés par les utilisateurs) doit vivre dans un volume nommé ou un répertoire de l'hôte clairement identifié. Attention : docker compose down -v supprime les volumes nommés du projet, et donc les données.
Un volume n'est pas une sauvegarde. Pour une base de données, effectuez un export logique régulier plutôt que de copier les fichiers bruts d'un moteur en cours d'exécution :
# Export MySQL depuis le conteneur, compressé sur l'hôte
docker compose exec -T db sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction app' \
| gzip > backup-$(date +%F).sql.gz
Pensez ensuite à copier ces fichiers hors du serveur et à tester régulièrement leur restauration.
Déployer une nouvelle version
Avec des images construites en CI et publiées dans un registre, une mise à jour se résume à récupérer les nouvelles images et à recréer les conteneurs qui ont changé :
docker compose pull
docker compose up -d --remove-orphans --wait
docker image prune -f
L'option --wait attend que les services soient en cours d'exécution et sains avant de rendre la main, ce qui permet à un script de déploiement d'échouer proprement si un healthcheck ne passe pas. --remove-orphans supprime les conteneurs de services retirés du fichier. Gardez en tête que Compose recrée un conteneur en arrêtant l'ancien avant de démarrer le nouveau : il y a donc une courte interruption. Si elle n'est pas acceptable, il faut un reverse proxy capable de basculer entre deux instances, ou un orchestrateur.
Pièges courants
- Tag
latesten production : impossible de savoir quelle version tourne ni de revenir en arrière. Utilisez un tag par version ou par commit. - Ports publiés sur toutes les interfaces :
"3306:3306"expose la base sur Internet, et les règles de Docker contournent souvent le pare-feu UFW. Ne publiez rien pour les services internes, ou liez-les à127.0.0.1. - Healthcheck absent ou trop laxiste :
condition: service_healthyne sert à rien si le test vérifie seulement que le processus existe. - Construire les images sur le serveur de production : cela consomme ses ressources et rend les déploiements non reproductibles. Construisez en CI, déployez des images.
Points essentiels
- Toujours définir
restart: alwayspour la résilience - Limiter les ressources avec
deploy.resources - Utiliser des healthchecks pour la haute disponibilité
- Séparer les volumes de données persistantes
- Ne jamais exposer les ports internes inutilement
- Faire tourner les logs et sauvegarder les données hors du serveur
Quand Compose ne suffit plus
Compose reste limité à un seul hôte : pas de répartition automatique entre plusieurs machines, pas de mise à jour progressive sans interruption, pas de reprise si le serveur lui-même tombe. Si votre application a besoin de ces garanties, c'est le signal pour passer à un orchestrateur. D'ici là, un serveur bien dimensionné piloté par Compose reste une solution simple, lisible et fiable.