TP09 : Docker propre, sécurisé et optimisé
Objectif du TP
Transformer une application Node.js "mal" dockerisée en une image prête pour la production :
- Configurable : Externalisation de la configuration et gestion des secrets.
- Sécurisée : Réduction de la surface d'attaque, gestion des droits et Docker Secrets.
- Optimisée : Réduction de la taille de l'image et gestion des ressources.
Rendu attendu
- Les notes dans votre carnet répondant aux questions posées avec le docker-compose final.
1. Pour débuter
A partir du code suivant qui est celui d'une petite API Express. Créez un dossier tp9 et placez-y les fichiers suivants :
app.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const secret = process.env.APP_SECRET || "mon_super_secret";
app.get('/', (req, res) => {
res.json({
message: "Hello World",
secret: secret
});
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
package.json
{
"name": "tp9-optimized-node",
"version": "1.0.0",
"main": "app.js",
"dependencies": {
"express": "^4.18.2"
}
}
Dockerfile
docker-compose.yaml
En observant ces fichiers, listez au moins 3 problèmes majeurs (sécurité, taille, configuration) qui empêchent cette application d'être mise en production "proprement".
2. Configuration & Environnement
Objectif
Ne plus jamais coder de secrets ou de configurations (port) en dur.
- Supprimer les valeurs sensibles codées en dur dans
app.js. - Créer un fichier
.envà la racine : - Modifier le
docker-compose.yamlpour injecter ces variables :
Pourquoi ne doit-on jamais versionner le fichier
.envsur Git ? Quelle est la différence entre une variable passée au "Build-time" (ARG) et au "Runtime" (ENV) ?
Inspecter ses variables d'environnement
Une fois le conteneur lancé, vérifiez les variables présentes avec ces deux commandes :
# Sur l'image buildée (sans lancer de conteneur)
# Affiche uniquement les ENV "baked" dans l'image via le Dockerfile
docker inspect node-bad --format '{{range .Config.Env}}{{println .}}{{end}}'
# Sur un conteneur en cours d'exécution
# Affiche toutes les variables, y compris celles injectées par env_file
docker exec <container_id> env
Observez la différence:
docker inspect <image>montre lesENVbaked dans l'image via le Dockerfile. Les variables du.envn'y apparaissent jamais elles ne sont pas dans l'image.docker exec <id> envmontre les variables du conteneur en cours d'exécution : celles du Dockerfile et celles injectées au runtime mais seulement si le conteneur a été démarré viadocker compose upavecenv_file:configuré. Un simpledocker runn'applique pas lesenv_filedu compose.
Pourquoi les variables d'environnement ne suffisent pas pour les secrets ?
Problème : docker inspect sur un conteneur expose ses variables d'environnement en clair. Quiconque a accès au daemon Docker peut lire vos secrets.
| Méthode | Visible via | Cas d'usage |
|---|---|---|
| Valeur en dur dans le code | git log |
❌ Jamais |
ENV dans le Dockerfile |
docker inspect <image> |
⚠️ Valeurs par défaut non-sensibles |
env_file + docker-compose |
docker exec env, système de fichiers |
⚠️ Dev uniquement |
| Docker Secret (Swarm) | Chiffré dans le Raft store | ✅ Production |
| Vault / Secrets Manager | Chiffré (externe) | ✅✅ Entreprise |
Gérer les secrets avec Docker Secrets
Comme montré ci-dessus, les variables d'environnement ne suffisent pas pour les données vraiment sensibles. Docker Secrets chiffre les secrets dans le Raft store du Swarm et les monte sous forme de fichiers inaccessibles via docker inspect ou les logs.
Créer et gérer des secrets
Swarm doit être initialisé au préalable (
docker swarm init).
# Créer un secret depuis la console (le "-" signifie "lire depuis stdin")
echo "super_password_secret" | docker secret create db_password -
# Lister les secrets
docker secret ls
# Inspecter — la valeur n'est JAMAIS affichée, même en tant qu'admin
docker secret inspect db_password
# Supprimer
docker secret rm db_password
Les secrets sont chiffrés dans le Raft store du Swarm. Une fois créés, leur valeur est irrécupérable même pour un administrateur.
Utiliser dans une stack Swarm
# docker-compose.yml (mode Swarm)
services:
app:
image: registry.local/my-app:v1
secrets:
- db_password
secrets:
db_password:
external: true
Le secret est automatiquement monté en fichier à /run/secrets/db_password à l'intérieur du conteneur. Dans l'application Node.js :
const fs = require('fs');
// Lire le secret depuis le fichier monté par Swarm
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
// Le secret n'est jamais exposé dans les logs, les variables d'env ou docker inspect
Question : Testez avec un secret Swarm : pouvez-vous lire sa valeur via
docker inspectoudocker secret inspect? Comparez avec ce que vous avez observé avec les variables d'environnement classiques.
3. Sécurité
Objectif
Réduire la surface d'attaque et ne pas donner les droits "root" au conteneur.
-
Scanner l'image :
Observez le nombre de vulnérabilités (CVE).Affinez le scan :
# Filtrer uniquement les CVE critiques et hautes trivy image --severity HIGH,CRITICAL node-bad # Faire échouer le build si des CVE critiques sont trouvées (utile pour le CI plus tard) trivy image --exit-code 1 --severity CRITICAL node-bad # Scanner le code source avant même de builder l'image trivy fs .
Questions :
- Qu'est-ce qu'une CVE ? Cherchez un exemple récent.
- Quelle différence entre
trivy fs .ettrivy image?
Corriger le Dockerfile :
- Utiliser une image de base officielle et minimale (
node:20-alpineounode:20-slim). - Utilisateurs non-root : Les images Node officielles incluent un utilisateur
node. Utilisez-le) - Limiter les copies : Ne copiez que le nécessaire (et pas des .env ou .git).
Ajouter le dockerfile dans votre carnet de notes avec les modifications de sécurité appliquées. Pourquoi est-il dangereux de laisser un conteneur tourner en tant que
root? Quel est l'impact du passage denode:latestànode:alpinesur le nombre de vulnérabilités détectées par Trivy ?
4. Optimisation & FinOps
Objectif
Réduire la taille de l'image et limiter la consommation de ressources.
-
Utiliser un
.dockerignore: Empêchez l'envoi denode_modules,.env,.gitou des logs vers Docker lors du build. -
Multi-stage Build : Séparez l'étape d'installation des dépendances de l'étape de run pour ne garder que le strict nécessaire.
Dans cette mini API Express (sans compilation), le gain peut sembler limité. On garde quand même le multi-stage ici pour comprendre le principe. Son intérêt devient beaucoup plus visible avec des applications qui compilent/transpilent (TypeScript, React, NestJS) ou qui génèrent un dossier
dist.
- Limitation des ressources dans
docker-compose.yaml:
Comparez la taille de l'image avant (
node:latest) et après (node:alpine+.dockerignore). Quel est le gain en Mo ? Pourquoi la taille de l'image a-t-elle un impact direct sur le coût (FinOps) et la rapidité de déploiement (CI/CD) ?
4.4. FinOps : mesurer avant d'optimiser
On ne peut pas optimiser ce qu'on ne mesure pas. Ces commandes permettent de quantifier l'impact de vos choix.
# Taille de toutes les images locales (triées)
docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# Espace total occupé par Docker sur le disque
docker system df
# Consommation CPU/RAM en temps réel de tous les conteneurs
docker stats
# Processus dans chaque service de la stack
docker compose top
Exercice :
- Lancez votre stack et observez
docker statspendant 10 secondes. - Lancez
docker system dfet identifiez ce qui consomme le plus d'espace (images, volumes, conteneurs stoppés). - Utilisez
docker system prunepour nettoyer ce qui est inutilisé, puis relancezdocker system df.
| Commande | Ce qu'elle mesure | Utilité FinOps |
|---|---|---|
docker image ls |
Taille des images | Coût de stockage registry |
docker system df |
Espace total Docker | Audit disque |
docker stats |
CPU/RAM en temps réel | Right-sizing des limites |
docker compose top |
Processus par service | Détecter les fuites de processus |
Question : Si vous limitez un service à
memory: 256Met qu'il consomme régulièrement 240M, que faut-il faire ? Et s'il consomme seulement 50M ?
5. Conclusion
À la fin de ce TP, vous devez être capable d'expliquer :
- Pourquoi externaliser la configuration et ne jamais coder de secrets en dur.
- Comment Docker Secrets protège les données sensibles dans un cluster Swarm.
- Les bonnes pratiques pour sécuriser une image Docker (utilisateur non-root, image de base minimale).
- Comment optimiser une image pour la production (multi-stage, .dockerignore).
- L'importance de mesurer la consommation de ressources avant d'optimiser (FinOps).