Skip to content

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

FROM node:latest

WORKDIR /app

COPY . .

RUN npm install

CMD ["node", "app.js"]

docker-compose.yaml

services:
  app:
    build: .
    ports:
      - "3000:3000"

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.

  1. Supprimer les valeurs sensibles codées en dur dans app.js.
  2. Créer un fichier .env à la racine :
    PORT=3000
    APP_SECRET=MaSuperCleSecrete
    
  3. Modifier le docker-compose.yaml pour injecter ces variables :
    services:
      app:
        build: .
        ports:
          - "${PORT}:${PORT}"
        env_file:
          - .env
    

Pourquoi ne doit-on jamais versionner le fichier .env sur 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 les ENV baked dans l'image via le Dockerfile. Les variables du .env n'y apparaissent jamais elles ne sont pas dans l'image.
  • docker exec <id> env montre 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é via docker compose up avec env_file: configuré. Un simple docker run n'applique pas les env_file du 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 inspect ou docker 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.

  1. Scanner l'image :

    docker build -t node-bad .
    trivy image node-bad
    
    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 . et trivy image ?

Corriger le Dockerfile :

  • Utiliser une image de base officielle et minimale (node:20-alpine ou node: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 de node:latest à node:alpine sur 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.

  1. Utiliser un .dockerignore : Empêchez l'envoi de node_modules, .env, .git ou des logs vers Docker lors du build.

    node_modules
    .env
    .git
    npm-debug.log
    

  2. 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.

  1. Limitation des ressources dans docker-compose.yaml :
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
    

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 stats pendant 10 secondes.
  • Lancez docker system df et identifiez ce qui consomme le plus d'espace (images, volumes, conteneurs stoppés).
  • Utilisez docker system prune pour nettoyer ce qui est inutilisé, puis relancez docker 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: 256M et 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).