Carnet, support de cours
Docker
Docker construit et exécute une application dans un environnement reproductible.
Sommaire
- Dockerfile multi-stageExemple Python (FastAPI), Exemple Node : le Dockerfile en un seul stage, Stage base, Stage dev, Stage build, Stage production
- Docker, Git et les environnementsPush, Pull
- Docker Cheat-SheetModèle mental, Build, Pull / suppression, Attention à localhost, Bind mount, Simple, Avec healthcheck, Docker secrets
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.
Pour aller plus loin
Dockerfile multi-stage
Un Dockerfile multi-stage déclare plusieurs étapes (stages) dans un seul fichier. Chaque environnement construit l’étape dont il a besoin.
Exemple Python (FastAPI)
# ---------- Base commune ----------
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# ---------- Développement ----------
FROM base AS dev
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY . .
CMD ["uvicorn","app.main:app","--host","0.0.0.0","--port","8000","--reload"]
# ---------- Build des dépendances ----------
FROM base AS builder
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel \
--no-cache-dir \
--wheel-dir /wheels \
-r requirements.txt
# ---------- Production ----------
FROM base AS production
COPY --from=builder /wheels /wheels
RUN pip install \
--no-cache-dir \
/wheels/* \
&& rm -rf /wheels
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
flowchart TD Base["base<br/>Python + config commune"] Base --> Dev["dev<br/>outils dev + reload"] Base --> Builder["builder<br/>gcc + libpq-dev<br/>compile les dépendances"] Builder --> Production["production<br/>runtime minimal"] Dev --> Local["Développement local"] Production --> Staging["Staging"] Production --> Prod["Production"]
builder ne se déploie pas. C’est une étape intermédiaire qui compile les dépendances en wheels.
En local, Compose demande un stage précis avec target :
services:
api:
build:
context: .
target: dev
Docker construit alors jusqu’au stage dev, qui lance Uvicorn avec --reload.
Exemple Node : le Dockerfile en un seul stage
FROM node:24
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
Ce Dockerfile fonctionne, mais l’image finale embarque des outils inutiles en production :
- TypeScript ;
- Vite ;
- ESLint ;
- Vitest ;
- fichiers source ;
- outils de build ;
devDependencies.
Le multi-stage découpe ce fichier en étapes spécialisées.
Stage base
FROM node:24-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
base contient ce que les autres stages partagent : Node.js, le dossier de travail et les fichiers package.json.
Stage dev
FROM base AS dev
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
Cette image peut contenir :
- dépendances de développement ;
- TypeScript ;
- Vite ;
- hot reload ;
- code source ;
- outils de debug.
Compose sélectionne ce stage avec target :
build:
context: .
target: dev
Stage build
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
Ce stage compile les sources TypeScript dans dist/. Il ne fait rien d’autre.
Stage production
FROM node:24-alpine AS production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
La ligne qui compte :
COPY --from=build /app/dist ./dist
Elle copie seulement le résultat du stage build.
flowchart TD Base["base"] Base --> Dev["dev"] Base --> Build["build"] Build -->|"COPY --from=build"| Production["production"] Dev --> DevInfo["sources<br/>devDependencies<br/>Vite<br/>tests"] Production --> ProdInfo["dist/<br/>prod dependencies<br/>runtime"]
L’image de production n’a donc pas besoin de contenir :
- les tests ;
- ESLint ;
- Vite ;
- TypeScript ;
- les outils de build ;
- les devDependencies.
Résultat :
- des images plus petites ;
- moins de surface d’attaque ;
- des démarrages et transferts plus rapides ;
- une séparation claire entre build et runtime.
Docker, Git et les environnements
Un projet a en général trois environnements :
- développement local ;
- staging / préproduction ;
- production.
Ils ne correspondent pas forcément à trois branches Git. Le modèle à retenir :
flowchart TD Git["Git<br/>commit abc123"] CI["CI"] Build["docker build<br/>target production"] Registry["Registry<br/>app:abc123"] Staging["Staging"] Prod["Production"] Git --> CI CI --> Build Build --> Registry Registry --> Staging Registry --> Prod
Build once, deploy many.
La CI construit l’image une seule fois, puis cette même image part dans chaque environnement.
Registry
Un registry est un serveur qui stocke des images Docker.
Exemples :
- Docker Hub ;
- GitHub Container Registry ;
- GitLab Container Registry ;
- Forgejo Container Registry ;
- registry privé.
GitHub ou Forgejo peuvent donc fournir deux services différents : un dépôt Git pour le code source et un Container Registry pour les images Docker.
Par exemple, git.example.org/theo/monapp peut stocker le code Git, et git.example.org/theo/monapp:abc123 peut désigner une image dans le registry Forgejo.
Push
Après un build avec docker build -t git.example.org/theo/monapp:abc123 ., envoyez l’image avec docker push git.example.org/theo/monapp:abc123.
Pull
Un serveur récupère ensuite cette image précise : docker pull git.example.org/theo/monapp:abc123
CI/CD
Un pipeline classique :
- Le développeur fait
git push. - Forgejo Actions ou GitHub Actions lance les tests, le lint et le typecheck.
- La CI construit l’image avec
docker build. - La CI pousse l’image dans le Container Registry, par exemple
app:abc123. - Le staging déploie cette image.
- Après validation, la production déploie la même image.
Les serveurs de staging et de production ne reconstruisent pas l’application. Ils récupèrent une image déjà construite.
Développement local
En local, les besoins changent. On veut :
- hot reload ;
- bind mounts ;
- debug ;
- devDependencies ;
- données locales.
Docker Compose construit l’image au stage dev et lance deux conteneurs : l’API et PostgreSQL. Un bind mount place le code local dans le conteneur de l’API.
Un docker compose up --build suffit. Le registry n’intervient pas dans la boucle de développement quotidienne.
Staging et production
Staging et production diffèrent par leur configuration. Le code est le même.
Staging
APP_URL=https://staging.example.org
DATABASE_URL=...
BETTER_AUTH_SECRET=secret-staging
Production
APP_URL=https://example.org
DATABASE_URL=...
BETTER_AUTH_SECRET=secret-production
L’image reste la même.
Git et versions
Trois branches develop -> staging -> production ne sont pas nécessaires.
On peut garder main comme branche principale et identifier les déploiements par commit.
Par exemple, avec les commits A, B, C, D et E sur main, la production peut utiliser l’image app:C pendant que staging teste app:E.
Une fois E validé, la production passe à l’image app:E, celle que staging a testée.
Tags
Pour les releases, on peut associer un tag Git à une image Docker.
Git tag
v1.4.0
Docker image
app:1.4.0
On peut aussi taguer par SHA : app:a81f25d.
Ne dépendez pas uniquement de app:latest : ce tag ne dit pas quelle version tourne.
Docker Cheat-Sheet
Modèle mental
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
Une même image peut lancer plusieurs conteneurs avec des commandes différentes :
image API
├── api → uvicorn app.main:app
├── migrate → alembic upgrade head
└── worker → python worker.py
État de Docker
docker --version
docker info # daemon, drivers, stockage...
docker ps # conteneurs actifs
docker ps -a # tous les conteneurs
docker images # images locales
docker stats # CPU / mémoire en direct
docker system df # occupation disque Docker
Images
Build
# Dockerfile dans le dossier courant
docker build -t app:1.0 .
# Contexte ./app
docker build -t app:1.0 ./app
# Dockerfile spécifique
docker build -t app:1.0 -f app/Dockerfile .
# Sans cache
docker build --no-cache -t app:1.1 .
# Met à jour l'image de base
docker build --pull -t app:1.1 .
-t signifie tag :
app:1.0
│ └── version/tag
└────── nom de l'image
Une image est immuable : un nouveau build produit une nouvelle image.
En développement, un bind mount permet cependant de modifier le code sans rebuild.
Pull / suppression
docker pull nginx:alpine
docker image rm app:1.0
docker image prune
Conteneurs
docker run -d \
--name web \
-p 8080:80 \
nginx:alpine
-d arrière-plan
--name web nom du conteneur
-p 8080:80 HOST:CONTAINER
-e VAR=value variable d'environnement
-v SRC:DEST volume / bind mount
--rm supprime après arrêt
-it terminal interactif
Exemple éphémère :
docker run --rm -it alpine sh
Arrêter / supprimer :
docker stop web
docker rm web
docker rm -f web
Logs & debug
docker logs web
docker logs --tail 100 -f web
docker exec -it web sh
docker top web
docker port web
docker exec exécute une commande dans un conteneur déjà lancé.
Ports
docker run -p 8080:80 nginx
signifie :
machine hôte conteneur
localhost:8080 → :80
Dans Compose :
ports:
- "8080:80"
Attention :
EXPOSE 80
documente le port attendu par l’image, mais ne publie pas le port sur l’hôte.
Réseau Docker Compose
Compose crée automatiquement un réseau default.
Les services peuvent se joindre par leur nom de service.
services:
api:
# ...
db:
image: postgres:16
Depuis api :
db:5432
désigne PostgreSQL.
Exemple :
postgresql://user:password@db:5432/app
Attention à localhost
Depuis votre ordinateur :
localhost = votre ordinateur
Depuis un conteneur :
localhost = ce conteneur lui-même
Donc depuis api :
on ne fait pas : localhost:5432
mais on indique le service : db:5432
Volumes
Les conteneurs sont considérés comme remplaçables.
Sans volume, leurs données disparaissent lorsque le conteneur est supprimé.
Volume nommé
services:
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Postgres
│
▼
db_data
db_data survit à la suppression/recréation du conteneur.
Typiquement utilisé pour :
- bases de données ;
- uploads ;
- fichiers utilisateurs ;
- données applicatives persistantes.
Bind mount
volumes:
- ./src:/app/src
machine hôte conteneur
./src ↔ /app/src
Utilisé surtout en développement pour avoir :
modification IDE
↓
fichier modifié dans le conteneur
↓
hot reload
Résumé
| Type | Usage |
|---|---|
| bind mount | partager des fichiers avec l’hôte |
| volume | persister des données Docker |
Node et node_modules
Éviter de monter les node_modules macOS/Windows dans Linux :
volumes:
- .:/app
- /app/node_modules
Le code vient de l’hôte, mais /app/node_modules reste propre au conteneur Linux.
Docker Compose
docker compose up
docker compose up -d
docker compose up -d --build
docker compose ps
docker compose logs -f
docker compose logs -f api
docker compose exec api sh
docker compose restart api
docker compose down
Cibler un service :
docker compose up -d --build api
Voir la configuration réellement résolue :
docker compose config
Pour vérifier :
- variables ;
- anchors ;
- extensions ;
- includes ;
- fichiers Compose combinés.
depends_on
Simple
api:
depends_on:
- db
Garantit l’ordre de démarrage, mais pas que PostgreSQL soit prêt.
Avec healthcheck
db:
image: postgres:16
healthcheck:
test:
- CMD-SHELL
- pg_isready -U postgres
interval: 5s
timeout: 5s
retries: 10
Puis :
api:
depends_on:
db:
condition: service_healthy
db démarre
↓
healthcheck OK
↓
api démarre
Pour une tâche ponctuelle :
api:
depends_on:
migrate:
condition: service_completed_successfully
db healthy
↓
migrate
↓ exit 0
api
Variables d’environnement
environment:
PORT: 3000
DATABASE_URL: ${DATABASE_URL}
Valeur par défaut :
PORT: ${PORT:-3000}
Variable obligatoire :
SECRET_KEY: ${SECRET_KEY:?SECRET_KEY is required}
Dans Node :
process.env.PORT
Dans Python :
os.environ["PORT"]
Secrets
Éviter :
SECRET_KEY: mon-super-secret
dans un fichier versionné.
En développement :
.env
gitignored :
.env
.env.*
et .dockerignore :
.env
.env.*
Puis :
environment:
SECRET_KEY: ${SECRET_KEY}
Docker secrets
services:
api:
secrets:
- auth_secret
secrets:
auth_secret:
file: ./secrets/auth_secret
Disponible dans :
/run/secrets/auth_secret
Attention : un Docker Secret est généralement exposé comme fichier, pas automatiquement comme variable d’environnement.
Extensions / anchors Compose
Pour éviter de dupliquer :
x-api: &api
build: .
environment:
DATABASE_URL: ${DATABASE_URL}
services:
migrate:
<<: *api
command: python manage.py migrate
api:
<<: *api
command: python manage.py runserver
x-api
├── migrate
└── api
x-* : extension Compose.
&api : crée une anchor YAML.
*api : réutilise l’anchor.
<<: : fusionne la configuration.
Multi-stage Dockerfile
Un seul Dockerfile peut contenir plusieurs stages.
# ----- base -----
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# ----- dev -----
FROM base AS dev
COPY requirements-dev.txt .
RUN pip install \
--no-cache-dir \
-r requirements-dev.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# ----- builder -----
FROM base AS builder
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev
COPY requirements.txt .
RUN pip wheel \
--wheel-dir /wheels \
-r requirements.txt
# ----- production -----
FROM base AS production
COPY --from=builder /wheels /wheels
RUN pip install \
--no-cache-dir \
/wheels/*
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Relation :
base
├── dev
└── builder
↓
production
En développement :
build:
context: .
target: dev
Pour staging / production :
docker build \
--target production \
-t app:abc123 \
.
Important :
DEV
→ image target dev
STAGING
→ image target production
PRODUCTION
→ même image target production
Staging n’a normalement pas besoin de son propre stage Docker.
Registry
Un registry stocke les images Docker.
Exemples :
- Docker Hub ;
- GitHub Container Registry ;
- Forgejo Container Registry ;
- GitLab Registry.
Login :
docker login git.example.org
Tag :
docker tag app:1.0 \
git.example.org/theo/app:1.0
Push :
docker push \
git.example.org/theo/app:1.0
Pull :
docker pull \
git.example.org/theo/app:1.0
Git → Registry → Deploy
Pattern recommandé :
Git commit abc123
↓
tests
↓
docker build
↓
app:abc123
↓
Registry
↙ ↘
staging production
Principe :
Build once, deploy many.
Staging et production utilisent idéalement exactement la même image.
Ce qui change :
configuration
secrets
URL
base de données
ressources
Pas le code ni l’image.
Nettoyage
docker container prune
docker image prune
docker network prune
docker volume prune
Nettoyage global :
docker system prune
Plus agressif :
docker system prune -a
Les volumes ne sont pas supprimés par défaut.
Pour les inclure :
docker system prune --volumes
⚠️ À utiliser avec précaution.
Avec Compose :
docker compose down
supprime les conteneurs et réseaux du projet mais conserve les volumes nommés.
docker compose down -v
supprime aussi les volumes.
Pour PostgreSQL, cela peut signifier :
base de données supprimée
Rappels mentaux
- Image ≠ conteneur : l’image est le modèle, le conteneur son instance.
- Les conteneurs doivent être considérés comme remplaçables.
- Les données importantes vont dans des volumes ou services externes.
EXPOSEdocumente ;ports/-ppublient.- Entre conteneurs Compose, utiliser
db:5432, pas une IP. localhostdans un conteneur désigne ce conteneur.- Un conteneur devrait généralement avoir une responsabilité principale.
- Une même image peut servir à
api,migrate,worker, etc. - Un Dockerfile multi-stage peut produire une image
devet une imageproduction. - Staging et production devraient idéalement utiliser la même image.
- Dockeriser dépend surtout du runtime et de la plateforme de déploiement.