Pourquoi les conteneurs ont gagné
Avant les conteneurs, déployer signifiait reproduire un environnement : la bonne version du runtime, les bonnes bibliothèques système, la bonne configuration. Le fameux « ça marche sur ma machine » était un problème réel, et les serveurs divergeaient progressivement des environnements de développement.
Docker a résolu ce problème en encapsulant l’application et son environnement dans une image unique. L’image s’exécute de manière identique sur un ordinateur portable, en CI et en production, car elle contient tout ce dont elle a besoin. Cette cohérence est la raison pour laquelle les conteneurs sont devenus la norme pour le déploiement, et pourquoi apprendre Docker est l’une des compétences les plus précieuses pour livrer des logiciels.
Images et conteneurs
Une image est un modèle en lecture seule construit à partir d’un Dockerfile, composé de couches mises en cache. Un conteneur est une instance exécutable d’une image, possédant sa propre couche accessible en écriture, son propre processus et son propre réseau. On construit une fois pour exécuter plusieurs fois.
docker build -t my-app:1.0 .
docker run -p 3000:3000 my-app:1.0
docker ps
docker logs <container>
Le flag -p mappe un port de l’hôte vers un port du conteneur. Les conteneurs sont éphémères par conception : vous pouvez les arrêter, les supprimer et les recréer librement car l’image constitue la source de vérité.
Le Dockerfile
Le Dockerfile est la recette de build. Chaque instruction crée une couche (layer).
# Dockerfile
FROM node:22-alpine
WORKDIR /app
# install dependencies first for better caching
COPY package*.json ./
RUN npm ci --omit=dev
# then copy source
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]
L’ordre est primordial. Copier les manifestes de dépendances et les installer avant de copier le code source permet de s’assurer qu’une modification du code n’invalide que la couche source (peu coûteuse) et non l’étape d’installation (coûteuse). npm ci garantit une installation reproductible, et USER node permet l’exécution avec un utilisateur non-root.
Mise en cache des couches
Docker met en cache chaque couche et la réutilise lorsque les entrées restent inchangées. C’est pourquoi l’ordre ci-dessus est délibéré : placez les éléments qui changent rarement en haut et ceux qui changent souvent en bas.
# cached unless package files change
COPY package*.json ./
RUN npm ci
# invalidated on every source change
COPY . .
Un fichier .dockerignore est également important. Sans lui, node_modules, .git et les fichiers de build sont copiés dans le contexte de build, ce qui ralentit la compilation et peut entraîner la fuite de fichiers dans l’image.
node_modules
.git
dist
.env
Builds multi-étapes (Multi-stage builds)
Un build multi-étape permet de compiler dans une image et de n’expédier que le résultat final dans une autre.
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
L’image finale ne contient que les fichiers statiques compilés et un serveur web, et non le runtime Node, le code source ou les outils de build. Le résultat est plus léger, plus rapide à télécharger et présente une surface d’attaque beaucoup plus réduite. Pour une application serveur, l’étape finale serait une image Node légère contenant uniquement les dépendances de production.
Compose pour les applications multi-services
Les applications réelles nécessitent généralement plus d’un processus. Compose permet de les définir ensemble.
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- data:/var/lib/postgresql/data
volumes:
data:
depends_on contrôle l’ordre de démarrage, volumes permet de persister les données lors du redémarrage des conteneurs, et les services communiquent entre eux via leur nom sur le réseau partagé. Compose est idéal pour le développement local et les déploiements simples sur un seul hôte.
Registres et CI/CD
Les images transitent par un registre tel que Docker Hub, GitHub Container Registry ou un registre privé. Un pipeline typique se compose ainsi :
- Installation des dépendances et exécution des tests.
- Build de l’image et marquage (tag) avec le SHA du commit.
- Push de l’image vers le registre.
- Déploiement du nouveau tag vers l’environnement cible.
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA
Comme l’image est immuable et taguée par commit, les rollbacks consistent simplement à déployer un tag précédent. C’est le cœur d’un processus de déploiement fiable : build unique, promotion du même artefact à travers les environnements, et aucun rebuild pour la production.
Pratiques de production
- Health checks. Exposez un endpoint que la plateforme peut interroger pour savoir si le container est prêt.
- Utilisateur non-root. Exécutez l’application avec un utilisateur disposant des privilèges minimums nécessaires.
- Système de fichiers en lecture seule dans la mesure du possible, avec des volumes d’écriture explicites.
- Graceful shutdown. Gérez
SIGTERMpour terminer le traitement des requêtes en cours. - Limites de ressources. Définissez le CPU et la mémoire pour éviter qu’un container ne monopolise les ressources des autres.
- Épinglez les versions des images de base pour garantir des builds reproductibles.
- Secrets au runtime, ne les intégrez jamais directement dans l’image.
- Images de base légères comme Alpine ou distroless pour réduire les vulnérabilités.
Bonnes pratiques
- Ordonnez les instructions du Dockerfile de la moins fréquente à la plus fréquente en termes de modification.
- Utilisez des builds multi-étapes (multi-stage builds) et n’expédiez que le strict nécessaire.
- Ajoutez un
.dockerignorepournode_modules,.git,distet.env. - Exécutez le processus en tant qu’utilisateur non-root.
- Marquez (taggez) les images par le SHA du commit, et pas seulement par
latest. - Construisez l’image une seule fois et faites-la progresser à travers les différents environnements.
- Gérez
SIGTERMpour permettre un arrêt progressif (graceful shutdown).
Erreurs courantes
- Copier
node_modulesdans l’image. - Exécuter en tant que root.
- Intégrer des secrets dans l’image ou commiter
.env. - Utiliser
latestcomme seul tag et perdre la reproductibilité. - Reconstruire l’image différemment pour chaque environnement.
- Ignorer
.dockerignoreet créer des builds énormes et lents.
Et après ?
Les containers rendent le déploiement prévisible, et les compétences acquises s’appliquent aussi bien pour un déploiement sur une VM, Kubernetes ou une plateforme de containers serverless. Consolidez vos bases sur le runtime avec le guide Node.js, gérez vos dépendances avec npm, exposez votre application via HTTP et sécurisez-la grâce à la Web Security. Ensuite, containerisez une petite application et déployez-la de bout en bout.