Sur cette page
- Docker
- Docker en bref
- Image et conteneur
- Docker Compose
- Nom du projet
- Réseau
- Réseau par défaut
- localhost
- Ports
- Réseaux explicites
- Build
- Contexte de build
- Volumes et stockage
- Volume nommé
- Bind mounts
- node_modules et Docker
- Dépendances entre services
- service_started
- service_healthy
- service_completed_successfully
- Healthchecks
- Une image commune pour plusieurs services
- Plusieurs applications dans un même conteneur
- Variables d’environnement
- environment
- Interpolation Compose
- Valeur par défaut
- Variable obligatoire
- Secrets
- .env local
- Docker Secrets
- Dockerfile multi-stage
- Écrire un bon Compose
- Modèle mental à retenir
- Pour aller plus loin
Docker
Docker en bref
Docker construit et exécute une application dans un environnement reproductible.
Les concepts principaux :
Dockerfile→ décrit comment construire une imageImage→ environnement exécutable immuable contenant l’application et ses dépendancesContainer→ instance en cours d’exécution d’une imageDocker Compose→ orchestre plusieurs services et conteneursVolume→ conserve ou partage des données en dehors du cycle de vie d’un conteneurRegistry→ stocke et distribue les images DockerGit→ stocke et versionne le code source
flowchart LR accTitle: Cycle de vie d'une image Docker accDescr: Du code source au conteneur qui tourne, en passant par le registry. Git["Git<br/>code source"] Dockerfile["Dockerfile"] Image["Image Docker"] Container["Container"] Registry["Registry"] Volume["Volume"] Git --> Dockerfile Dockerfile -->|docker build| Image Image -->|docker run| Container Image -->|docker push| Registry Registry -->|docker pull| Image Container --> Volume
Image et conteneur
Une image est un modèle immuable, en lecture seule, à partir duquel Docker crée des conteneurs. Elle peut contenir par exemple :
- un système Linux minimal ;
- Node.js ;
- les dépendances npm ;
- le code compilé ;
- une commande de démarrage.
Un conteneur est une instance de cette image en cours d’exécution.
Une même image peut lancer plusieurs conteneurs avec des commandes différentes. Avec l’image projet-api:1.0 :
- le conteneur API lance
npm start; - le conteneur de migration lance
npm run db:migrate; - le conteneur worker lance
npm run worker.
Les trois conteneurs partagent le même environnement logiciel et exécutent chacun leur commande.
Docker Compose
Docker Compose décrit et orchestre une application composée de plusieurs services.
Il se charge :
- de construire ou récupérer les images ;
- de créer les conteneurs ;
- de mettre en place leur réseau ;
- de monter les volumes ;
- d’injecter leur configuration ;
- de gérer certaines dépendances de démarrage.
Exemple :
name: mon-projet
services:
api:
build: .
ports:
- "3000:3000"
db:
image: postgres:16-alpine
Le projet contient ici deux services. Le navigateur appelle api (Node.js), et api appelle db (PostgreSQL).
Nom du projet
name: mon-projet définit le nom du projet Compose.
Compose s’en sert pour nommer et isoler les ressources qu’il crée, par exemple :
mon-projet-api-1
mon-projet-db-1
mon-projet_default
mon-projet_db-data
Sans name, Compose utilise généralement le nom du dossier courant.
Documentation : https://docs.docker.com/reference/compose-file/version-and-name/
Réseau
Réseau par défaut
Si aucun réseau n’est déclaré, Compose crée automatiquement un réseau default.
Tous les services y sont connectés. Le DNS interne de Docker les rend joignables par leur nom de service.
services:
api:
# ...
db:
image: postgres:16
L’API contacte PostgreSQL avec db:5432, sans adresse IP.
Une URL PostgreSQL ressemble donc à postgresql://user:password@db:5432/mydb.
Documentation :
- https://docs.docker.com/reference/compose-file/networks/
- https://docs.docker.com/compose/how-tos/networking/
localhost
Dans un conteneur, localhost désigne le conteneur lui-même. Depuis l’API, localhost:5432 ne mène donc pas à PostgreSQL. L’API utilise le nom du service : db:5432.
Ports
La syntaxe :
ports:
- "3000:3000"
signifie : PORT_MACHINE:PORT_CONTENEUR
Donc :
ports:
- "8080:3000"
permet d’accéder depuis la machine à : http://localhost:8080 alors que l’application écoute toujours sur le port 3000 dans le conteneur.
Les services Docker n’utilisent pas ces ports exposés pour communiquer entre eux. Ils utilisent directement :
api:3000
db:5432
redis:6379
Par conséquent, une base PostgreSQL utilisée uniquement par une API Docker n’a pas nécessairement besoin de :
ports:
- "5432:5432"
Exposez seulement ce que la machine hôte doit atteindre.
Réseaux explicites
Des réseaux explicites servent à isoler certains services.
services:
frontend:
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend
networks:
frontend:
backend:
Le frontend contacte l’API. L’API contacte PostgreSQL. Le frontend ne peut pas joindre PostgreSQL, car ils ne partagent aucun réseau.
Pour un environnement de développement simple, le réseau default suffit.
Build
Contexte de build
Avec :
build:
context: .
Docker utilise le répertoire courant comme contexte de build.
Le Dockerfile accède aux fichiers présents dans ce contexte :
COPY package.json .
COPY apps/api ./apps/api
Limitez ce que Docker reçoit avec .dockerignore.
Exemple :
.git
node_modules
.env
.env.*
coverage
dist
.DS_Store
Ce fichier sert à :
- accélérer les builds ;
- ne pas envoyer de fichiers inutiles ;
- ne pas inclure un secret par accident.
Documentation : https://docs.docker.com/reference/compose-file/build/
Volumes et stockage
Un conteneur possède son propre système de fichiers.
Les modifications effectuées pendant son exécution sont écrites dans sa couche writable. Cette couche s’ajoute au-dessus de l’image, qui reste en lecture seule.
Par exemple PostgreSQL écrit ses données dans : /var/lib/postgresql/data
Sans volume, ces données appartiennent au conteneur. Si le conteneur est supprimé, sa couche writable disparaît avec lui.
Volume nommé
On peut sortir les données du cycle de vie du conteneur avec :
services:
db:
image: postgres:16-alpine
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Ici db_data est un volume Docker.
On peut supprimer puis recréer le conteneur PostgreSQL tout en conservant :
- les tables ;
- les lignes ;
- les index ;
- les utilisateurs ;
- les données internes PostgreSQL.
Le volume possède un cycle de vie indépendant du conteneur.
Attention : docker compose down supprime les conteneurs et conserve les volumes nommés. docker compose down -v supprime aussi les volumes. Pour une base de développement, cela veut dire base remise à zéro.
Bind mounts
Un bind mount connecte directement un fichier ou dossier de la machine hôte au conteneur.
volumes:
- ./apps/api:/repo/apps/api
C’est utile en développement : quand vous modifiez apps/api/src/server.ts dans l’IDE, le conteneur voit la modification tout de suite.
Documentation : https://docs.docker.com/engine/storage/bind-mounts/
En résumé :
| Type | Usage |
|---|---|
| Bind mount | Partager des fichiers avec la machine hôte |
| Volume Docker | Conserver des données gérées par Docker |
node_modules et Docker
Attention quand vous montez tout le projet :
volumes:
- .:/app
Le node_modules de la machine hôte peut contenir des dépendances spécifiques à macOS ou Windows alors que le conteneur utilise Linux.
Un pattern fréquent est :
volumes:
- .:/app
- /app/node_modules
Le code vient de la machine hôte, mais node_modules reste propre au conteneur Linux. Le volume anonyme /app/node_modules masque le dossier node_modules de l’hôte à l’intérieur du conteneur.
Dépendances entre services
depends_on déclare qu’un service dépend d’un autre.
api:
depends_on:
db:
condition: service_healthy
Trois conditions sont utiles.
service_started
depends_on:
db:
condition: service_started
Le conteneur db doit être démarré. PostgreSQL n’accepte pas forcément encore de connexions à ce moment-là.
service_healthy
depends_on:
db:
condition: service_healthy
Compose attend que le healthcheck du service réussisse. C’est le bon choix pour une base de données.
service_completed_successfully
depends_on:
migrate:
condition: service_completed_successfully
Compose attend que le service se termine avec un code de sortie 0.
Cette condition convient aux :
- migrations ;
- initialisations ;
- générations de fichiers ;
- tâches ponctuelles.
Healthchecks
Un healthcheck vérifie qu’un service répond, pas seulement que son conteneur tourne.
Exemple PostgreSQL :
db:
image: postgres:16-alpine
healthcheck:
test:
- CMD-SHELL
- pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}
interval: 5s
timeout: 5s
retries: 10
Le démarrage peut ensuite suivre cet ordre :
- PostgreSQL démarre.
pg_isreadyrépond OK :dbpasse à l’étatservice_healthy.- La migration démarre et se termine avec le code 0 : elle passe à l’état
service_completed_successfully. - L’API démarre.
Sans healthcheck, l’API démarre dès que le conteneur Postgres existe et peut échouer à sa première connexion.
Une image commune pour plusieurs services
Une même image peut servir à plusieurs rôles.
Par exemple :
services:
migrate:
image: monapp-api:dev
command: npm run db:migrate
api:
image: monapp-api:dev
L’image peut contenir :
- Node.js ;
- l’application ;
- Drizzle ;
- les dépendances ;
- les scripts npm.
Chaque conteneur choisit ce qu’il exécute.
- Le service
migrateeffectue son travail puis s’arrête. - Le service
apireste actif et écoute les requêtes HTTP.
Vous évitez ainsi de maintenir :
Dockerfile.api
Dockerfile.migrate
alors que les deux utilisent le même environnement.
Plusieurs applications dans un même conteneur
Un seul conteneur peut lancer plusieurs processus :
app
├── Vite
└── API
Par exemple :
command: >
sh -c '
npm run app1 &
npm run app2 &
wait
'
Pour un environnement de développement simple, ça passe. Un conteneur par application reste plus lisible : app1 (Vite, port 5173) appelle app2 (API, port 8787), qui appelle postgres (port 5432).
Vous obtenez :
- des logs distincts ;
- des healthchecks distincts ;
- des redémarrages indépendants ;
- une meilleure visibilité de l’état de chaque service.
Vous pouvez lancer docker compose logs app1 ou docker compose restart app2 sans toucher aux autres services.
Variables d’environnement
Compose a plusieurs mécanismes.
environment
services:
api:
environment:
PORT: 3000
NODE_ENV: development
Ces variables sont injectées dans le conteneur.
Node peut les lire avec : process.env.PORT
Interpolation Compose
Avec :
environment:
DATABASE_URL: ${DATABASE_URL}
Compose remplace ${DATABASE_URL} avant de démarrer le conteneur.
La valeur vient :
- du shell ;
- d’un fichier
.env.
Valeur par défaut
POSTGRES_USER: ${POSTGRES_USER:-monapp}
signifie :
utilise
POSTGRES_USERsi elle existe, sinonmonapp.
Variable obligatoire
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET is required}
Compose refuse de démarrer si la variable n’est pas définie.
Utilisez-le pour les paramètres sans lesquels l’application ne peut pas tourner.
Documentation : https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
Secrets
Ne stockez pas de vrais secrets dans :
- le Dockerfile ;
compose.yaml;- Git ;
- l’image Docker.
À éviter :
environment:
BETTER_AUTH_SECRET: abc123supersecret
.env local
En développement, un .env non commité suffit en général.
BETTER_AUTH_SECRET=...
DATABASE_PASSWORD=...
Puis :
environment:
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
Ajoutez au .gitignore :
.env
.env.local
et ajoutez aussi .env dans .dockerignore.
Un fichier .env.example peut en revanche être versionné :
BETTER_AUTH_SECRET=
DATABASE_URL=
Docker Secrets
Compose déclare aussi des secrets.
services:
api:
secrets:
- better_auth_secret
secrets:
better_auth_secret:
file: ./secrets/better_auth_secret
Le secret devient disponible dans le conteneur sous : /run/secrets/better_auth_secret
Attention : un Docker Secret devient un fichier, pas une variable d’environnement. Une application qui lit process.env.BETTER_AUTH_SECRET ne le trouvera pas. Deux solutions :
- lire le fichier depuis l’application ;
- injecter le secret autrement.
En développement local, un .env ignoré par Git suffit. En production, préférez un gestionnaire de secrets, un Docker Secret ou un secret de CI.
Documentation : https://docs.docker.com/reference/compose-file/secrets/
Dockerfile multi-stage
Un Dockerfile multi-stage sépare le développement, le build et la production dans un seul fichier. L’image de production ne garde que le résultat du build, sans les outils de développement. Le chapitre Dockerfile multi-stage détaille un exemple Python et un exemple Node.
Écrire un bon Compose
Pour des projets Node.js / TypeScript :
-
Utiliser le nom du service comme hostname :
db:5432,redis:6379,api:3000, plutôt que des IP. -
Ne déclarer des
networks:explicites qu’en cas de besoin d’isolation. Le réseaudefaultsuffit souvent. -
Utiliser
service_healthypour une base de données plutôt queservice_startedsi le service suivant doit immédiatement s’y connecter. -
Utiliser
service_completed_successfullypour les migrations, setups ou tâches ponctuelles. -
Ajouter un
healthcheckaux services dont d’autres dépendent. -
Ne pas commiter de vrais secrets dans
compose.yaml. -
Utiliser un
.envignoré par Git pour le développement et un mécanisme de secrets adapté pour la production. -
Utiliser :
${VAR:-default}pour les valeurs ayant un fallback acceptable. -
Utiliser :
${VAR:?required}pour les variables obligatoires. -
Éviter
container_name:sans raison particulière. -
Exposer avec
ports:uniquement les services que l’hôte doit pouvoir atteindre. -
Utiliser des volumes nommés pour les données persistantes.
-
Utiliser des bind mounts pour le code source pendant le développement.
-
Garder les
node_modulesLinux dans le conteneur plutôt que de réutiliser ceux de macOS ou Windows. -
Maintenir un
.dockerignore, surtout aveccontext: . -
Utiliser une image commune avec des commandes différentes pour des services comme
api,migrateetworkerlorsqu’ils partagent le même environnement. -
Préférer un Dockerfile multi-stage pour séparer développement, build et production.
-
Construire l’image de production une seule fois, puis déployer cette même image en staging et en production.
-
Identifier les images avec un numéro de version ou un SHA Git plutôt que de dépendre uniquement de
latest. -
Lancer
docker compose configpour voir ce que Compose va exécuter, variables résolues.
Modèle mental à retenir
Git versionne le code. Docker construit un artefact exécutable. Le registry distribue cet artefact. Compose organise son exécution. Les volumes conservent les données qui doivent survivre aux conteneurs.