Retour au cours

Carnet, support de cours

Docker

3 chapitres. Mis à jour le 24 septembre 2026.

Docker construit et exécute une application dans un environnement reproductible.

Sommaire

Présentation

  1. Dockerfile multi-stageExemple Python (FastAPI), Exemple Node : le Dockerfile en un seul stage, Stage base, Stage dev, Stage build, Stage production
  2. Docker, Git et les environnementsPush, Pull
  3. 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 image
  • Image → environnement exécutable immuable contenant l’application et ses dépendances
  • Container → instance en cours d’exécution d’une image
  • Docker Compose → orchestre plusieurs services et conteneurs
  • Volume → conserve ou partage des données en dehors du cycle de vie d’un conteneur
  • Registry → stocke et distribue les images Docker
  • Git → 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é :

TypeUsage
Bind mountPartager des fichiers avec la machine hôte
Volume DockerConserver 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 :

  1. PostgreSQL démarre.
  2. pg_isready répond OK : db passe à l’état service_healthy.
  3. La migration démarre et se termine avec le code 0 : elle passe à l’état service_completed_successfully.
  4. 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.

  1. Le service migrate effectue son travail puis s’arrête.
  2. Le service api reste 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_USER si elle existe, sinon monapp.

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 :

  1. Utiliser le nom du service comme hostname : db:5432, redis:6379, api:3000, plutôt que des IP.

  2. Ne déclarer des networks: explicites qu’en cas de besoin d’isolation. Le réseau default suffit souvent.

  3. Utiliser service_healthy pour une base de données plutôt que service_started si le service suivant doit immédiatement s’y connecter.

  4. Utiliser service_completed_successfully pour les migrations, setups ou tâches ponctuelles.

  5. Ajouter un healthcheck aux services dont d’autres dépendent.

  6. Ne pas commiter de vrais secrets dans compose.yaml.

  7. Utiliser un .env ignoré par Git pour le développement et un mécanisme de secrets adapté pour la production.

  8. Utiliser : ${VAR:-default} pour les valeurs ayant un fallback acceptable.

  9. Utiliser : ${VAR:?required} pour les variables obligatoires.

  10. Éviter container_name: sans raison particulière.

  11. Exposer avec ports: uniquement les services que l’hôte doit pouvoir atteindre.

  12. Utiliser des volumes nommés pour les données persistantes.

  13. Utiliser des bind mounts pour le code source pendant le développement.

  14. Garder les node_modules Linux dans le conteneur plutôt que de réutiliser ceux de macOS ou Windows.

  15. Maintenir un .dockerignore, surtout avec context: .

  16. Utiliser une image commune avec des commandes différentes pour des services comme api, migrate et worker lorsqu’ils partagent le même environnement.

  17. Préférer un Dockerfile multi-stage pour séparer développement, build et production.

  18. Construire l’image de production une seule fois, puis déployer cette même image en staging et en production.

  19. Identifier les images avec un numéro de version ou un SHA Git plutôt que de dépendre uniquement de latest.

  20. Lancer docker compose config pour 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
  • Docker, Git et les environnements
  • Docker Cheat-Sheet

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 :

  1. Le développeur fait git push.
  2. Forgejo Actions ou GitHub Actions lance les tests, le lint et le typecheck.
  3. La CI construit l’image avec docker build.
  4. La CI pousse l’image dans le Container Registry, par exemple app:abc123.
  5. Le staging déploie cette image.
  6. 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 image
  • Image → environnement exécutable immuable contenant l’application et ses dépendances
  • Container → instance en cours d’exécution d’une image
  • Docker Compose → orchestre plusieurs services et conteneurs
  • Volume → conserve ou partage des données en dehors du cycle de vie d’un conteneur
  • Registry → stocke et distribue les images Docker
  • Git → 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é

TypeUsage
bind mountpartager des fichiers avec l’hôte
volumepersister 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.
  • EXPOSE documente ; ports / -p publient.
  • Entre conteneurs Compose, utiliser db:5432, pas une IP.
  • localhost dans 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 dev et une image production.
  • Staging et production devraient idéalement utiliser la même image.
  • Dockeriser dépend surtout du runtime et de la plateforme de déploiement.