Docker Compose peut exploiter correctement une application sur un serveur unique. Le problème n'est pas le fichier YAML : c'est tout ce qui manque souvent autour de lui. Une image nommée latest, un mot de passe dans Git, une base exposée sur Internet et un docker compose up -d sans contrôle transforment un déploiement simple en procédure imprévisible.
Une mise en production sérieuse doit répondre à cinq questions : quelle version tourne, comment vérifier qu'elle fonctionne, comment protéger ses dépendances, comment préserver les données et comment revenir à la version précédente. Compose fournit les primitives ; l'équipe doit construire la discipline opérationnelle.
Quand Compose est le bon outil
Compose convient bien à un site, une API, un outil interne ou un service métier installé sur une seule machine, lorsque l'équipe accepte que ce serveur constitue un périmètre de panne. Docker documente explicitement l'utilisation de Compose sur un serveur unique.
Il atteint ses limites lorsque le service exige :
- une haute disponibilité entre plusieurs machines ;
- un placement automatique selon les ressources ;
- une montée en charge horizontale sur plusieurs nœuds ;
- des déploiements progressifs complexes sans interruption ;
- une politique réseau et d'identité distribuée à grande échelle.
Dans ces cas, un orchestrateur ou une plateforme managée peut devenir justifié. Ne migrez toutefois pas vers Kubernetes uniquement pour compenser un déploiement Compose non documenté. Un serveur unique bien sauvegardé et observable reste souvent plus fiable qu'un cluster que personne ne maîtrise.
L'architecture minimale
| Élément | Rôle | Erreur fréquente |
|---|---|---|
| reverse proxy | termine TLS et route le domaine vers l'application | exposer directement tous les ports des conteneurs |
| application | exécute une image versionnée et non modifiable | monter le code source du serveur dans le conteneur |
| base ou stockage | conserve l'état dans un volume sauvegardé | confondre volume persistant et sauvegarde |
| réseau interne | relie les services non publics | publier PostgreSQL, Redis ou MinIO sur toutes les interfaces |
| registre | conserve les images identifiées par version et digest | déployer uniquement latest |
| supervision | détecte panne, saturation et régression | surveiller seulement que le conteneur existe |
Le reverse proxy est généralement le seul composant qui doit recevoir du trafic public. La base, le cache et les services internes communiquent par leur nom de service sur un réseau Compose privé.
Séparer la définition commune de la production
Docker recommande de conserver un fichier spécifique à la production pour modifier les paramètres qui diffèrent du développement : ports, variables, stratégie de redémarrage, volumes et services d'observabilité.
Une organisation lisible utilise par exemple :
compose.yaml
compose.production.yaml
.env.production
Le fichier commun décrit les services. L'override de production retire les bind mounts de code, sélectionne les images du registre et fixe les contraintes d'exploitation. Avant chaque déploiement, rendez la configuration fusionnée :
docker compose \
--env-file .env.production \
-f compose.yaml \
-f compose.production.yaml \
config --quiet
Retirez --quiet lors d'une inspection, mais faites attention : la sortie résolue peut contenir des valeurs sensibles provenant de l'environnement. Ne l'archivez pas automatiquement dans les logs de CI.
Déployer une image identifiable
Une image de production doit être construite en CI, envoyée au registre et identifiée par une référence immuable. Un tag de commit comme app:3495e8e permet de savoir quel code a produit le conteneur. Un tag de version comme v1.8.2 facilite l'exploitation. Le digest fournit l'identité exacte du manifeste distribué.
Vous pouvez conserver latest comme alias pratique, mais le serveur ne devrait pas dépendre de lui pour son historique. Si latest change, la commande de rollback ne sait pas quelle ancienne image récupérer.
Dans l'environnement de production :
APP_IMAGE=registry.example.com/team/app:3495e8e
Puis dans Compose :
services:
web:
image: ${APP_IMAGE}
restart: unless-stopped
Conservez dans le pipeline le digest publié et, si le niveau de risque le justifie, générez les attestations de provenance et le SBOM avec BuildKit. Cela ne sécurise pas automatiquement l'image, mais permet de relier l'artefact à sa construction et à ses dépendances.
Ne pas embarquer les secrets dans l'image
Une variable définie pendant le build peut se retrouver dans les couches, les métadonnées ou le JavaScript public selon le framework. Une clé API serveur ne doit jamais être injectée dans un argument destiné au client.
Compose prend en charge les secrets montés sous forme de fichiers dans /run/secrets/<nom>, uniquement pour les services qui les déclarent. De nombreuses images officielles reconnaissent les variantes de variables suffixées par _FILE.
services:
db:
image: postgres:18.1
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Cette mécanique évite d'exposer le secret comme variable d'environnement du conteneur, mais elle ne chiffre pas magiquement le fichier source sur le serveur. Limitez ses permissions, excluez le dossier de Git, contrôlez les sauvegardes et prévoyez la rotation.
Les valeurs publiques intégrées au bundle, comme l'URL du site, ne sont pas des secrets. Documentez explicitement quelles variables sont résolues au build et lesquelles restent disponibles uniquement au runtime.
Réduire les privilèges du conteneur
Une image doit exécuter le processus avec un utilisateur non privilégié lorsque l'application le permet. Évitez privileged: true, le montage de /var/run/docker.sock et les capacités Linux inutiles. Monter le socket Docker revient pratiquement à donner le contrôle de l'hôte au service concerné.
Pour une application sans écriture dans son système de fichiers :
services:
web:
image: ${APP_IMAGE}
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
Testez ces options : certains runtimes écrivent temporairement des caches ou des fichiers PID. Ajoutez uniquement les chemins temporaires nécessaires plutôt que de rendre tout le conteneur modifiable.
Évitez aussi les bind mounts du code en production. Docker recommande de conserver le code dans l'image afin qu'il ne puisse pas changer depuis l'hôte. Les volumes servent aux données explicitement persistantes, pas à remplacer le système de fichiers livré par la CI.
Fermer les ports qui n'ont pas à être publics
Dans Compose, expose documente un port disponible entre conteneurs ; ports publie un port sur l'hôte. Une base utilisée uniquement par l'application n'a généralement pas besoin de ports.
services:
web:
networks: [proxy, internal]
db:
networks: [internal]
networks:
proxy:
external: true
internal:
internal: true
L'option internal: true isole le réseau des connexions externes directes. Vérifiez néanmoins les besoins réels : une base peut devoir joindre un stockage de sauvegarde ou un service de supervision. Concevez les flux au lieu de bloquer puis de rouvrir largement.
Sur le pare-feu de l'hôte, n'autorisez que SSH selon votre politique, HTTP/HTTPS vers le proxy et les ports de supervision strictement nécessaires. Un port absent du Compose peut encore être exposé par un autre processus de la machine : contrôlez l'hôte lui-même.
Healthcheck, démarrage et disponibilité
Une stratégie restart: unless-stopped redémarre un conteneur après une panne ou un redémarrage du moteur Docker. Elle ne prouve pas que l'application répond correctement. Un processus peut rester vivant tout en renvoyant des erreurs à chaque requête.
Ajoutez un healthcheck qui vérifie une dépendance essentielle sans effectuer une opération coûteuse :
services:
web:
healthcheck:
test: ["CMD", "node", "scripts/healthcheck.mjs"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
L'image doit contenir la commande utilisée. Une requête HTTP avec curl échouera si l'image minimale n'embarque pas curl. Un script Node interne évite parfois d'ajouter un outil uniquement pour le contrôle.
depends_on avec condition: service_healthy peut ordonner le démarrage, mais l'application doit tout de même tolérer qu'une base ou une API disparaisse plus tard. Implémentez des reconnexions avec délai et limite ; l'ordre initial n'est pas une stratégie de résilience.
Traiter les migrations comme une opération contrôlée
Lancer automatiquement une migration destructive au démarrage de chaque réplique crée des courses et complique le rollback. Séparez autant que possible la migration du démarrage applicatif.
Un pipeline prudent suit cet ordre :
- sauvegarder et vérifier le point de reprise nécessaire ;
- télécharger l'image cible ;
- exécuter les migrations compatibles avec l'ancienne et la nouvelle version ;
- recréer le service applicatif ;
- vérifier santé et parcours métier ;
- effectuer plus tard les suppressions de colonnes devenues inutiles.
La compatibilité en deux temps, parfois appelée expand/contract, rend le retour applicatif possible. Si la nouvelle version renomme immédiatement une colonne attendue par l'ancienne, redéployer l'ancien conteneur ne suffit plus.
Une procédure de déploiement reproductible
Sur le serveur, le script doit échouer dès qu'une étape importante échoue et verrouiller les déploiements concurrents. Le flux minimal ressemble à ceci :
docker compose --env-file .env.production -f docker-compose.yml pull
docker compose --env-file .env.production -f docker-compose.yml config --quiet
docker compose --env-file .env.production -f docker-compose.yml up -d --remove-orphans
docker compose --env-file .env.production -f docker-compose.yml ps
Ajoutez ensuite un contrôle HTTP depuis l'extérieur du conteneur et un parcours fonctionnel court. Le statut healthy ne détecte pas un domaine mal routé, un certificat expiré ou un stockage inaccessible depuis l'application.
Le déploiement doit journaliser la version précédente et la nouvelle, l'heure, l'auteur ou le pipeline, ainsi que le résultat des contrôles. Évitez d'imprimer les variables sensibles.
Préparer le rollback avant l'incident
Un retour arrière crédible ne consiste pas à retrouver un ancien commit pendant la panne. Conservez plusieurs tags d'image dans le registre et la valeur précédente de APP_IMAGE.
APP_IMAGE=registry.example.com/team/app:previous \
docker compose --env-file .env.production up -d --no-deps web
Cette commande recrée le service web sans redémarrer ses dépendances. Elle reste insuffisante si une migration incompatible a transformé les données. Le runbook doit donc indiquer :
- quelles versions peuvent revenir sans restauration ;
- comment annuler ou corriger une migration ;
- quel point de sauvegarde utiliser ;
- combien de données pourraient être perdues ;
- qui décide de restaurer la base.
Notre guide de sauvegarde et restauration des bases de données détaille les tests RPO/RTO, PostgreSQL, MySQL et PITR.
Surveiller ce qui annonce une panne
Collectez les logs avec rotation, car un conteneur bavard peut remplir le disque de l'hôte. Surveillez espace disque, mémoire, CPU, redémarrages, état des healthchecks, latence, taux d'erreur et expiration TLS.
Pour les volumes, suivez la capacité du système de fichiers et la date de la dernière restauration réussie. Un volume Docker persistant survit à la recréation du conteneur ; il ne protège ni contre la panne du disque, ni contre une suppression, ni contre un ransomware.
Ajoutez une alerte externe qui vérifie le domaine public. Si la supervision tourne uniquement sur le même serveur et que celui-ci s'éteint, elle ne peut plus prévenir personne.
La checklist avant ouverture
| Contrôle | Preuve attendue |
|---|---|
| image | tag de commit ou version et digest enregistrés |
| configuration | docker compose config --quiet réussi |
| secrets | absents de Git, de l'image et des logs |
| réseau | seuls les ports du proxy sont publics |
| processus | utilisateur non privilégié et droits réduits |
| santé | healthcheck interne et contrôle HTTP externe |
| données | volume identifié et restauration récemment testée |
| migration | procédure séparée et compatibilité de rollback |
| déploiement | script automatisé, verrouillé et journalisé |
| retour arrière | image précédente disponible et commande testée |
Le guide Vue et Nuxt pour les sites de contenu explique pourquoi une image applicative peut rester simple. Ici, l'objectif est de rendre cette simplicité exploitable : un artefact identifiable, une configuration vérifiée, des données récupérables et un retour arrière déjà écrit.



