Aller au contenu

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

Fonctionne dans votre navigateur 17 règles, DF001–DF017 Sans inscription Mis à jour le 31 juil. 2026

Playground du Dockerfile Linter

Examples

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.

Dockerfile input

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.

Findings

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.

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

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

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

  4. 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
DF001 erreur

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

DOCKERFILE
RUN apt-get update
FROM debian:bookworm-slim

Écrivez

DOCKERFILE
FROM debian:bookworm-slim
RUN apt-get update
DF002 avertissement

É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

DOCKERFILE
FROM node
FROM node:latest

Écrivez

DOCKERFILE
FROM node:22-bookworm-slim
DF003 erreur

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

DOCKERFILE
FROM alpine:3.20
COPY --from=builder /out /srv

Écrivez

DOCKERFILE
FROM golang:1.23-alpine AS builder
FROM alpine:3.20
COPY --from=builder /out /srv
DF004 avertissement

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

DOCKERFILE
ADD entrypoint.sh /entrypoint.sh

Écrivez

DOCKERFILE
COPY entrypoint.sh /entrypoint.sh
DF005 avertissement

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

DOCKERFILE
ADD https://example.com/tool.tgz /tmp/tool.tgz

Écrivez

DOCKERFILE
RUN curl -fsSL https://example.com/tool.tgz -o /tmp/tool.tgz \
 && echo "<sha256sum>  /tmp/tool.tgz" | sha256sum -c -
DF006 avertissement

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

DOCKERFILE
WORKDIR app

Écrivez

DOCKERFILE
WORKDIR /app
DF007 avertissement

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

DOCKERFILE
RUN apt-get update
RUN apt-get install -y curl

Écrivez

DOCKERFILE
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
DF008 avertissement

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

DOCKERFILE
RUN apt-get update && apt-get install -y curl

Écrivez

DOCKERFILE
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
DF009 avertissement

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

DOCKERFILE
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2

Écrivez

DOCKERFILE
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci
DF010 avertissement

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

DOCKERFILE
FROM node:22-bookworm-slim
COPY . /app
CMD ["node", "/app/server.js"]

Écrivez

DOCKERFILE
FROM node:22-bookworm-slim
COPY --chown=node:node . /app
USER node
CMD ["node", "/app/server.js"]
DF011 avertissement

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

DOCKERFILE
RUN cd /src && make

Écrivez

DOCKERFILE
WORKDIR /src
RUN make
DF012 avertissement

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

DOCKERFILE
RUN curl -fsSL https://get.example.com/install.sh | sh

Écrivez

DOCKERFILE
RUN curl -fsSL https://get.example.com/install.sh -o /tmp/install.sh \
 && echo "<sha256sum>  /tmp/install.sh" | sha256sum -c - \
 && sh /tmp/install.sh
DF013 avertissement

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

DOCKERFILE
CMD ["node", "server.js"]
CMD ["node", "worker.js"]

Écrivez

DOCKERFILE
CMD ["node", "server.js"]
# le worker tourne comme son propre conteneur, pas comme un second CMD
DF014 erreur

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

DOCKERFILE
CMD ['nginx', '-g', 'daemon off;']

Écrivez

DOCKERFILE
CMD ["nginx", "-g", "daemon off;"]
DF015 avertissement

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

DOCKERFILE
RUN sudo apt-get install -y curl

Écrivez

DOCKERFILE
USER root
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
USER app
DF016 note

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

DOCKERFILE
MAINTAINER ops@example.com

Écrivez

DOCKERFILE
LABEL org.opencontainers.image.authors="ops@example.com"
DF017 note

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

DOCKERFILE
COPY . /app
RUN npm ci

Écrivez

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

report.txt
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.

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.

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.

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.

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.

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.

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.

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.

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.

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.