Cours DevOps — fiche 2

Docker, du Dockerfile au conteneur qui tourne

Ce qu'est vraiment une image, comment on l'écrit, comment on la construit, comment les conteneurs se parlent, et où passent les variables d'environnement.

1

Le principe : isoler, pas virtualiser

Une machine virtuelle embarque un système d'exploitation entier. Un conteneur, lui, emprunte le noyau Linux de la machine hôte : il ne contient que l'application et ses dépendances. D'où le démarrage en une seconde.

Machine virtuelle

App A  ·  App B
OS invité complet (≈ 1 Go) ×2
Hyperviseur
Matériel

Lourd, lent à démarrer, isolation maximale.

Conteneurs Docker

App A + libs
App B + libs
Moteur Docker
Noyau Linux de l'hôte (partagé)
Matériel

Quelques dizaines de Mo, démarrage instantané.

2

Image, conteneur, registry

La confusion la plus fréquente en classe. L'image est le moule, en lecture seule ; le conteneur est le gâteau, jetable, qu'on peut refaire à l'identique.

Fichier texte

Dockerfile

La recette, versionnée avec le code.

build
Lecture seule

Image

Empilement de couches figées : app:v12.

run
Éphémère

Conteneur

L'image en train de tourner, avec sa couche d'écriture.

Une image → autant de conteneurs qu'on veut, en parallèle, isolés les uns des autres.
Le registry (Docker Hub, GHCR) est l'entrepôt d'images : docker push / docker pull.
3

Écrire un Dockerfile, ligne par ligne

# étape de build FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build   # image finale, légère FROM node:22-alpine WORKDIR /app COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules ENV NODE_ENV=production EXPOSE 3000 USER node CMD ["node", "dist/server.js"]
FROML'image de départ. Toujours une version précise (node:22-alpine), jamais latest.
WORKDIRLe dossier de travail à l'intérieur du conteneur.
COPYCopie depuis le contexte de build vers l'image. On copie package.json avant le code : le cache de npm ci survit aux modifications de code.
RUNExécuté pendant le build. Chaque RUN crée une couche.
ENVVariable inscrite dans l'image. Uniquement pour du non-sensible.
EXPOSEDocumentation du port. N'ouvre rien à lui seul — c'est -p qui publie.
CMDExécuté au démarrage du conteneur. Un seul processus au premier plan.
Multi-stage : le premier FROM compile, le second ne garde que le résultat. L'image finale n'embarque ni les outils de build ni le code source.
.dockerignore — à côté du Dockerfile : node_modules, .git, .env. Sinon le contexte de build pèse des centaines de Mo et les secrets partent dans l'image.
4

Le build et le cache de couches

Chaque instruction produit une couche. Docker réutilise les couches inchangées — mais dès qu'une couche change, toutes celles du dessous sont refaites. C'est tout l'enjeu de l'ordre des lignes.

# construire (le "." = contexte de build) docker build -t mon-app:v12 . # voir les images locales docker images # envoyer au registry docker tag mon-app:v12 ghcr.io/classe/app:v12 docker push ghcr.io/classe/app:v12
-t nomme l'image et lui donne un tag. Un tag = une version identifiable, donc un rollback possible.
Empilement des couches
CMD node dist/server.jsmétadonnée
RUN npm run buildrefaite à chaque commit
COPY . .change souvent
RUN npm cien cache
COPY package*.jsonen cache
FROM node:22-alpinebase
Se lit de bas en haut : la base d'abord, la commande de démarrage en dernier.
5

Lancer un conteneur : ports et volumes

docker run -d --name web -p 8080:3000 -v données:/app/data --env-file .env.production mon-app:v12
-d En arrière-plan

Le terminal est rendu. Sans -d, les logs défilent et Ctrl+C arrête le conteneur.

-p 8080:3000 Publier un port

hôte : conteneur. Toujours dans ce sens. Sans -p, le conteneur est injoignable de l'extérieur.

-v données:/app/data Volume

Le stockage qui survit à la suppression du conteneur. Sans volume, la base de données disparaît au redéploiement.

Règle à retenir : un conteneur est jetable. Tout ce qu'il écrit hors d'un volume est perdu à sa destruction — et c'est voulu.
6

Les réseaux : comment les conteneurs se parlent

Sur un réseau Docker personnalisé, chaque conteneur est joignable par son nom — Docker fournit le DNS interne. L'application appelle db:5432, pas une adresse IP.

Visiteur → port 443 de l'hôte -p 443:80
réseau app-netbridge, créé par nous
nginxseul conteneur publié
apijoignable en http://api:3000
dbdb:5432, aucun port publié
docker network create app-net docker run -d --name db --network app-net postgres:16 docker run -d --name api --network app-net mon-app:v12 docker network inspect app-net
Bonne pratique : on ne publie que le point d'entrée. La base de données reste invisible depuis Internet.
Le réseau bridge par défaut n'a pas de résolution par nom : il faut créer son réseau (ou laisser Compose le faire).
7

Configuration et secrets : --env-file

La même image doit tourner en local, en test et en production. Ce qui change n'est pas l'image : c'est l'environnement injecté au démarrage.

# .env.production (jamais commité) NODE_ENV=production PORT=3000 DATABASE_URL=postgres://app:s3cr3t@db:5432/app JWT_SECRET=xY9…
# tout le fichier d'un coup docker run --env-file .env.production mon-app:v12 # une seule variable, qui écrase le fichier docker run --env-file .env.production -e PORT=4000 mon-app:v12 # vérifier ce que le conteneur voit docker exec web env | sort
Format du fichier : une paire CLE=valeur par ligne, pas de guillemets, pas d'espaces autour du =, # pour commenter. Ce n'est pas un script shell — aucune expansion de variable.
Ordre de priorité (du plus faible au plus fort) : ENV du Dockerfile → --env-file-e.
Les trois règles de sécurité
1. .env* dans .gitignore et .dockerignore.
2. Jamais de secret dans un ENV du Dockerfile : il reste lisible dans les couches de l'image.
3. En CI, les valeurs viennent des secrets du dépôt, qui écrivent le fichier .env sur le serveur.
Avec Compose, le fichier .env du dossier est lu automatiquement pour substituer ${VAR} dans le YAML ; pour les variables du conteneur, on déclare env_file: dans le service.
8

Docker Compose : la même chose, écrite une fois

Compose remplace la longue ligne de docker run par un fichier versionné. Il crée le réseau, les volumes et les conteneurs, et gère l'ordre de démarrage.

# compose.yaml services: api: build: . image: ghcr.io/classe/app:v12 env_file: [.env.production] ports: ["8080:3000"] depends_on: [db] restart: unless-stopped db: image: postgres:16 env_file: [.env.db] volumes: ["pgdata:/var/lib/postgresql/data"] volumes: pgdata:
docker compose up -d --build
docker compose ps
docker compose logs -f api
docker compose down  # -v supprime les volumes
C'est ce fichier qu'on retrouve sur le VPS : le pipeline pousse l'image, le serveur fait pull puis up -d.

Aide-mémoire

CommandeCe qu'elle fait
docker build -t nom:tag .construit une image depuis le Dockerfile du dossier courant
docker imagesliste les images locales et leur poids
docker run -d -p 8080:3000 nom:taglance un conteneur en arrière-plan avec un port publié
docker ps -aconteneurs en cours, -a inclut les arrêtés
docker logs -f websuit la sortie du conteneur — le premier réflexe de débogage
docker exec -it web shouvre un shell dans le conteneur
docker stop web && docker rm webarrête puis supprime le conteneur
docker network create / inspectcrée un réseau, ou liste ce qui y est branché
docker volume lsliste les volumes — les données persistantes
docker system prune -alibère l'espace disque : images et couches inutilisées