Décrire une application complète avec Docker Compose
Pourquoi
À la fin de la leçon précédente, faire tourner Signalements demandait cinq commandes : deux réseaux, un volume, une base avec une dizaine d'options, une application avec autant, sans oublier l'ordre. Ces commandes vivaient dans un terminal ou, au mieux, dans un fichier README que personne ne tient à jour. Le jour où un collègue doit reproduire l'environnement, il en oublie une, publie la base sur toutes les interfaces, ou monte le volume au mauvais endroit.
Docker Compose décrit tout cela dans un fichier YAML versionné avec le code : quels services, construits ou téléchargés, avec quelle configuration, sur quels réseaux, avec quels volumes, dans quel ordre. Une seule commande crée ou met à jour l'ensemble, et la même commande, relancée, ne change que ce qui a changé. C'est l'outil de référence pour le développement local, les environnements de test, et l'hébergement d'applications simples sur un serveur unique.
Les concepts
Un fichier, un projet
Un fichier compose.yaml décrit un projet, composé de :
- services : chacun correspond à une image et à une configuration de conteneur, l'équivalent d'un
docker run; un service peut avoir plusieurs conteneurs (répliques) ; - networks : les réseaux définis par l'utilisateur (leçon 10) ;
- volumes : les volumes nommés (leçon 9) ;
- secrets et configs : des fichiers à fournir aux conteneurs.
Le nom du projet (le répertoire courant par défaut, ou le champ name) préfixe tout ce que Compose crée : le réseau donnees du projet signalements devient signalements_donnees, le conteneur du service db devient signalements-db-1. Deux projets peuvent donc cohabiter sur un hôte sans collision.
Déclaratif et idempotent
Compose est déclaratif : vous décrivez l'état voulu, pas les étapes. docker compose up compare l'état voulu à l'état réel et ne fait que les changements nécessaires : il recrée un conteneur dont la configuration a changé, laisse tranquilles les autres. Pour le savoir, il pose sur chaque objet des étiquettes (com.docker.compose.project, com.docker.compose.service, et une empreinte de la configuration, com.docker.compose.config-hash). Relancer up sans rien changer ne fait rien : la commande est idempotente.
Une spécification, un outil
Le format est défini par la Compose Specification, un standard ouvert. Les anciens numéros de format (version: "2", version: "3.8", hérités de l'ancien outil docker-compose écrit en Python) sont obsolètes : la ligne version est ignorée, Compose avertit si elle est présente. L'outil actuel est le greffon docker compose (avec une espace), écrit en Go. Sa version majeure est passée de 2 à 5 fin 2025 : les numéros 3 et 4 ont été sautés précisément pour ne plus confondre la version de l'outil avec les anciens numéros de format. Depuis la version 5, Compose délègue aussi la construction des images à Docker Bake (un composant de buildx), qui s'appuie sur BuildKit comme docker build.
En pratique
Le fichier compose.yaml de Signalements
Voici la traduction des commandes des leçons précédentes, avec les bonnes pratiques accumulées :
name: signalements
services:
app:
build: .
image: signalements:1.0
environment:
DATABASE_URL: postgresql://signalements:${POSTGRES_PASSWORD:?à définir dans .env}@db:5432/signalements
APP_VERSION: "1.0"
ports:
- "127.0.0.1:8000:8000"
networks: [frontal, donnees]
depends_on:
db:
condition: service_healthy
read_only: true
tmpfs:
- /tmp:size=16m
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/sante', timeout=2)"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
restart: unless-stopped
db:
image: postgres:18.6
environment:
POSTGRES_USER: signalements
POSTGRES_DB: signalements
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?à définir dans .env}
volumes:
- pgdata:/var/lib/postgresql
networks: [donnees]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U signalements -d signalements -h 127.0.0.1"]
interval: 5s
timeout: 3s
retries: 5
start_period: 30s
restart: unless-stopped
networks:
frontal:
donnees:
internal: true
volumes:
pgdata:Chaque élément renvoie à une leçon :
build: .etimage: signalements:1.0: Compose construit l'image à partir du Dockerfile du répertoire (leçon 8) et l'étiquette avec ce nom.environmentavec${POSTGRES_PASSWORD:?...}: la valeur vient de l'environnement ou d'un fichier.env, et la syntaxe:?fait échouer Compose si elle est absente, plutôt que de démarrer avec un mot de passe vide.portspublié sur127.0.0.1uniquement (leçon 10) ; la base n'a aucun port publié.- Deux réseaux :
frontalpour l'accès à l'application,donneeseninternalpour la base, sans sortie vers l'extérieur. L'application est sur les deux, la base sur le second seulement. depends_onaveccondition: service_healthy: l'application ne démarre que lorsque la base est saine, pas seulement démarrée.read_onlyettmpfs: racine en lecture seule,/tmpen mémoire (leçon 9).- Deux
healthcheckqui testent le chemin réel des clients (leçon 6). L'imagepython:3.14-slimne contenant nicurlniwget, la vérification de l'application utilise Python lui-même. volumes: pgdatamonté sur/var/lib/postgresql, le chemin de PostgreSQL 18 (leçon 9).postgres:18.6plutôt quepostgres:18: une version précise, que l'on fait évoluer volontairement.
Valider avant de lancer
docker compose config analyse le fichier, applique l'interpolation et affiche la configuration complète, normalisée. C'est le premier réflexe après chaque modification :
$ docker compose config
error while interpolating services.app.environment.DATABASE_URL: required variable POSTGRES_PASSWORD is missing a value: à définir dans .env
Le garde-fou fonctionne. Créons le fichier .env, que Compose lit automatiquement dans le répertoire du projet :
$ printf 'POSTGRES_PASSWORD=change-moi-en-production\n' > .env
$ chmod 600 .env
$ docker compose config
name: signalements
services:
app:
build:
context: /home/vous/signalements
dockerfile: Dockerfile
depends_on:
db:
condition: service_healthy
required: true
environment:
APP_VERSION: "1.0"
DATABASE_URL: postgresql://signalements:change-moi-en-production@db:5432/signalements
...
ports:
- mode: ingress
host_ip: 127.0.0.1
target: 8000
published: "8000"
protocol: tcp
...
(Sortie abrégée.) Les valeurs par défaut sont explicitées (le Dockerfile, required: true), la syntaxe courte des ports est développée, et le mot de passe apparaît en clair : ne collez jamais la sortie de docker compose config dans un ticket ou une discussion. Le fichier .env ne doit pas non plus être versionné : ajoutez-le au .gitignore, et vérifiez qu'il figure dans le .dockerignore pour qu'il n'entre pas dans l'image (c'est le cas du nôtre depuis la leçon 8).
Démarrer
$ docker compose up -d --wait
Volume signalements_pgdata Created
Network signalements_donnees Created
Container signalements-db-1 Creating
Network signalements_frontal Created
Container signalements-db-1 Created
Container signalements-app-1 Creating
Container signalements-app-1 Created
Container signalements-db-1 Starting
Container signalements-db-1 Started
Container signalements-db-1 Waiting
Container signalements-db-1 Healthy
Container signalements-app-1 Starting
Container signalements-app-1 Started
...
Container signalements-app-1 Healthy
(Sortie abrégée.) On lit l'ordre exact : volume et réseaux, création des conteneurs, démarrage de la base, attente de sa santé, puis seulement démarrage de l'application. -d détache ; --wait ne rend la main qu'une fois tous les services sains, ce qui est précieux dans un script de déploiement ou de test.
$ docker compose ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}\t{{.Ports}}'
NAME SERVICE STATUS PORTS
signalements-app-1 app Up 5 seconds (healthy) 127.0.0.1:8000->8000/tcp
signalements-db-1 db Up 11 seconds (healthy) 5432/tcp
$ curl -s -X POST localhost:8000/signalements -H 'Content-Type: application/json' \
-d '{"lieu":"Chemin du Moulin","description":"Nid-de-poule"}'
{"description":"Nid-de-poule","id":1,"lieu":"Chemin du Moulin"}
Pour la base, 5432/tcp sans adresse signifie « port exposé par l'image, non publié ».
Les commandes du quotidien
Les commandes de Compose reprennent celles de Docker, mais s'adressent aux services par leur nom :
$ docker compose logs --tail 3 app
app-1 | [2026-10-01 10:24:45 +0000] [1] [INFO] Using worker: sync
app-1 | [2026-10-01 10:24:45 +0000] [7] [INFO] Booting worker with pid: 7
app-1 | [2026-10-01 10:24:45 +0000] [8] [INFO] Booting worker with pid: 8
$ docker compose exec db psql -U signalements -tAc "SELECT id, lieu FROM signalements"
1|Chemin du Moulin
docker compose logs -f sans nom de service mélange les journaux de tous les services, préfixés et colorés : la vue idéale pendant le développement. Les étiquettes posées par Compose permettent aussi de retrouver ses objets avec les commandes Docker ordinaires :
$ docker network ls --filter label=com.docker.compose.project=signalements --format '{{.Name}} {{.Internal}}'
signalements_donnees true
signalements_frontal false
$ docker volume ls --filter label=com.docker.compose.project=signalements --format '{{.Name}}'
signalements_pgdata
Mettre à jour : ce que Compose recrée, et ce qu'il ne voit pas
Passons la version affichée de 1.0 à 1.1 dans compose.yaml, puis relançons la même commande :
$ docker compose up -d --wait
Container signalements-db-1 Running
Container signalements-app-1 Recreate
Container signalements-app-1 Recreated
Container signalements-app-1 Starting
Container signalements-app-1 Started
$ curl -s localhost:8000/
{"application":"signalements","conteneur":"915d7fb04801","stockage":"postgresql","version":"1.1"}
Compose a recalculé l'empreinte de configuration de chaque service : celle de app a changé, il l'a recréé ; celle de db non, il l'a laissé tourner. Le nom d'hôte de l'application a changé : c'est un nouveau conteneur.
Modifions maintenant le code de app.py (ajout d'un champ message dans la réponse) et relançons :
$ docker compose up -d --wait
Container signalements-db-1 Running
Container signalements-app-1 Running
$ curl -s localhost:8000/
{"application":"signalements","conteneur":"915d7fb04801","stockage":"postgresql","version":"1.1"}
Rien n'a changé. La configuration du service est identique, et l'image signalements:1.0 existe déjà : Compose n'a aucune raison de reconstruire. Il faut le demander :
$ docker compose up -d --wait --build
Image signalements:1.0 Built
Container signalements-db-1 Running
Container signalements-app-1 Recreate
Container signalements-app-1 Recreated
Container signalements-app-1 Started
$ curl -s localhost:8000/
{"application":"signalements","conteneur":"3e4126b03d50","message":"bonjour","stockage":"postgresql","version":"1.1"}
Retenez le réflexe : après une modification du code ou du Dockerfile, --build. Le cache de construction (leçon 8) rend l'opération rapide quand rien n'a changé.
Arrêter : down et down -v
$ docker compose down
Container signalements-app-1 Stopping
Container signalements-app-1 Stopped
Container signalements-app-1 Removing
Container signalements-app-1 Removed
Container signalements-db-1 Stopping
Container signalements-db-1 Stopped
Container signalements-db-1 Removing
Container signalements-db-1 Removed
Network signalements_frontal Removing
Network signalements_donnees Removing
Network signalements_donnees Removed
Network signalements_frontal Removed
$ docker volume ls --filter name=signalements_pgdata --format '{{.Name}}'
signalements_pgdata
down supprime les conteneurs et les réseaux, dans l'ordre inverse des dépendances, mais conserve les volumes. Au prochain up, les données sont là :
$ docker compose up -d --wait
$ curl -s localhost:8000/signalements
[{"description":"Nid-de-poule","id":1,"lieu":"Chemin du Moulin"}]
down -v supprime en plus les volumes du projet, données comprises :
$ docker compose down -v
...
Volume signalements_pgdata Removed
...
C'est la commande pour repartir d'un environnement propre en développement. Sur un serveur, c'est la commande qui efface la production : elle ne doit figurer dans aucun script de déploiement.
Développement : le fichier de surcharge
En développement, on veut recharger le code à chaque modification, sans reconstruire. Compose fusionne automatiquement compose.yaml avec un fichier compose.override.yaml s'il existe. On y met tout ce qui est propre au poste de développement :
# Réglages du poste de développement, chargés automatiquement par « docker compose ».
services:
app:
command: ["flask", "--app", "app", "run", "--host", "0.0.0.0", "--port", "8000", "--debug"]
environment:
APP_VERSION: "dev"
volumes:
- type: bind
source: .
target: /app
read_only: true
adminer:
image: adminer:5
profiles: [outils]
ports:
- "127.0.0.1:8081:8080"
networks: [donnees, frontal]commandremplace Gunicorn par le serveur de développement de Flask, en mode debug : il recharge l'application à chaque modification de fichier.- Le répertoire du projet est monté sur
/app, par-dessus le code copié dans l'image, en lecture seule : le conteneur voit vos modifications en direct. adminerest une interface web d'administration de bases de données, rangée dans le profiloutils: elle ne démarre que si on le demande.
Les listes et les dictionnaires se fusionnent selon des règles précises (une variable d'environnement redéfinie remplace l'ancienne, un volume s'ajoute aux autres) ; docker compose config montre le résultat de la fusion.
$ docker compose up -d --wait --build
$ curl -s localhost:8000/
{
"application": "signalements",
"conteneur": "cfb6cd1e3b09",
"stockage": "postgresql",
"version": "dev"
}
En mode debug, Flask met en forme le JSON. Modifions app.py sur l'hôte, puis interrogeons de nouveau, sans aucune commande Docker :
$ curl -s localhost:8000/
{
"application": "signalements",
"conteneur": "cfb6cd1e3b09",
"message": "rechargé à chaud",
"stockage": "postgresql",
"version": "dev"
}
$ docker compose logs --tail 6 app | grep -iE "Detected|Restarting"
app-1 | * Restarting with stat
Même conteneur, nouveau code : Flask a détecté le changement et redémarré l'application.
Les profils
$ docker compose config --services
db
app
$ docker compose --profile outils config --services
db
app
adminer
$ docker compose --profile outils up -d --wait
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8081/
200
Un service avec profiles n'existe que si l'un de ses profils est activé, par --profile ou la variable COMPOSE_PROFILES. C'est le moyen de garder dans le même fichier des outils occasionnels (administration, jeux de données de test, outils de profilage) sans les lancer à chaque fois.
En production : sans la surcharge
Sur un serveur, on ne veut ni le serveur de développement, ni le montage du code, ni Adminer. Deux façons de faire : ne pas déployer compose.override.yaml sur le serveur, ou désigner explicitement les fichiers, ce qui désactive le chargement automatique de la surcharge :
$ docker compose -f compose.yaml up -d --wait
On peut aussi empiler un fichier propre à la production : docker compose -f compose.yaml -f compose.prod.yaml up -d. La règle des douze facteurs s'applique : la même image en développement et en production, seule la configuration change.
Sous le capot
Compose n'a pas de démon ni d'état propre. Tout ce qu'il sait d'un projet, il le relit à chaque commande dans le démon Docker, grâce aux étiquettes qu'il a posées sur les conteneurs, réseaux et volumes. C'est pourquoi on peut piloter un projet depuis n'importe quel terminal, à condition d'utiliser le même nom de projet, et pourquoi supprimer un conteneur à la main avec docker rm ne perturbe pas Compose : il le recréera au prochain up.
Pour décider s'il faut recréer un conteneur, Compose calcule l'empreinte de la configuration normalisée du service (celle qu'affiche docker compose config) et la compare à l'étiquette com.docker.compose.config-hash du conteneur existant :
$ docker inspect signalements-app-1 --format '{{json .Config.Labels}}' | jq '...'
{
"project": "signalements",
"service": "app",
"config_hash": "b6d2dbee80a3"
}
Compose compare aussi l'image : il enregistre son empreinte dans l'étiquette com.docker.compose.image et recrée le conteneur si l'image locale portant ce nom a changé. Encore faut-il que l'image ait été reconstruite (--build) ou téléchargée de nouveau (docker compose pull) : sans cela, l'image locale est toujours la même, et c'est pourquoi la modification de app.py n'avait rien changé.
depends_on avec service_healthy est appliqué au démarrage par Compose, qui attend l'état healthy de la dépendance avant de démarrer le service. Il ne vaut que pour Compose : si la base redémarre plus tard, l'application ne sera pas redémarrée ni prévenue. Une application robuste doit donc, de toute façon, savoir réessayer sa connexion.
Pièges courants
Le code modifié n'est pas pris en compte. up sans --build réutilise l'image existante. En développement, utilisez un montage du code ; en déploiement, --build ou un pull d'une nouvelle étiquette.
Le montage d'un fichier unique « ne se met pas à jour ». Une première version de la surcharge montait seulement ./app.py sur /app/app.py. Après modification du fichier sur l'hôte, le conteneur voyait toujours l'ancien contenu :
$ docker compose exec app grep -c "rechargé" /app/app.py
0
$ ls -i app.py
25460489 app.py
$ docker compose exec app stat -c %i /app/app.py
25460486
Les numéros d'inode diffèrent. Un montage de fichier unique est attaché à l'inode du fichier au moment du montage. Or la plupart des éditeurs (et sed -i) enregistrent en écrivant un nouveau fichier puis en le renommant par-dessus l'ancien : le chemin désigne alors un nouvel inode, et le conteneur garde l'ancien. Montez le répertoire parent, comme dans la version finale.
--scale et les ports publiés. Un service qui publie un port fixe de l'hôte ne peut pas avoir deux répliques :
$ docker compose up -d --scale app=2
...
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint signalements-app-2 (1172dc9cfc3e...): Bind for 127.0.0.1:8000 failed: port is already allocated
Pour plusieurs répliques, on retire la publication et on place un reverse proxy devant (leçon 10, exercice 3), qui répartit les requêtes entre les conteneurs du service grâce au DNS du réseau.
depends_on sans condition. La forme courte depends_on: [db] n'attend que le démarrage du conteneur de la base, pas sa disponibilité : l'application démarre pendant l'initialisation de PostgreSQL et échoue, comme à la leçon 10 où ses workers refusaient de démarrer sans base joignable. Utilisez condition: service_healthy avec une vérification de santé honnête.
Le .env de Compose n'est pas celui de --env-file. Le fichier .env du projet sert à l'interpolation du fichier Compose (les ${...}) ; il n'est pas transmis aux conteneurs, sauf si une variable y est explicitement reprise. Pour passer un fichier de variables à un conteneur, c'est l'attribut env_file du service. Et les règles de syntaxe diffèrent de celles de docker run --env-file (leçon 5).
Le nom de projet implicite. Sans champ name, le projet prend le nom du répertoire. Deux copies du projet dans deux répertoires de même nom, sur le même hôte, partagent alors leurs conteneurs et leurs volumes. Fixer name dans le fichier rend le nom prévisible, mais toutes les copies le partagent alors : pour faire cohabiter deux copies (deux environnements de recette, par exemple), donnez à chacune son nom avec -p ou la variable COMPOSE_PROJECT_NAME.
Sécurité
- Les secrets ne vont ni dans
compose.yamlni dans Git. Utilisez l'interpolation depuis un.envnon versionné aux droits restreints (chmod 600), ou des secrets. - Les secrets de Compose, et leurs limites. Compose peut fournir un secret sous forme de fichier, ce qui évite de l'exposer dans l'environnement du conteneur (visible par
docker inspect, leçon 6). L'image PostgreSQL lit son mot de passe dans un fichier si l'on utilisePOSTGRES_PASSWORD_FILE:
services:
db:
image: postgres:18.6
environment:
POSTGRES_USER: signalements
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
secrets:
db_password:
file: ./secrets/db_password.txt$ docker compose -f compose.secrets.yaml -p essai-secrets exec db sh -c 'ls -l /run/secrets; grep /run/secrets /proc/mounts'
total 4
-rw------- 1 1000 1000 33 Oct 1 10:26 db_password
/dev/nvme0n1p2 /run/secrets/db_password ext4 ro,relatime 0 0
$ docker inspect essai-secrets-db-1 --format '{{json .Config.Env}}' | tr ',' '\n' | grep POSTGRES
["POSTGRES_USER=signalements"
"POSTGRES_PASSWORD_FILE=/run/secrets/db_password"
Le mot de passe n'apparaît plus dans docker inspect. Mais regardez le montage : hors du mode Swarm, un secret de Compose est un simple montage en lecture seule du fichier de l'hôte (ici sur une partition ext4), pas un tmpfs. Il garde le propriétaire et les droits du fichier de l'hôte (UID 1000, 0600) : un service non-root avec un autre UID ne pourra pas le lire. PostgreSQL y parvient parce que son script d'entrée lit le fichier en root avant d'abandonner ses privilèges. Les secrets de Compose protègent donc contre l'exposition dans l'environnement, pas contre un accès au disque de l'hôte.
- Jamais le mode debug en dehors du poste de développement. Le débogueur de Werkzeug, activé par
--debug, permet d'exécuter du code Python arbitraire depuis le navigateur sur une page d'erreur, protégé seulement par un code PIN affiché dans les journaux. La documentation de Flask l'interdit explicitement en production. Notre surcharge le publie sur127.0.0.1uniquement, et le fichier de surcharge ne doit jamais être déployé. - Relisez la configuration effective.
docker compose configmontre ce qui sera vraiment appliqué : ports publiés, montages, privilèges. Les outils d'analyse de configuration (comme KICS, qui sait lire les fichiers Compose) peuvent le faire en CI.
En production
Compose est un outil sérieux pour l'hébergement d'une application sur un seul serveur : une petite application métier, un outil interne, un environnement de recette. C'est d'ailleurs ainsi que tournent beaucoup d'applications chez les clients de Lyneko avant, ou à la place, d'un cluster. Pour qu'il le soit vraiment :
- le fichier Compose et sa surcharge de production sont versionnés ; le
.envest fourni par un gestionnaire de secrets ou déposé par l'outil de déploiement ; - les images viennent d'un registre, avec des étiquettes uniques (pas de
build:sur le serveur) ; le déploiement estdocker compose pull && docker compose up -d --wait; - les politiques
restart: unless-stopped, les vérifications de santé, la rotation des journaux (leçon 4) et des sauvegardes testées (leçon 9) sont en place ; - on connaît les limites : un seul hôte, donc pas de haute disponibilité ; pas de mise à jour progressive sans interruption ; pas de réordonnancement automatique si le serveur tombe.
Au-delà, on passe à un orchestrateur. Le passage est facilité par le fait que les concepts sont les mêmes : un service Compose devient un Deployment et un Service Kubernetes, un volume un PersistentVolumeClaim, un réseau des NetworkPolicy, un secret un Secret. Des outils comme Kompose proposent une première traduction automatique, à relire.
Exercices
1. Traduire en Compose. Traduisez en un fichier compose.yaml l'architecture de la leçon 10, exercices 3 et 4 : application, base sur un réseau interne, et un reverse proxy nginx publié sur 127.0.0.1:8080, l'application n'étant pas publiée.
Solution
Partez du fichier de la leçon, retirez ports du service app, et ajoutez :
proxy:
image: nginx:1.29
ports:
- "127.0.0.1:8080:80"
volumes:
- type: bind
source: ./proxy.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
networks: [frontal]
depends_on:
app:
condition: service_healthyproxy.conf est celui de la leçon 10, avec proxy_pass http://app:8000;. Vérifiez avec docker compose up -d --wait puis curl -s localhost:8080/. Le proxy, seulement sur frontal, ne peut pas joindre db.
2. Ce qui sera recréé. Pour chacune de ces modifications, prévoyez ce que fera docker compose up -d, puis vérifiez : (a) changer interval du healthcheck de db ; (b) ajouter un commentaire YAML dans compose.yaml ; (c) changer POSTGRES_PASSWORD dans .env ; (d) modifier le Dockerfile.
Solution
(a) Seul db est recréé (sa configuration change) ; app reste en place (Running), bien qu'il en dépende. Les données sont conservées dans le volume. L'application perd sa connexion pendant quelques secondes : c'est là qu'on apprécie une application qui sait se reconnecter. (b) Rien : un commentaire ne change pas la configuration normalisée. (c) Les deux services sont recréés (la variable apparaît dans leurs deux configurations), mais la base garde l'ancien mot de passe, puisque POSTGRES_PASSWORD n'est lu qu'à l'initialisation (leçon 9), alors que l'application utilise le nouveau : elle ne peut plus se connecter. Il faut changer le mot de passe dans la base avec ALTER ROLE avant de changer .env. (d) Rien sans --build.
3. Une sauvegarde intégrée (niveau 200). Ajoutez au projet un service sauvegarde, dans un profil maintenance, qui produit un pg_dump de la base dans un répertoire ./sauvegardes de l'hôte puis s'arrête. Lancez-le avec docker compose run.
Solution
sauvegarde:
image: postgres:18.6
profiles: [maintenance]
networks: [donnees]
environment:
PGPASSWORD: ${POSTGRES_PASSWORD:?à définir dans .env}
volumes:
- type: bind
source: ./sauvegardes
target: /sauvegardes
command: ["sh", "-c", "pg_dump -h db -U signalements --format=custom signalements > /sauvegardes/signalements-$$(date +%Y%m%d-%H%M%S).dump"]
depends_on:
db:
condition: service_healthy$ mkdir -p sauvegardes
$ docker compose --profile maintenance run --rm sauvegarde
Le $$ échappe le $ pour que Compose ne tente pas d'interpoler $(date ...) : c'est le shell du conteneur qui l'évaluera. run lance un conteneur ponctuel du service, --rm le supprime ensuite. L'horodatage du nom de fichier est en UTC, le fuseau par défaut des conteneurs. Le répertoire ./sauvegardes doit exister et être accessible en écriture par root (le conteneur tourne en root ici).
4. Les secrets jusqu'au bout (niveau 300). Modifiez le projet pour qu'aucun mot de passe n'apparaisse dans docker inspect d'aucun conteneur. Que faut-il changer dans l'application ?
Solution
La base utilise déjà POSTGRES_PASSWORD_FILE. L'application, elle, reçoit son mot de passe dans DATABASE_URL. Il faut qu'elle sache lire un fichier : par exemple une variable DATABASE_PASSWORD_FILE dont le contenu est injecté dans la chaîne de connexion au démarrage (psycopg accepte un paramètre password séparé). On déclare alors le même secret pour app, en s'assurant que l'UID 10001 peut le lire : hors Swarm, Compose conserve le propriétaire du fichier de l'hôte, il faut donc un fichier lisible par cet UID (par exemple propriétaire 10001, droits 0400), ce qui demande sudo sur l'hôte. C'est une limite réelle de Compose sans Swarm, et l'une des raisons pour lesquelles on passe à un gestionnaire de secrets (Vault, Scaleway Secret Manager) ou à Kubernetes pour des secrets sensibles.
Récapitulatif
- Un
compose.yamldécrit services, réseaux, volumes et secrets d'un projet ; il remplace les commandesdocker runet se versionne avec le code. La ligneversion:est obsolète. docker compose configvalide et montre la configuration effective ;${VAR:?message}refuse de démarrer sans une variable obligatoire.up -d --waitcrée ou met à jour l'ensemble et attend la santé ; Compose ne recrée que ce dont la configuration ou l'image locale a changé.--buildaprès une modification du code.depends_onaveccondition: service_healthyordonne les démarrages sur la santé réelle.downconserve les volumes,down -vles supprime.compose.override.yamlpour le développement (code monté, rechargement), profils pour les outils occasionnels,-f compose.yamlen production.- Les secrets de Compose évitent l'exposition dans l'environnement, mais restent, hors Swarm, des fichiers de l'hôte montés en lecture seule.
Pour aller plus loin
- La Compose Specification, la référence de chaque attribut.
- La page Merge Compose files, qui détaille les règles de fusion des fichiers multiples.
- La fonctionnalité
docker compose watch(attributdevelop), alternative au montage de code qui synchronise les fichiers ou reconstruit l'image selon des règles. - Leçon suivante : sécuriser et limiter les conteneurs, avant de partir en production.
Sources
- Compose Specification
- Docker, Control startup and shutdown order in Compose
- Docker, Interpolation et fichier .env
- Docker, Merge Compose files (fichiers de surcharge)
- Docker, Using profiles with Compose
- Docker, Secrets in Compose
- Docker Compose v5.0.0, notes de version
- Flask, Debug Mode (avertissement de sécurité)
- The Twelve-Factor App, X. Dev/prod parity