Construire des images Docker distroless : anatomie, outils et comparatif
Scratch, Google Distroless, Chainguard, Ubuntu Chiseled, Docker Hardened Images — comment fonctionnent les images minimales, comment les construire, et laquelle choisir.
Cet article fait suite à Optimisation Docker pour sites Astro.js — Recherche complète. L’article précédent effleurait le sujet des images distroless ; celui-ci plonge en profondeur dans leurs principes de construction, les outils disponibles, et les tradeoffs concrets.
Pourquoi distroless ?
Une image Docker classique basée sur ubuntu:24.04 ou debian:12 contient des centaines de paquets : shells, gestionnaire de paquets, utilitaires système (curl, wget, find…), compilateurs, fichiers de documentation. La plupart ne servent jamais en production. Selon Sysdig (State of Cloud-Native Security, 2024), 87% des images en production contiennent des vulnérabilités de sévérité haute ou critique — et la majorité provient de ces composants inutilisés.
Le principe distroless est simple : si votre application n’en a pas besoin, ça ne devrait pas être dans le container. On ne parle pas juste de réduire la taille — on parle de supprimer la surface d’attaque. Pas de shell signifie qu’un attaquant qui exploite une faille dans votre application ne peut pas obtenir de session interactive. Pas de curl signifie qu’il ne peut pas télécharger d’outils. Pas de gestionnaire de paquets signifie qu’il ne peut rien installer.
C’est exactement la manière dont Google exécute ses containers en interne depuis des années — et c’est désormais accessible à tous.
Le spectre des images minimales
Avant de plonger dans chaque approche, il est utile de visualiser le spectre complet :
Plus grand ◄─────────────────────────────────────────────────► Plus petit
ubuntu:24.04 alpine:3.21 distroless/base distroless/static scratch
~77 MB ~5 MB ~20 MB ~2 MB 0 B
100+ pkgs ~17 pkgs glibc + certs certs seulement rien
shell ✓ shell ✓ shell ✗ shell ✗ shell ✗
apt ✓ apk ✓ rien rien rien
Cinq approches coexistent aujourd’hui pour construire des images minimales. Chacune a sa philosophie, ses outils, et ses tradeoffs.
1. scratch — le vide absolu
scratch n’est pas une image — c’est un mot-clé réservé par Docker qui indique un filesystem vide. Zéro octet. Rien. L’implémentation est dans le PR #8827 de Moby : quand le builder rencontre FROM scratch, il ne tire aucune image et commence avec un filesystem vide.
Ce que le kernel fournit quand même
Même avec scratch, quand un container démarre, le runtime OCI (runc) monte automatiquement :
/proc,/sys,/dev(avec/dev/shm,/dev/pts)/etc/hostname,/etc/hosts,/etc/resolv.conf- Le fichier marqueur
.dockerenv
Ce sont des filesystems de namespace gérés par le kernel — ils ne font pas partie des layers de l’image.
Ce que vous devez fournir
Tout le reste. Si votre application a besoin de certificats TLS pour des appels HTTPS, c’est à vous de les copier. Si elle a besoin de données de timezone, idem. Si vous voulez un utilisateur non-root, il faut créer /etc/passwd manuellement.
Quand utiliser scratch
scratch est idéal pour les binaires statiquement liés :
# Go — binaire 100% statique
FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /myapp
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /myapp /myapp
ENTRYPOINT ["/myapp"]
# Rust — compilation statique via musl
FROM rust:1.83 AS builder
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
ENTRYPOINT ["/myapp"]
Quand ne pas utiliser scratch
Dès que votre binaire est dynamiquement lié (ce qui est le cas de Node.js, Python, Java, et de la plupart des applications C/C++), scratch ne fonctionnera pas — il manque glibc, libssl, et les autres bibliothèques partagées. C’est là qu’interviennent les vraies images distroless.
Pour un serveur HTTP statique sur scratch (BusyBox httpd, darkhttpd), voir la section “Dockerfiles de référence” de l’article précédent.
2. Google Distroless — Debian dégraissée par Bazel
Origine et philosophie
Le projet GoogleContainerTools/distroless est la première implémentation grand public du concept distroless. Les images sont servies depuis gcr.io/distroless/* (migré vers Google Artifact Registry en backend, mais le domaine gcr.io reste fonctionnel).
La philosophie : prendre des paquets Debian et n’en extraire que le strict nécessaire pour faire tourner un runtime donné. Le résultat ne contient ni shell, ni gestionnaire de paquets, ni utilitaires système.
Comment elles sont construites
C’est ici que ça devient intéressant. Google n’utilise ni Docker, ni apt-get pour construire ces images. Le build repose entièrement sur Bazel, le système de build de Google, avec deux rulesets spécialisés :
-
rules_distroless — des règles Bazel qui remplacent les commandes système : au lieu de
apt-get install,useradd,update-ca-certificates, tout est exprimé comme des règles Bazel déclaratives qui extraient des fichiers depuis des paquets.debsans jamais les “installer”. -
rules_oci — le successor de
rules_docker, qui construit des images OCI sans démon Docker. Les layers sont assemblées directement à partir des fichiers extraits et poussées vers le registre via Crane.
Le processus concret :
Snapshots Debian (snapshot.debian.org)
↓ SHA256 checksums + timestamps
Bazel rules_distroless
↓ extraction sélective des .deb
rules_oci
↓ assemblage en layers OCI
gcr.io/distroless/*
Les mises à jour de sécurité sont gérées par des GitHub Actions qui génèrent automatiquement des PRs quand des paquets Debian sont mis à jour.
Les variantes disponibles
Toutes les images sont basées sur Debian 12 (Bookworm) ou Debian 13, avec des suffixes explicites.
Images de base :
| Image | Contenu | Taille | Usage |
|---|---|---|---|
static-debian12 | Certificats TLS, tzdata, /etc/passwd. Pas de libc. | ~2 MB | Binaires Go/Rust statiques |
base-debian12 | static + glibc + libssl | ~20 MB | Binaires dynamiques (C, Rust/glibc) |
cc-debian12 | base + libgcc + libstdc++ | ~25 MB | Applications C/C++ |
Images avec runtime :
| Runtime | Image | Taille approx. |
|---|---|---|
| Node.js 22 | gcr.io/distroless/nodejs22-debian12 | ~141 MB |
| Python 3 | gcr.io/distroless/python3-debian12 | ~52 MB |
| Java 21 | gcr.io/distroless/java21-debian12 | ~220 MB |
Tags disponibles pour chaque image :
| Tag | Description |
|---|---|
:latest | Version courante |
:nonroot | Exécution sous UID 65534 (non-root) |
:debug | Inclut un shell BusyBox pour le troubleshooting |
:debug-nonroot | Debug + non-root |
Exemple pratique — Node.js sur distroless
FROM node:22 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22 AS prod-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM gcr.io/distroless/nodejs22-debian12:nonroot
WORKDIR /app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
EXPOSE 4321
CMD ["dist/server/entry.mjs"]
Point critique : le CMD (et ENTRYPOINT) doivent utiliser la forme JSON (["..."]), pas la forme shell ("..."). La forme shell nécessite /bin/sh, qui n’existe pas. Cette image distroless Node.js utilise le binaire Node.js comme entrypoint — le chemin passé en CMD est le fichier à exécuter, pas une commande shell.
Adopteurs notables
Le projet distroless est utilisé par Kubernetes (depuis v1.15), Knative, Tekton, Teleport, et BloodHound entre autres.
Limites
- Difficile à étendre : si vous avez besoin d’un paquet qui n’est pas dans les images pré-construites, il faut maîtriser Bazel — la courbe d’apprentissage est raide.
- Pas de SLA formel sur les CVEs : les mises à jour dépendent du rythme des PRs automatisées.
- Base Debian uniquement : si votre stack de développement est sur Ubuntu, les versions de paquets peuvent diverger légèrement.
3. Chainguard — Wolfi, apko et le zéro CVE
L’entreprise et sa philosophie
Chainguard a été fondée par les ingénieurs Google qui ont créé Sigstore, le standard open source de signature cryptographique. L’entreprise a levé $892M au total (dont une Series D de $356M début 2025) et atteint $40M d’ARR en 2025.
Leur approche diffère fondamentalement de Google Distroless : au lieu de reconstruire à partir de paquets Debian, Chainguard a créé son propre OS et ses propres outils de build.
Wolfi — la “undistro” Linux
Wolfi est un OS Linux conçu exclusivement pour les containers. Ce n’est pas une distribution complète — il n’a pas de kernel (il repose sur celui de l’hôte via le runtime de container).
Le choix technique le plus important de Wolfi : utiliser glibc (pas musl comme Alpine). C’est ce qui le distingue fondamentalement d’Alpine et résout le problème de compatibilité musl/glibc qui affecte Alpine :
| Aspect | Alpine (musl) | Wolfi (glibc) |
|---|---|---|
| Wheels Python pré-compilées | Souvent recompilées depuis les sources | Fonctionnent directement |
| Modules natifs Node.js | Pas officiellement supportés | Support complet |
| Résolution DNS | Comportements inhabituels connus | Standard |
| Allocation mémoire sous charge | Jusqu’à 2x plus lent | Performance standard |
| Format de paquets | apk | apk (mais non interchangeable avec Alpine !) |
Attention : malgré le format .apk commun, les paquets Alpine et Wolfi ne sont pas interchangeables. Ne jamais copier des binaires Alpine dans un container Wolfi (ni l’inverse).
apko — construction déclarative d’images
apko est l’outil qui transforme un fichier YAML en image OCI. Pas de Dockerfile, pas de commandes RUN — tout est déclaratif :
# apko.yaml — exemple d'image Node.js minimale
contents:
repositories:
- https://packages.wolfi.dev/os
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
packages:
- wolfi-baselayout
- nodejs-22
- ca-certificates-bundle
accounts:
groups:
- groupname: nonroot
gid: 65532
users:
- username: nonroot
uid: 65532
run-as: 65532
entrypoint:
command: /usr/bin/node
archs:
- amd64
- arm64
environment:
NODE_ENV: production
apko build apko.yaml my-node:latest output.tar
# OU pousser directement vers un registre :
apko publish apko.yaml registry.example.com/my-node:latest
L’absence de RUN n’est pas un oubli — c’est un choix de design. Comme l’explique la documentation apko, cette contrainte garantit que les images sont reproductibles bit-à-bit : le même YAML produit toujours la même image binaire. Et chaque build génère automatiquement un SBOM (Software Bill of Materials).
melange — construction de paquets APK
Si vous avez besoin d’un paquet qui n’existe pas dans le dépôt Wolfi, melange permet d’en construire un via un pipeline YAML :
package:
name: my-app
version: 1.0.0
environment:
contents:
packages:
- wolfi-baselayout
- build-base
- nodejs-22
- npm
pipeline:
- runs: |
npm ci --omit=dev
mkdir -p ${{targets.destdir}}/app
cp -r node_modules dist package.json ${{targets.destdir}}/app/
melange produit des .apk que apko peut directement consommer — c’est le combo melange + apko qui remplace le traditionnel Dockerfile multi-stage pour les utilisateurs Chainguard.
Le catalogue d’images
Chainguard propose plus de 2 000 images couvrant la plupart des stacks : Node.js, Python, Go, Java, nginx, PostgreSQL, Redis, etc.
Tarification :
| Tier | Images | Tags | SLA CVE | Coût |
|---|---|---|---|---|
| Free | ~50 images curatées | :latest et :latest-dev uniquement | Aucun | Gratuit |
| Production | 2 000+ images | Versions spécifiques + timestamps | 7j critique, 14j haut/moyen/bas | Payant (par catalogue ou par image) |
Différence clé avec Google Distroless : Chainguard reconstruit chaque image chaque nuit automatiquement via leur plateforme DriftlessAF, ce qui intègre les patchs de sécurité en quelques heures. Google Distroless fonctionne par PRs plus espacées.
Exemple pratique — Node.js sur Chainguard
# Build stage — inclut npm, shell, outils de build
FROM cgr.dev/chainguard/node:latest-dev AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production — distroless, pas de shell
FROM cgr.dev/chainguard/node:latest
WORKDIR /app
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --from=builder --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/package.json ./
ENV NODE_ENV=production
CMD ["dist/index.js"]
Le tag :latest-dev inclut un shell et npm pour le build stage. Le tag :latest est l’image distroless de production — uniquement le binaire Node.js et ses dépendances système.
4. Ubuntu Chiseled — le ciseau de Canonical
Le concept
Ubuntu Chiseled est la réponse de Canonical au mouvement distroless. L’idée : plutôt que de reconstruire un OS from scratch (Wolfi) ou d’extraire des paquets Debian via Bazel (Google), Canonical utilise un outil appelé chisel pour découper chirurgicalement les paquets Ubuntu en ne gardant que les fichiers strictement nécessaires.
La métaphore officielle est celle de Michel-Ange : la sculpture existe déjà dans le bloc de marbre — chisel retire le superflu.
Comment chisel fonctionne
chisel est un outil CLI en Go qui décompose les paquets .deb d’Ubuntu en composants plus fins appelés slices (tranches). Contrairement à apt qui installe des paquets entiers, chisel n’installe que les fichiers listés dans la définition d’une slice.
Installation :
go install github.com/canonical/chisel/cmd/chisel@latest
Usage basique :
chisel cut --release ubuntu-24.04 --root /rootfs/ \
base-files_base \
ca-certificates_data \
libc6_libs \
libssl3t64_libs
Cette commande :
- Télécharge les paquets
.debdepuis les archives Ubuntu - Extrait uniquement les fichiers définis dans chaque slice
- Résout les dépendances inter-slices
- Place le tout dans
/rootfs/
Les slices en détail
Le concept central de chisel est la slice — un sous-ensemble minimal et fonctionnel d’un paquet Debian. Les définitions sont stockées dans le dépôt canonical/chisel-releases, organisé par branches Git (une par version d’Ubuntu : ubuntu-22.04, ubuntu-24.04, etc.).
Prenons un exemple concret. Le paquet libc6 contient des dizaines de fichiers : bibliothèques partagées, configurations, documentation, fichiers de localisation. La définition de slices pour libc6 le découpe en :
libc6_libs— les bibliothèques partagées (.so) uniquementlibc6_config— les fichiers de configurationlibc6_copyright— les informations de licence
Quand une application a besoin de glibc, on n’installe que libc6_libs — pas la configuration, pas la documentation, pas les locales.
Format d’une définition de slice :
# slices/vim-tiny.yaml dans chisel-releases
package: vim-tiny
essential:
- vim-tiny_copyright
slices:
bins:
essential:
- libacl1_libs
- libc6_libs
- libtinfo6_libs
- vim-common_config
contents:
/usr/bin/vim.tiny:
config:
contents:
/etc/vim/vimrc.tiny:
copyright:
contents:
/usr/share/doc/vim-tiny/copyright:
Chaque slice déclare ses dépendances (champ essential) sur d’autres slices — pas sur des paquets entiers, mais sur des slices spécifiques d’autres paquets. C’est ce qui permet la granularité fine.
La convention de nommage est <paquet>_<slice> avec un underscore, ce qui fonctionne car les noms de paquets Debian ne peuvent pas contenir d’underscores.
Construire une image chiseled custom
La méthode recommandée utilise un Dockerfile multi-stage :
ARG UBUNTU_RELEASE=24.04
# Stage 1 : construire le rootfs chiseled
FROM ubuntu:${UBUNTU_RELEASE} AS builder
RUN apt-get update && apt-get install -y ca-certificates
# Installer chisel
ADD https://github.com/canonical/chisel/releases/download/v1.1.0/chisel_v1.1.0_linux_amd64.tar.gz /tmp/
RUN tar -xzf /tmp/chisel_*.tar.gz -C /usr/bin/
# Découper les slices nécessaires
RUN mkdir -p /rootfs
RUN chisel cut --release "ubuntu-${UBUNTU_RELEASE}" --root /rootfs \
base-files_base \
base-files_release-info \
ca-certificates_data \
libc6_libs \
libssl3t64_libs \
libstdc++6_libs
# Préparer un utilisateur non-root
RUN echo "appuser:x:1000:1000::/home/appuser:/sbin/nologin" >> /rootfs/etc/passwd && \
echo "appuser:x:1000:" >> /rootfs/etc/group && \
mkdir -p /rootfs/home/appuser
# Stage 2 : image finale from scratch
FROM scratch
COPY --from=builder /rootfs /
USER appuser
WORKDIR /home/appuser
On part de scratch, mais avec un rootfs pré-rempli par chisel contenant uniquement les slices nécessaires. Le résultat : une image Ubuntu-compatible (glibc, certificats TLS) mais sans shell, sans apt, sans rien de superflu.
Le partenariat Microsoft / .NET
Le cas d’usage le plus mature de chisel est le partenariat entre Microsoft et Canonical pour les images .NET. Les résultats sont parlants :
| Métrique | Ubuntu standard | Ubuntu Chiseled |
|---|---|---|
| Taille (aspnet 8.0) | 216 MB | 110 MB |
| Composants Linux | 92 paquets | 7 paquets |
| Temps de démarrage | Baseline | ~20% plus rapide |
| Utilisation mémoire | Baseline | ~20% plus faible |
| Temps de réponse | Baseline | ~40% plus rapide |
Richard Lander (Microsoft) a qualifié les images chiseled de “base image recommandée pour les développeurs .NET”.
Images pré-construites disponibles
| Runtime | Image Docker Hub |
|---|---|
| .NET Runtime | mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled |
| ASP.NET Core | mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled |
| Java JRE | ubuntu/jre (JRE 8, 17, 21) |
| Node.js | canonical/chiseled-nodejs |
Limites
- Couverture de slices limitée : vous ne pouvez installer que les paquets pour lesquels une définition de slice existe dans
chisel-releases. Pour un paquet non couvert, il faut écrire sa propre définition YAML et la contribuer au dépôt. - Pas de version pinning : chisel utilise la version du paquet présente dans les archives Ubuntu au moment du build. Pas de possibilité de figer une version spécifique de
libc6. - Compatibilité des scanners : certains scanners de vulnérabilités (Trivy, Grype) ne reconnaissent pas les images chiseled car le fichier
/var/lib/dpkg/statusest absent. Il faut générer un fichier de statut dpkg viachisel-wrapperpour que les scanners fonctionnent. - Dernière release : v1.4.0 (13 février 2026).
5. Docker Hardened Images — le nouveau venu
Le contexte
En mai 2025, Docker a lancé les Hardened Images (DHI) comme produit commercial. En décembre 2025, revirement stratégique : Docker a rendu les 1 000+ images DHI gratuites et open source sous licence Apache 2.0, sans restrictions d’usage ni de redistribution.
C’est une réponse directe à la montée de Chainguard — et un signal fort de validation du marché des images zéro CVE.
Ce qu’elles offrent
Les Docker Hardened Images sont des images distroless construites à partir de Debian et Alpine, avec :
- Jusqu’à 95% de vulnérabilités en moins par rapport aux images communautaires
- Exécution non-root par défaut
- SBOMs inclus avec provenance SLSA Build Level 3
- Signatures cryptographiques
- Un tier Enterprise (payant) avec engagement de remédiation CVE critique sous 7 jours
- Un Extended Lifecycle Support (payant) ajoutant 5 ans de couverture sécurité au-delà de la fin de vie upstream
Accès
Les images sont disponibles sur Docker Hub avec le préfixe docker/ :
docker pull docker/nginx-dhx:latest
La documentation officielle DHI détaille les concepts et l’utilisation.
6. Outils spécialisés
Au-delà des approches “image de base”, plusieurs outils permettent de construire ou optimiser des images minimales.
ko — images Go sans Docker
ko est un outil CNCF Sandbox qui construit des images pour les applications Go sans démon Docker. Il exécute go build localement, package le binaire dans une image OCI, et la pousse vers un registre.
export KO_DOCKER_REPO=registry.example.com/my-project
ko build ./cmd/myapp
Par défaut, ko utilise cgr.dev/chainguard/static comme image de base (changement récent — c’était auparavant gcr.io/distroless/static:nonroot). Il génère aussi un SBOM automatiquement.
Configuration .ko.yaml :
defaultBaseImage: cgr.dev/chainguard/static:latest
baseImageOverrides:
github.com/my-org/my-app/cmd/web: cgr.dev/chainguard/glibc-dynamic:latest
ko est utilisé par Knative, Tekton, Karpenter, Kyverno, et Sigstore.
SlimToolkit — optimiser des images existantes
SlimToolkit (anciennement DockerSlim, désormais projet CNCF Sandbox) prend une approche radicalement différente : au lieu de construire une image minimale from scratch, il analyse une image existante au runtime et supprime tout ce qui n’est pas utilisé.
Résultats annoncés :
- Node.js : 432 MB → 14 MB (30x)
- Python : 916 MB → 27.5 MB (33x)
- Go : 700 MB → 1.56 MB (449x)
slim build --target my-fat-image:latest
Le tradeoff : SlimToolkit nécessite une phase de profiling qui exécute l’application — si tous les chemins de code ne sont pas exercés pendant l’analyse, les fichiers nécessaires mais non utilisés pendant le profiling seront supprimés. C’est un outil puissant mais qui demande de la rigueur dans la phase d’analyse.
7. Sécurité : signatures, SBOM et supply chain
Les images distroless ne se limitent pas à réduire la taille — elles s’inscrivent dans un mouvement plus large de sécurisation de la supply chain logicielle.
Cosign et Sigstore
Cosign (partie du projet Sigstore) permet de signer et vérifier cryptographiquement des images de containers. Toutes les images Google Distroless et Chainguard sont signées.
Vérifier une image Google Distroless :
cosign verify gcr.io/distroless/nodejs22-debian12 \
--certificate-oidc-issuer https://accounts.google.com \
--certificate-identity keyless@distroless.iam.gserviceaccount.com
Le modèle de signature “keyless” fonctionne sans clé privée persistante :
- Authentification via OIDC (Google, GitHub, Microsoft)
- Demande d’une paire de clés éphémère
- Signature de l’artefact
- Enregistrement dans un log de transparence (Rekor)
- Destruction de la clé privée
Plus de clés à gérer, à sécuriser, à faire tourner.
SBOM (Software Bill of Materials)
Un SBOM est l’inventaire complet des composants d’une image. La distinction importante : les SBOMs peuvent être générés au moment du build (ce que font apko et Chainguard) ou après coup par scanning (ce que font Trivy, Syft). Les SBOMs au build sont plus précis car ils savent exactement ce qui a été installé, tandis que les scanners peuvent manquer des composants ou en identifier de faux.
Récupérer le SBOM d’une image Chainguard :
cosign download attestation cgr.dev/chainguard/node:latest \
| jq -r '.payload' | base64 -d | jq .
Le contre-argument
Red Hat a publié un article argumentant que les images distroless ne sont pas la solution de sécurité qu’on imagine. Leur point : l’application elle-même reste la surface d’attaque principale. Supprimer le shell est un obstacle mineur pour un attaquant déterminé — il peut toujours exploiter l’application directement, exfiltrer des données via le réseau, ou utiliser des techniques de living-off-the-land.
C’est un argument valide — le distroless n’est pas une silver bullet. C’est une couche de défense en profondeur qui rend l’exploitation plus difficile, pas impossible.
8. Déboguer un container sans shell
La question inévitable : “comment je debug si je ne peux même pas faire un docker exec ?“
1. Images debug / -dev
Toutes les approches distroless proposent des variantes avec shell :
# Google Distroless — tag :debug (BusyBox)
docker run -it gcr.io/distroless/nodejs22-debian12:debug sh
# Chainguard — tag :latest-dev (shell + apk)
docker run -it cgr.dev/chainguard/node:latest-dev sh
En production, utilisez l’image distroless. En développement/debug, basculez temporairement sur la variante debug.
2. Containers éphémères Kubernetes
Depuis Kubernetes 1.25 (stable), kubectl debug permet d’attacher un container temporaire à un pod existant :
kubectl debug -it pod/myapp --image=busybox --target=myapp
Le container éphémère partage le namespace de processus du container cible — vous pouvez voir ses processus, inspecter ses fichiers, diagnostiquer des problèmes réseau.
3. cdebug — le couteau suisse
cdebug (par Ivan Velichko, créateur d’iximiuz Labs) est un outil open source qui lance un container sidecar attaché aux namespaces process et réseau du container cible :
# Docker
cdebug exec -it mycontainer
# Kubernetes
cdebug exec -it --privileged pod/myapp/myapp
4. docker debug (Docker Desktop 4.27+)
Une fonctionnalité de Docker Desktop qui injecte un environnement Nix dans le container. Nécessite une licence Docker Desktop Pro.
5. Partage de namespaces manuels
docker run --rm -it \
--pid container:target \
--network container:target \
busybox sh
Vous obtenez un shell dans un container BusyBox qui partage les namespaces PID et réseau du container cible.
9. Comparatif synthétique
Par critère technique
| scratch | Google Distroless | Chainguard | Ubuntu Chiseled | Docker DHI | |
|---|---|---|---|---|---|
| Base | Rien | Debian | Wolfi | Ubuntu | Debian/Alpine |
| libc | Aucune | glibc | glibc | glibc | glibc/musl |
| Taille (base) | 0 B | ~2-20 MB | ~2-20 MB | ~5-11 MB | Variable |
| Shell | Non | Non (sauf :debug) | Non (sauf -dev) | Non | Non |
| Build tool | Dockerfile | Bazel | apko + melange | chisel + Dockerfile | Propriétaire |
| SBOM | Non | Non par défaut | Oui (build-time) | Non par défaut | Oui |
| Signatures | Non | Cosign (keyless) | Cosign (keyless) | Non | Oui |
| CVE SLA | N/A | Aucun | 7j critique (payant) | Via Ubuntu Pro | 7j critique (payant) |
| Rebuild auto | N/A | PR-based | Nightly | LTS schedule | Non documenté |
| Extensibilité | Totale mais manuelle | Difficile (Bazel) | Facile (YAML) | Moyenne (YAML slices) | Limitée |
| Coût | Gratuit | Gratuit | Free tier + payant | Gratuit (+ Pro) | Gratuit (+ Enterprise) |
Par langage/runtime
| Langage | Recommandation distroless | Alternative |
|---|---|---|
| Go (static) | scratch ou gcr.io/distroless/static | ko (auto) |
| Go (CGO) | gcr.io/distroless/base | Chainguard glibc-dynamic |
| Rust (musl) | scratch | — |
| Rust (glibc) | gcr.io/distroless/cc | Chainguard cc |
| Node.js | gcr.io/distroless/nodejs22 | Chainguard node |
| Python | gcr.io/distroless/python3 | Chainguard python |
| Java | gcr.io/distroless/java21 | Ubuntu Chiseled JRE |
| .NET | Ubuntu Chiseled (jammy-chiseled) | — |
| Statique (HTML/CSS) | scratch + serveur HTTP statique | nginx Chainguard |
10. Focus Node.js — pourquoi pas Ubuntu Chiseled ?
Le tableau synthétique ci-dessus mentionne Ubuntu Chiseled pour Java et .NET, mais pas pour Node.js. Ce n’est pas un oubli — c’est le reflet de la réalité du terrain.
L’état des lieux
Canonical propose une image chiseled Node.js via le dépôt canonical/chiseled-nodejs. En février 2026, voici ce qu’on constate :
| Indicateur | Valeur |
|---|---|
| Stars GitHub | 3 |
| Commits totaux | ~19 |
| Dernière activité | Septembre 2025 (refactoring) |
| Versions Node.js | 18 seulement (Ubuntu 24.04) |
| Node.js 20 | PR ouverte, non mergée |
| Node.js 22 | Slice dispo dans chisel-releases (Ubuntu 24.10+), mais pas de rock officiel |
| Releases formelles | Aucune |
| Retours de production | Aucun publié |
Pour comparaison, Node.js 18 a atteint sa fin de vie en avril 2025. L’image officielle Canonical ne propose donc qu’une version de Node.js qui n’est plus supportée upstream.
Pourquoi cette immaturité ?
La raison est structurelle. Les versions de Node.js disponibles dans chisel sont liées aux paquets Ubuntu :
| Branche chisel-releases | Paquet | Version Node.js |
|---|---|---|
ubuntu-22.04 (Jammy LTS) | — | Pas de slice Node.js |
ubuntu-24.04 (Noble LTS) | libnode109 | Node.js 18.x |
ubuntu-24.10 (Oracular) | libnode115 | Node.js 22.x |
ubuntu-25.04 (Plucky) | libnode115 | Node.js 22.x |
Source : canonical/chisel-releases, branches par version Ubuntu.
Le problème : Ubuntu 24.04 LTS (la base stable) embarque Node.js 18. Pour obtenir Node.js 22 via chisel, il faut utiliser Ubuntu 24.10 ou 25.04 — des releases non-LTS, donc sans le support long terme qui fait l’intérêt de Chiseled. On perd l’argument principal.
À titre de comparaison, le partenariat .NET avec Microsoft a bénéficié d’un investissement conjoint significatif : images officielles sur MCR, documentation complète, benchmarks publiés, intégration dans la chaîne d’outils .NET. Rien de comparable n’existe pour Node.js.
Comparatif détaillé des options Node.js distroless
| Google Distroless | Chainguard | Ubuntu Chiseled | node:22-alpine (réf.) | |
|---|---|---|---|---|
| Image | gcr.io/distroless/nodejs22-debian12 | cgr.dev/chainguard/node | ubuntu/node | node:22-alpine |
| Disponible depuis | ~2018 | ~2023 | Sept. 2023 | ~2016 |
| Étoiles GitHub | 22 284 (projet global) | N/A (propriétaire) | 3 | N/A (officiel) |
| Versions Node.js | 20, 22, 24 | latest (gratuit) ; toutes (payant) | 18 seulement | Toutes LTS |
| Taille compressée | ~55-60 MB | ~55-60 MB | ~37 MB | ~55 MB |
| Taille décompressée | ~141 MB | ~145 MB | ~102 MB | ~155 MB |
| libc | glibc | glibc | glibc | musl |
| npm inclus | Non | Oui | Non | Oui |
| Shell | Non (:debug = BusyBox) | Non (:latest-dev = shell) | Non | Oui |
| SBOM | Non | Oui (build-time) | Non | Non |
| Signatures Cosign | Oui | Oui | Non | Non |
| Fréquence rebuild | Quotidienne (automatisée) | Quotidienne (automatisée) | Rare | Régulière |
| SLA CVE | Aucun | 7j critique (payant) | Via Ubuntu Pro | Aucun |
| Modules natifs | Difficile (pas de compilateur) | Meilleur (glibc + -dev) | Difficile | Possible (musl) |
| Compatibilité scanners | Oui (Debian dpkg) | Oui | Problématique | Oui |
Trois scénarios concrets
1. Vous voulez du battle-tested, gratuit → Google Distroless
C’est le choix par défaut. Supporté depuis ~2018, utilisé par Kubernetes et Knative, mis à jour quotidiennement. Node.js 20, 22 et 24 sont disponibles. La seule contrainte : pas de npm dans l’image de production (multi-stage obligatoire).
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm run build
RUN npm ci --omit=dev
FROM gcr.io/distroless/nodejs22-debian12:nonroot
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["dist/server/entry.mjs"]
2. Vous voulez zéro CVE avec npm dans l’image → Chainguard
Chainguard est la seule option distroless qui inclut npm dans l’image de production. Utile si vous avez des scripts postinstall ou des dépendances optionnelles résolues au démarrage. Le tag gratuit :latest est suffisant pour du développement ; la production sérieuse nécessite le tier payant pour le version pinning.
FROM cgr.dev/chainguard/node:latest-dev AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM cgr.dev/chainguard/node:latest
WORKDIR /app
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/package.json ./
ENV NODE_ENV=production
CMD ["dist/server/entry.mjs"]
3. Vous voulez absolument du Chiseled Ubuntu Node.js → DIY
Si la cohérence Ubuntu dev/prod est une priorité absolue, il est possible de construire son propre image chiseled Node.js. Le projet communautaire nvitaterna/nodejs-distroless fait exactement ça : il utilise chisel pour produire des images Node.js 22 et 24 distroless basées sur Ubuntu. C’est plus activement maintenu que l’image officielle de Canonical.
Mais il faut être lucide sur ce que ça implique :
- Vous dépendez des versions Node.js empaquetées par Ubuntu
- Vous devez gérer la compatibilité des scanners (génération du fichier dpkg status)
- Vous n’avez ni SBOM, ni signatures Cosign
- Le support communautaire est quasi inexistant
Verdict Node.js
Pour Node.js en 2026, Ubuntu Chiseled n’est pas prêt pour la production. Le partenariat Canonical/.NET montre ce que Chiseled peut être quand il y a un investissement sérieux — mais cet investissement n’a pas eu lieu pour Node.js.
| Votre priorité | Choix recommandé |
|---|---|
| Stabilité et track record | Google Distroless (nodejs22-debian12) |
| Zéro CVE + npm + DX | Chainguard (node:latest gratuit, ou payant pour pinning) |
| Cohérence Ubuntu dev/prod | Attendre la maturation de Chiseled, ou utiliser node:22-slim en attendant |
| Taille minimale absolue | Chiseled DIY (~37 MB compressé) — mais au prix de la maturité |
11. Quelle image choisir ?
Le choix dépend de trois facteurs : votre langage, votre tolérance au risque, et votre budget.
Arbre de décision
Votre binaire est-il 100% statiquement lié ?
├── Oui → scratch (ou distroless/static pour les certs TLS)
└── Non → Quel runtime ?
├── Go (avec CGO) → distroless/base ou Chainguard glibc-dynamic
├── Node.js → distroless/nodejs22 ou Chainguard node
├── Python → distroless/python3 ou Chainguard python
├── Java → distroless/java21 ou Ubuntu Chiseled JRE
├── .NET → Ubuntu Chiseled (partenariat Microsoft)
└── Autre → évaluer Chainguard ou construire custom avec chisel
Avez-vous besoin d'un SLA sur les CVEs ?
├── Oui → Chainguard Production ou Docker DHI Enterprise
└── Non → Google Distroless ou les free tiers
Votre stack est-elle Ubuntu-native ?
├── Oui → Ubuntu Chiseled (même paquets dev → prod)
└── Non → Google Distroless ou Chainguard
Ce qui ne change pas
Quelle que soit l’approche choisie, les fondamentaux restent les mêmes :
- Multi-stage build obligatoire — les outils de build (npm, pip, go, cargo) restent dans le build stage.
- CMD/ENTRYPOINT en forme JSON —
["node", "app.js"]pas"node app.js". - Utilisateur non-root —
:nonroottags ouUSERdirective. - Pas de secrets dans l’image — utiliser les secret mounts BuildKit ou des variables d’environnement au runtime.
- Scanner régulièrement — même une image zéro CVE aujourd’hui peut en avoir demain. Intégrer Trivy, Grype ou Snyk dans la CI.