Dockerfile Linter · Docker
Analysez un Dockerfile avant qu’il arrive en revue.
Dix-sept règles sur un vrai parse du fichier : l’apt-get update qui servira un index périmé, le secret écrit dans l’historique de votre image, la forme exec que Docker a discrètement exécutée comme chaîne shell. Chaque résultat nomme une ligne et porte le correctif — et rien de ce que vous collez ne quitte l’onglet.
S’exécute dans votre navigateur — rien de ce que vous collez ne quitte cette page. Comment nous le prouvons
Playground du Dockerfile Linter
A build stage is one FROM and every instruction under it; a multi-stage file has several, and only the last one ships. The layer cache reuses an instruction's result until that instruction — or anything it copied — changes.
Results update as you type — press Enter to run now.
Press Esc to release keyboard focus from the editor; ⌘/Ctrl + Enter lints and leaves the editor. Tap a Line badge to jump to that line. Nothing you paste is uploaded.
Paste a Dockerfile above — or tap an example — to see line-numbered findings here, each with the reason it matters and the fix.
Le Manque
Un Dockerfile qui compile n’est pas un Dockerfile juste.
Toutes les erreurs du catalogue ci-dessous compilent proprement. FROM node compile. apt-get update dans sa propre couche compile — puis installe depuis un index en cache depuis le trimestre dernier. CMD ['nginx'] compile, et Docker exécute discrètement /bin/sh -c "['nginx']" au lieu du processus que vous vouliez comme PID 1. Un build vert n’est pas une preuve ; c’est l’absence de preuve.
Demandez le Dockerfile à un assistant et vous héritez de la même classe de bogue, avec davantage d’assurance en prime. Les Dockerfiles générés sont fluides et plausibles : ils passent la revue parce qu’ils ressemblent à tous les autres, et ils portent des défauts de la forme DF007 et DF014 que jamais aucun échec de build ne montrera. Vérifier l’affirmation prend quelques secondes ; le difficile est de savoir quoi vérifier.
hadolint est la bonne réponse en CI, et ceci ne le remplace pas. Ceci couvre le cas qu’il ne couvre pas : un fichier que vous voulez vérifier maintenant, depuis un navigateur, sur une machine où vous ne pouvez pas installer de binaire, sans envoyer un Dockerfile plein d’hôtes de registres internes et d’arguments de build sur le serveur de quelqu’un d’autre. Dix-sept règles, un correctif sur chaque résultat, et une liste explicite de ce qui reste silencieux — parce qu’un linter que vous ne pouvez pas auditer est un linter auquel vous devez faire confiance aveuglément.
Un conteneur tourne déjà ? Docker Run vers Compose transforme la commande en fichier de service, et Env Example Checker repère les variables que votre image attend et que votre environnement ne définit jamais.
Le Pipeline
Comment ça marche.
Quatre étapes, toutes dans votre onglet, réexécutées à mesure que vous tapez.
-
Parser comme BuildKit.
Continuations de ligne, lignes de commentaire entières à l’intérieur, corps de heredoc, forme exec JSON, étapes et table des ARG précédant le premier FROM. Une directive `# escape=` change vraiment le caractère de continuation.
-
Analyser le parse, pas le texte.
Dix-sept règles lisent les instructions et les étapes. Les règles shell parcourent un balayage de chaque RUN qui respecte les guillemets : un pipe dans une chaîne n’est pas un pipe, et un corps de heredoc reste du shell.
-
Signaler une ligne et un correctif.
Chaque résultat nomme la ligne PHYSIQUE — la ligne 4 d’un RUN plié, pas la ligne où le RUN a commencé — et porte la modification à faire, prête à coller dans une revue.
-
Dire ce qu’il n’a pas vérifié.
Les règles qu’il refuse délibérément d’exécuter figurent sur cette page, avec la raison de chacune. Un linter que vous ne pouvez pas auditer est un linter auquel vous devez faire confiance aveuglément.
Référence
Le catalogue de règles.
Les dix-sept règles : trois erreurs, douze avertissements et deux notes. Une erreur signifie que Docker rejette le fichier ou exécute discrètement autre chose ; un avertissement signifie que cela compile et que c’est faux ; une note est bonne à savoir. Chaque résultat du playground renvoie à sa règle ici.
| Règle | Gravité | Ce qu’elle détecte |
|---|---|---|
| DF001 | erreur | La première instruction doit être FROM |
| DF002 | avertissement | Épinglez l’image de base sur un tag |
| DF003 | erreur | COPY --from doit nommer une étape antérieure |
| DF004 | avertissement | Utilisez COPY pour les fichiers locaux, pas ADD |
| DF005 | avertissement | Vérifiez un ADD distant |
| DF006 | avertissement | WORKDIR devrait être absolu |
| DF007 | avertissement | Update et installation dans le même RUN |
| DF008 | avertissement | Nettoyez le cache de paquets dans la même couche |
| DF009 | avertissement | N’enfermez pas de secrets dans ENV ou ARG |
| DF010 | avertissement | Ne faites pas tourner l’étape finale en root |
| DF011 | avertissement | Utilisez WORKDIR au lieu de cd |
| DF012 | avertissement | Ne redirigez pas un téléchargement dans un shell |
| DF013 | avertissement | Un CMD et un ENTRYPOINT par étape |
| DF014 | erreur | La forme exec doit être du JSON valide |
| DF015 | avertissement | Retirez sudo des étapes de build |
| DF016 | note | MAINTAINER est obsolète |
| DF017 | note | Copiez le manifeste avant d’installer les dépendances |
La première instruction doit être FROM
Seuls ARG (et les commentaires) peuvent précéder FROM. Tout le reste n’a aucune image sur laquelle agir, et Docker refuse le build. Un fichier sans aucun FROM relève de la même règle.
Au lieu de
RUN apt-get update
FROM debian:bookworm-slim Écrivez
FROM debian:bookworm-slim
RUN apt-get update Épinglez l’image de base sur un tag
Une référence sans tag signifie :latest, et :latest bouge. Les épinglages par digest, scratch, les références à une étape antérieure et les tags issus d’un argument de build non résoluble sont laissés tranquilles.
Au lieu de
FROM node
FROM node:latest Écrivez
FROM node:22-bookworm-slim COPY --from doit nommer une étape antérieure
Une étape ne peut copier que depuis une étape définie au-dessus. Un index numérique égal ou postérieur à l’étape courante, et un nom nu qui ne correspond à aucune étape, font échouer le build avec « invalid from flag value ». Tout ce qui porte un registre, un tag ou un digest est traité comme une image externe et laissé tranquille.
Au lieu de
FROM alpine:3.20
COPY --from=builder /out /srv Écrivez
FROM golang:1.23-alpine AS builder
FROM alpine:3.20
COPY --from=builder /out /srv Utilisez COPY pour les fichiers locaux, pas ADD
ADD extrait automatiquement les archives tar locales et peut télécharger des URL, ce qui fait qu’une simple copie se comporte autrement que ce que la ligne annonce. Extraire une archive est le seul travail pour lequel ADD vaut la peine, donc un ADD dont toutes les sources sont des tarballs reste silencieux.
Au lieu de
ADD entrypoint.sh /entrypoint.sh Écrivez
COPY entrypoint.sh /entrypoint.sh Vérifiez un ADD distant
Un ADD distant enferme dans une couche ce que le serveur renvoie, et rien dans le build ne remarque le changement. Le --checksum= de BuildKit satisfait la règle ; télécharger et vérifier dans un même RUN aussi.
Au lieu de
ADD https://example.com/tool.tgz /tmp/tool.tgz Écrivez
RUN curl -fsSL https://example.com/tool.tgz -o /tmp/tool.tgz \
&& echo "<sha256sum> /tmp/tool.tgz" | sha256sum -c - WORKDIR devrait être absolu
Un WORKDIR relatif se résout par rapport au WORKDIR précédent : le répertoire qu’il sélectionne dépend donc des lignes au-dessus — et change dès qu’on les réordonne. Les chemins commençant par une variable sont laissés tranquilles.
Au lieu de
WORKDIR app Écrivez
WORKDIR /app Update et installation dans le même RUN
Chaque RUN est sa propre couche. Une couche update en cache plus une couche d’installation reconstruite, cela signifie installer depuis un index de paquets qui peut avoir des mois. Couvre apt-get, apt et apk.
Au lieu de
RUN apt-get update
RUN apt-get install -y curl Écrivez
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* Nettoyez le cache de paquets dans la même couche
L’index de paquets est validé avec la couche : le supprimer dans un RUN ultérieur ne récupère rien. Satisfait par rm -rf /var/lib/apt/lists/* pour apt, --no-cache pour apk, et dnf/yum clean all — et entièrement supprimé quand le RUN porte un --mount=type=cache, car le cache n’entre alors jamais dans l’image.
Au lieu de
RUN apt-get update && apt-get install -y curl Écrivez
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* N’enfermez pas de secrets dans ENV ou ARG
Chaque valeur d’ENV et d’ARG est stockée dans l’historique de l’image et lisible avec docker history par quiconque peut la puller. L’écraser dans une couche ultérieure ne l’enlève pas. Seuls les noms qui ressemblent à une information d’identification ET portent une valeur non vide sont signalés.
Au lieu de
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2 Écrivez
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci Ne faites pas tourner l’étape finale en root
Sans USER, le processus tourne en uid 0. Seule l’étape FINALE est vérifiée — les étapes de build ont légitimement besoin de root — et seul le DERNIER USER y compte, car c’est celui sous lequel le conteneur tourne.
Au lieu de
FROM node:22-bookworm-slim
COPY . /app
CMD ["node", "/app/server.js"] Écrivez
FROM node:22-bookworm-slim
COPY --chown=node:node . /app
USER node
CMD ["node", "/app/server.js"] Utilisez WORKDIR au lieu de cd
Un cd ne dure que le temps du RUN où il est écrit. L’instruction suivante repart du WORKDIR de l’étape, ce qui est une cause fréquente de « no such file or directory » sur le COPY ou le CMD qui suit.
Au lieu de
RUN cd /src && make Écrivez
WORKDIR /src
RUN make Ne redirigez pas un téléchargement dans un shell
curl … | sh exécute ce que le serveur renvoie, sans signature ni somme de contrôle. Détecté aussi bien dans les lignes RUN pliées que dans les corps de heredoc, et signalé sur la ligne où le téléchargement est écrit.
Au lieu de
RUN curl -fsSL https://get.example.com/install.sh | sh Écrivez
RUN curl -fsSL https://get.example.com/install.sh -o /tmp/install.sh \
&& echo "<sha256sum> /tmp/install.sh" | sha256sum -c - \
&& sh /tmp/install.sh Un CMD et un ENTRYPOINT par étape
Docker garde le dernier de chaque étape et écarte les autres silencieusement. Le comptage est par étape : une étape de build avec son propre CMD ne pose donc pas de problème, et les instructions enveloppées dans ONBUILD ne comptent pas.
Au lieu de
CMD ["node", "server.js"]
CMD ["node", "worker.js"] Écrivez
CMD ["node", "server.js"]
# le worker tourne comme son propre conteneur, pas comme un second CMD La forme exec doit être du JSON valide
Docker ne lit l’argument comme un tableau en forme exec que s’il se parse comme du JSON — guillemets doubles, pas de virgule finale. Sinon il exécute tout silencieusement en forme shell via /bin/sh -c, crochets compris.
Au lieu de
CMD ['nginx', '-g', 'daemon off;'] Écrivez
CMD ["nginx", "-g", "daemon off;"] Retirez sudo des étapes de build
Une étape de build tourne déjà sous l’USER de l’étape — root si vous ne l’avez pas changé — sans TTY, et la plupart des images de base ne fournissent même pas sudo. Changez d’utilisateur explicitement à la place.
Au lieu de
RUN sudo apt-get install -y curl Écrivez
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
USER app MAINTAINER est obsolète
Obsolète depuis Docker 1.13, en 2017. Cela compile toujours, mais rien ne le lit — c’est le label OCI que les registres et les scanners affichent.
Au lieu de
MAINTAINER ops@example.com Écrivez
LABEL org.opencontainers.image.authors="ops@example.com" Copiez le manifeste avant d’installer les dépendances
Un COPY . placé plus haut dans l’étape que l’installation des dépendances signifie que le moindre changement de code source invalide la couche d’installation et réinstalle tout. Reconnaît npm, yarn, pnpm, pip -r, bundler, composer, go mod download et cargo. Associez la réorganisation à un .dockerignore pour que node_modules et .git n’entrent même pas dans le contexte.
Au lieu de
COPY . /app
RUN npm ci Écrivez
COPY package.json package-lock.json ./
RUN npm ci
COPY . . La Limite
Ce qu’il ne signale délibérément pas.
Chacun de ces points a été envisagé puis écarté, et la même liste figure en commentaire en tête du moteur. Un silence que vous pouvez lire vaut mieux qu’une règle que vous apprenez à ignorer.
L’épinglage de versions au-delà du tag
apt-get install curl sans version et pip install django sans == sont tous deux légitimes. DF002 demande un tag ; il n’exige jamais un digest ni un jeu de paquets épinglé.
EXPOSE
Il ne publie rien par lui-même. Son absence ne casse rien et un EXPOSE en trop ne coûte rien — pure opinion.
HEALTHCHECK
Complètement ignoré par Kubernetes, là où ce public fait tourner ses images.
Les caches de téléchargement de pip et npm
Réels mais petits, et les options changent d’une version d’outil à l’autre. DF008 couvre les gestionnaires de paquets système, là où sont les mégaoctets.
update ; install reliés par un point-virgule
Cela masque un update en échec, mais les deux commandes SONT dans la même couche : DF007 n’a donc rien à dire. Une règle distincte sur les opérateurs shell serait du bruit.
Les tags issus d’une variable non résoluble
FROM node:$TAG est décidé au moment du build par --build-arg. Deviner sa résolution serait une réponse assurée et fausse.
COPY --from pointant sur une image de registre
Tout ce qui porte une barre oblique, un deux-points ou un @digest est une référence externe légale : DF003 se tait donc.
La sémantique shell profonde
Il n’y a pas d’AST shell ici : aucun raisonnement sur set -e, aucune analyse de code de sortie, aucun suivi de variables. Le scanner respecte les guillemets et s’arrête là. Il ne lit pas non plus l’argv d’un RUN en forme exec comme du texte shell.
Les heredocs non terminés, les guillemets déséquilibrés et les dialectes # syntax=
Docker les signale lui-même, et un parse partiel ne doit pas inventer une règle par-dessus.
Tout ce qui est hors du fichier
Aucune image n’est téléchargée, aucun registre n’est interrogé et aucun .dockerignore n’est lu, parce que rien de tout cela n’est dans le texte que vous avez collé.
Limites annoncées plutôt que cachées : le linter analyse jusqu’à 200 000 caractères, conserve au plus 20 résultats par règle et 200 au total, et vous donne le nombre réel dès qu’un plafond s’applique.
Étape suivante
Collez le rapport dans la revue.
« Copy report » vous donne toute l’exécution en texte brut : une ligne par résultat, avec le numéro de ligne, l’identifiant de la règle et le correctif. Puis continuez : transformez la commande docker run en service Compose, ou vérifiez que les variables attendues par l’image existent réellement.
Dockerfile Linter — 1 error, 12 warnings, 2 info across 15 lines
ERRORS (1)
L14 DF014: CMD looks like a JSON array but is not valid JSON.
WARNINGS (12)
L1 DF002: Base image "ubuntu" has no tag, so Docker resolves it to :latest.
L6 DF007: apt-get update runs without an install in the same RUN.
L8 DF012: Piping a download straight into a shell. FAQ
Vos questions, nos réponses.
Appuyez sur une question pour afficher la réponse.
Que vérifie le Dockerfile Linter ?
Dix-sept règles, de DF001 à DF017, et chacune décrit une erreur qui compile quand même : une image de base sans tag ou en :latest, un COPY --from qui nomme une étape qui n’existe pas encore, ADD là où COPY convient, un téléchargement distant non vérifié, un WORKDIR relatif, un apt-get update isolé dans sa propre couche, un cache de paquets laissé dans l’image, un secret écrit dans l’historique de l’image par ENV ou ARG, une étape finale qui tourne en root, cd au lieu de WORKDIR, curl redirigé dans un shell, un second CMD qui gagne silencieusement, une forme exec JSON qui n’est pas du JSON valide, sudo dans une étape de build, MAINTAINER, et un COPY . qui détruit le cache de l’installation des dépendances. Chaque règle a sa propre sous-section sur cette page, avec un extrait avant/après.
Est-ce hadolint ?
Non, et ce n’est pas le but. hadolint est un binaire que vous installez, et il associe ses propres règles à tout ce que ShellCheck signale sur le shell contenu dans vos lignes RUN — excellent en CI, et la bonne réponse quand vous pouvez imposer une chaîne d’outils. Ceci couvre le cas qu’il ne couvre pas : un Dockerfile que vous voulez vérifier tout de suite, depuis un navigateur, sur une machine où vous ne pouvez rien installer, sans envoyer le fichier à un serveur. Dix-sept règles au lieu d’une longue traîne, chacune avec son correctif, et plus bas une liste explicite de ce qu’il refuse de signaler.
Mon Dockerfile quitte-t-il un jour mon navigateur ?
Non. Le parseur et les dix-sept règles sont du JavaScript exécuté dans votre onglet — pas de serveur, pas d’appel d’API, pas de journalisation, donc 0 octet envoyé. Cela compte plus ici que pour la plupart des outils : les Dockerfiles contiennent des noms d’hôtes de registres internes, des index de paquets privés, des arguments de build et, de temps en temps, une information d’identification que personne n’aurait dû committer.
Pourquoi apt-get update seul pose-t-il problème ?
Parce que chaque RUN est une couche distincte avec sa propre entrée de cache. Quand vous ne modifiez ensuite que la ligne d’installation, Docker réutilise la couche update mise en cache — elle peut avoir des semaines ou des mois — et exécute l’installation contre cet index de paquets périmé. Le résultat est soit une version inattendue, soit un 404 pour un paquet remplacé depuis. Les réunir dans un seul RUN fait partager la même entrée de cache à l’index et à l’installation : ils ne peuvent plus se contredire.
Qu’y a-t-il de mal avec le tag :latest ?
Rien, jusqu’à ce que l’éditeur pousse une nouvelle version. :latest est un pointeur mobile, et un FROM sans tag signifie :latest : l’image de base utilisée par votre build le mois dernier et celle de ce soir peuvent être des versions majeures différentes sans le moindre changement de votre côté — et rien dans le fichier ne consigne celle que vous avez réellement testée. Épinglez un tag que vous avez testé. S’il vous faut des rebuilds identiques à l’octet, ajoutez aussi le digest (@sha256:…). Ce linter demande un tag et n’exige jamais un digest : cet arbitrage vous appartient.
Pourquoi CMD ['nginx'] est-il une erreur et non un avertissement ?
Parce que Docker ne le rejette pas : il fait silencieusement autre chose. La forme exec n’est utilisée que si l’argument se parse comme du JSON, et le JSON exige des guillemets doubles. Avec des apostrophes, tout retombe en forme shell : Docker exécute /bin/sh -c "['nginx']" et les crochets comme les apostrophes deviennent une partie de la commande. Rien ne vous avertit à la compilation ; le conteneur ne démarre simplement pas, ou démarre un shell qui n’est pas le processus que vous vouliez comme PID 1.
Peut-il me dire si j’ai besoin d’un .dockerignore ?
Pas directement, et il le dit plutôt que de deviner. Un .dockerignore est un fichier séparé : un linter qui ne lit que le Dockerfile ne peut pas savoir si node_modules, .git et vos artefacts de build partent dans le contexte de build. Ce qu’il peut faire, c’est repérer le motif qui rend un .dockerignore manquant coûteux — un COPY . avant l’installation des dépendances, c’est-à-dire DF017 — et placer le conseil sur le .dockerignore dans le texte de correctif de cette règle.
Pourquoi ne signale-t-il pas un EXPOSE ou un HEALTHCHECK manquant ?
Les deux ont été envisagés puis délibérément écartés. EXPOSE est de la documentation : il ne publie rien par lui-même, donc son absence ne casse rien et un EXPOSE en trop ne coûte rien. HEALTHCHECK est complètement ignoré par Kubernetes, là où ces images tournent réellement : le signaler entraînerait ce public à ignorer le linter. La liste complète de ce qui reste silencieux, avec la raison de chaque point, se trouve dans le panneau « Ce qu’il ne signale délibérément pas » au-dessus de la FAQ.
Quelle taille de Dockerfile accepte-t-il ?
Jusqu’à 200 000 caractères — environ quatre mille lignes de texte dense, soit deux ordres de grandeur au-delà de tout Dockerfile réel. Au-delà, il refuse avec un message plutôt que de figer votre onglet, car une entrée de cette taille est un journal de build ou une archive, pas un Dockerfile. Les résultats sont plafonnés aussi, à 20 par règle et 200 au total, et le panneau annonce le plafond et le nombre réel dès qu’il s’applique.
More free, private DevOps tools.
Le Dockerfile Linter est l’un des outils d’OpsCanopy — une canopée grandissante de validateurs, convertisseurs et testeurs qui tournent dans le navigateur et ne touchent jamais un serveur.
39 free tools, every one offline-capable — opscanopy.com works with no signup and nothing uploaded.
À voir aussi : Docker Run vers Compose pour transformer une commande de conteneur en fichier de service, Env Example Checker pour les variables qu’une image attend, le GitHub Actions Validator et le GitLab CI Validator pour le pipeline qui construit l’image, et le Convertisseur JSON ↔ YAML quand la configuration autour doit être remodelée — ou parcourez le répertoire des outils complet.
Fourni tel quel, pour votre confort ; vérifiez toujours une configuration critique auprès de l’outil qui la consommera. OpsCanopy est gratuit et ouvert.