Suite à mon post linkedin sur l'optimisation Docker, une question a attiré toute mon attention:

"Et les images Distroless de Google, t'as essayé ?"

Oui. Et après les avoir utilisées sur plusieurs projets, voici ce que j'ai appris avec les cas où chaque approche s'impose vraiment.

Le contexte : pourquoi l'image de base est critique

Quand on parle de sécurité des conteneurs, on pense souvent aux vulnérabilités applicatives. Rarement à l'image de base elle-même.

Pourtant, c'est là que tout commence. Une image node:24 standard embarque Debian avec ses centaines de binaires, son gestionnaire de paquets, ses librairies système. La majorité de tout ça ne sert à rien pour faire tourner votre application Node.js — mais chaque outil présent est un vecteur d'attaque potentiel.

L'idée derrière Alpine et Distroless est la même : réduire la surface d'attaque en supprimant tout ce qui est superflu. La différence, c'est jusqu'où elles poussent cette logique.

Alpine : la légèreté pratique

Alpine Linux est une distribution minimaliste basée sur musl libc et busybox. C'est aujourd'hui l'image de base la plus utilisée en production pour les conteneurs Node.js.

Ce qu'elle apporte

FROM node:24-alpine
  • ~180 MB contre ~1.1 GB pour node:24 Debian
  • Un shell sh disponible
  • Un gestionnaire de paquets (apk) pour installer des dépendances système si nécessaire
  • Compatible avec la quasi-totalité des librairies npm

L'avantage décisif : le debugging

C'est là qu'Alpine brille vraiment. Quand quelque chose casse en production, vous pouvez ouvrir un terminal dans le conteneur :

docker exec -it mon-conteneur sh

Et de là, inspecter les fichiers, tester les connexions réseau, vérifier les variables d'environnement. Ce genre d'accès vous fait gagner des heures de débogage — surtout sur un projet solo à 23h.

La limite

Alpine contient encore un shell et un gestionnaire de paquets. Ce sont des outils légitimes pour l'administration, mais aussi des outils qu'un attaquant peut exploiter s'il parvient à s'introduire dans le conteneur. La surface d'attaque est réduite, mais non nulle.

Distroless : le minimalisme radical

Les images Distroless sont un projet open source de Google. L'idée est exprimée dans leur nom : sans distribution. Pas d'OS au sens traditionnel du terme — uniquement le runtime nécessaire à votre application.

Ce qu'elle apporte

FROM gcr.io/distroless/nodejs22-debian12
  • Plus léger qu'Alpine
  • Aucun shell — pas de sh, pas de bash
  • Aucun utilitaire — pas de ls, curl, wget, cat
  • Aucun gestionnaire de paquets
  • Basée sur Debian pour une meilleure compatibilité des librairies natives

L'avantage décisif : la sécurité

C'est le point central. Un attaquant qui parvient à s'introduire dans un conteneur Distroless se retrouve dans une impasse totale. Il ne peut exécuter aucune commande, télécharger aucun outil, pivoter vers aucun autre système.

docker exec -it mon-conteneur sh
# OCI runtime exec failed: exec: "sh": executable file not found

Cette contrainte est une fonctionnalité, pas un bug. Elle s'aligne sur le principe du moindre privilège poussé à l'extrême.

La limite

L'absence de shell est aussi son principal inconvénient opérationnel. Impossible de se connecter au conteneur pour investiguer un bug. Vous devez avoir un système de logs et d'observabilité solide avant de passer à Distroless — pas après.

Comparatif côte à côte

Critère Alpine Distroless Taille approximative ~180 MB ~110 MB Shell disponible sh Debug en direct docker exec Surface d'attaque Faible Minimale Compatibilité npm Excellente Bonne Courbe d'apprentissage Faible Moyenne Idéal pour Solo, startups, dev Équipes, données sensibles

Le Dockerfile complet avec les deux approches

Version Alpine (recommandée pour débuter)

# Stage 1 : build
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# Stage 2 : production Alpine
FROM node:24-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
USER appuser
EXPOSE 7860
ENV PORT=7860
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:7860/health || exit 1
CMD ["node", "server.js"]

Version Distroless (pour les projets avec enjeux de sécurité)

# Stage 1 : build (image complète pour avoir tous les outils)
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# Stage 2 : production Distroless
FROM gcr.io/distroless/nodejs22-debian12
COPY --from=builder /app /app
WORKDIR /app
USER nonroot
EXPOSE 7860
ENV PORT=7860
ENV NODE_ENV=production
CMD ["server.js"]

Deux points importants sur la version Distroless :

Premièrement, le CMD utilise ["server.js"] et non ["node", "server.js"]. L'image Distroless embarque Node.js comme entrypoint — vous passez directement le fichier à exécuter.

Deuxièmement, l'utilisateur nonroot est déjà défini dans l'image Distroless. Pas besoin de le créer manuellement comme avec Alpine.

Distroless sans sacrifier le debugging

Le principal frein à l'adoption de Distroless c'est l'absence de debugging en direct. Voici comment compenser.

Logs structurés dès le départ

Avec Alpine, on peut se permettre des console.log approximatifs et investiguer en direct si besoin. Avec Distroless, les logs sont votre seule fenêtre sur ce qui se passe :

// Remplacez vos console.log par des logs structurés
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: process.env.NODE_ENV !== 'production'
    ? { target: 'pino-pretty' }
    : undefined
});
// Utilisation
logger.info({ userId, action: 'login' }, 'User authenticated');
logger.error({ err, requestId }, 'Database connection failed');

Image de debug dédiée

Pour les investigations ponctuelles, maintenez une image Alpine parallèle avec les mêmes dépendances :

FROM node:24-alpine AS debug
WORKDIR /app
COPY --from=builder /app .
RUN apk add --no-cache curl
CMD ["sh"]
# En cas de besoin urgent
docker build --target debug -t mon-app:debug .
docker run -it --env-file .env mon-app:debug

Lequel choisir ?

Restez sur Alpine si :

  • Vous travaillez seul ou en petite équipe
  • Votre projet est en phase de démarrage ou de croissance rapide
  • Vous n'avez pas encore de système de logs structurés en place
  • La vélocité de développement prime sur la posture sécurité maximale

Passez à Distroless si :

  • Vous traitez des données sensibles (santé, finance, données personnelles)
  • Votre organisation a des exigences de conformité (SOC 2, ISO 27001, HDS)
  • Vous avez une équipe avec un système d'observabilité mature
  • Vous passez des audits de sécurité réguliers

La règle qui ne change pas dans les deux cas :

Peu importe votre choix, utilisez toujours le multi-stage build. On installe et on build dans une image complète. On ship dans une image épurée. C'est le fondement de toute image de production sérieuse.

Mon choix aujourd'hui

Alpine pour mes projets freelance en solo. La possibilité d'ouvrir un terminal dans le conteneur m'a sauvé plusieurs fois lors de bugs difficiles à reproduire localement.

Distroless dès qu'une équipe ou des données sensibles entrent dans l'équation. La contrainte opérationnelle est réelle, mais elle pousse à construire une infrastructure d'observabilité solide — ce qu'on aurait dû faire de toute façon.

Les deux sont infiniment meilleurs que FROM node:24. C'est là que commence vraiment le choix.

Vous utilisez Alpine, Distroless, ou autre chose en production ? Partagez votre expérience en commentaire.

Si cet article vous a été utile, un clap fait toujours plaisir 👏

Cet article fait partie d'une série sur l'optimisation d'infrastructure pour les projets Node.js en production. Lire le premier article : De 1.1 GB à 180 MB — comment j'ai refait mon Dockerfile from scratch