Docker et conteneurs
Conteneurs, images, bonnes pratiques de Dockerfile et builds multi-stage pour empaqueter et distribuer des applications.
Qu’est-ce que Docker ?
Docker est une plateforme qui permet d’empaqueter des applications avec l’ensemble de leurs dépendances dans des unités isolées appelées conteneurs. Un conteneur inclut le code, le runtime, les bibliothèques et la configuration nécessaires pour que l’application fonctionne de manière identique sur n’importe quelle machine.
Contrairement aux machines virtuelles, les conteneurs partagent le noyau du système d’exploitation hôte, ce qui les rend beaucoup plus légers et rapides à démarrer.
Concepts fondamentaux
Image
Une image est un paquet immuable qui contient tout le nécessaire pour exécuter une application. Elle se construit à partir d’un Dockerfile et se stocke dans un registry (comme Docker Hub, ECR ou GCR).
Les images se composent de couches (layers). Chaque instruction du Dockerfile crée une nouvelle couche. Docker met en cache les couches qui ne changent pas, ce qui accélère les builds ultérieurs.
Conteneur
Un conteneur est une instance en cours d’exécution d’une image. Il est éphémère par nature — lorsqu’il s’arrête, son état interne est perdu (à moins d’utiliser des volumes).
Registry
Un registry est un dépôt d’images. Les plus courants sont :
- Docker Hub : registry public par défaut
- Amazon ECR : registry privé sur AWS
- Google GCR / Artifact Registry : registry sur GCP
- GitHub Container Registry : intégré à GitHub
Dockerfile : bonnes pratiques
Le Dockerfile définit comment une image est construite. Un Dockerfile bien écrit produit des images petites, sécurisées et rapides à construire.
Exemple de base
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "dist/server.js"]
Bonnes pratiques
- Utiliser des images de base petites : privilégier les variantes
alpineouslimplutôt que les images complètes - Ordonner les instructions par fréquence de changement : les couches qui changent le moins viennent en premier pour tirer parti du cache
- Copier les dépendances avant le code :
COPY package*.jsonavantCOPY .pour mettre en cachenpm install - Ne pas exécuter en tant que root : créer un utilisateur non privilégié avec
USER - Utiliser
.dockerignore: exclurenode_modules,.git, les fichiers de test et la documentation - Minimiser le nombre de couches : combiner les commandes
RUNliées avec&& - Ne pas installer d’outils inutiles : chaque paquet supplémentaire augmente la surface d’attaque
Exemple avec un utilisateur non root
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
Builds multi-stage
Les builds multi-stage permettent d’utiliser plusieurs étapes dans un seul Dockerfile. C’est particulièrement utile pour séparer l’étape de compilation de l’étape d’exécution, ce qui produit des images finales beaucoup plus petites.
Pourquoi utiliser le multi-stage ?
- L’image finale n’inclut pas les outils de compilation (compilateur TypeScript, build tools, etc.)
- La taille de l’image est drastiquement réduite
- La surface d’attaque en production est minimisée
Exemple : application Node.js avec TypeScript
# Étape 1 : Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Étape 2 : Production
FROM node:20-alpine AS production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
Dans cet exemple, l’image finale ne contient que le code compilé et les dépendances de production — sans TypeScript, sans devDependencies, sans code source.
Docker Compose pour le développement
Docker Compose permet de définir et d’exécuter des applications multi-conteneurs. C’est idéal pour l’environnement de développement local :
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://postgres:password@db:5432/myapp
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: password
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:
Sécurité des conteneurs
- Scanner les images : utiliser des outils comme Trivy, Snyk ou Docker Scout pour détecter les vulnérabilités
- Mettre à jour les images de base : maintenir les images de base à jour avec les correctifs de sécurité
- Ne pas stocker de secrets dans les images : utiliser des variables d’environnement ou des gestionnaires de secrets au runtime
- Limiter les ressources : configurer des limites de CPU et de mémoire pour éviter qu’un conteneur ne consomme toutes les ressources de l’hôte
- Utiliser des images signées : vérifier la provenance des images avec Docker Content Trust
Résumé
Docker simplifie l’empaquetage et la distribution des applications en garantissant que le même artefact fonctionne dans n’importe quel environnement. Les points clés sont : des images petites grâce aux builds multi-stage, de bonnes pratiques de Dockerfile, la sécurité dès la conception et Docker Compose pour le développement local.