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.
  • Sécurisée : Réduction de la surface d'attaque et gestion des droits.
  • 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) ?

  1. Modifiez app.js pour que le APP_SECRET ne soit plus retourné dans la réponse JSON, mais utilisé uniquement en interne (log de démarrage par exemple).

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

  2. 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).
# Exemple de structure sécurisée
FROM node:20-alpine

# Création du répertoire et gestion des droits
WORKDIR /app
RUN chown node:node /app

# Switch vers l'utilisateur non-privilégié
USER node

# Copie des fichiers de dépendances uniquement pour le cache
COPY --chown=node:node package*.json ./
RUN npm install --only=production

# Copie du reste du code
COPY --chown=node:node . .

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

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.

  3. 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 :

  1. Lancez votre stack et observez docker stats pendant 30 secondes.
  2. Comparez docker image ls avant et après avoir ajouté .dockerignore et le multi-stage build.
  3. Lancez docker system df et identifiez ce qui consomme le plus d'espace (images, volumes, conteneurs stoppés).
  4. 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 l'ordre des instructions COPY et RUN impacte le temps de build. - Comment une image plus légère améliore la sécurité. - Comment orchestrer proprement les secrets entre Docker et votre application.