Dockerfile Linter · Docker
Ein Dockerfile linten, bevor es ins Review geht.
Siebzehn Regeln über einen echten Parse der Datei: das apt-get update, das einen veralteten Index ausliefern wird, das Secret in Ihrer Image-History, die Exec-Form, die Docker leise als Shell-String ausgeführt hat. Jeder Fund nennt eine Zeile und trägt den Fix — und nichts, was Sie einfügen, verlässt den Tab.
Läuft in Ihrem Browser — nichts, was Sie einfügen, verlässt diese Seite. Wie wir das belegen
Dockerfile Linter Playground
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.
Die Lücke
Ein Dockerfile, das baut, ist kein Dockerfile, das stimmt.
Jeder Fehler im Katalog unten baut sauber. FROM node baut. apt-get update in einem eigenen Layer baut — und installiert dann aus einem Index, der seit letztem Quartal im Cache liegt. CMD ['nginx'] baut, und Docker führt leise /bin/sh -c "['nginx']" aus statt des Prozesses, den Sie als PID 1 gemeint haben. Ein grüner Build ist kein Beweis; er ist das Fehlen eines Beweises.
Lassen Sie das Dockerfile von einem Assistenten schreiben, erben Sie dieselbe Fehlerklasse mit mehr Selbstsicherheit obendrauf. Generierte Dockerfiles sind flüssig und plausibel: Sie kommen durchs Review, weil sie wie jedes andere Dockerfile aussehen, und tragen Defekte in der Art von DF007 und DF014, auf die kein fehlgeschlagener Build je zeigen wird. Die Behauptung zu prüfen dauert Sekunden; das Schwere ist zu wissen, was man prüfen muss.
hadolint ist die richtige Antwort in der CI, und dies hier ersetzt es nicht. Dies ist der Fall, den es nicht abdeckt: eine Datei, die Sie jetzt geprüft haben wollen, aus dem Browser, auf einer Maschine, auf der Sie keine Binary installieren dürfen, ohne ein Dockerfile voller interner Registry-Hosts und Build-Argumente auf den Server von jemand anderem zu schicken. Siebzehn Regeln, ein Fix zu jedem Fund, und eine ausdrückliche Liste dessen, was still bleibt — denn ein Linter, den man nicht prüfen kann, ist ein Linter, dem man glauben muss.
Läuft schon ein Container? Docker Run zu Compose macht aus dem Kommando eine Service-Datei, und Env Example Checker findet die Variablen, die Ihr Image erwartet und Ihre Umgebung nie setzt.
Die Pipeline
So funktioniert es.
Vier Schritte, alle in Ihrem Browser-Tab, neu ausgeführt während Sie tippen.
-
Parsen wie BuildKit.
Zeilenfortsetzungen, ganze Kommentarzeilen darin, Heredoc-Rümpfe, die JSON-Exec-Form, Stages und die ARG-Tabelle vor dem ersten FROM. Eine `# escape=`-Direktive dreht die Fortsetzungszeichen wirklich um.
-
Den Parse analysieren, nicht den Text.
Siebzehn Regeln lesen Instruktionen und Stages. Die Shell-Regeln laufen über einen quote-bewussten Scan jedes RUN: eine Pipe in einer Zeichenkette ist keine Pipe, und ein Heredoc-Rumpf ist trotzdem Shell.
-
Zeile und Fix melden.
Jeder Fund nennt die PHYSISCHE Zeile — Zeile 4 eines gefalteten RUN, nicht die Zeile, in der das RUN begann — und trägt die Änderung, die zu machen ist, fertig zum Einfügen in ein Review.
-
Sagen, was nicht geprüft wurde.
Die Regeln, die er bewusst nicht ausführt, stehen mit Begründung auf dieser Seite. Ein Linter, den man nicht prüfen kann, ist ein Linter, dem man glauben muss.
Referenz
Der Regelkatalog.
Alle siebzehn Regeln: drei Fehler, zwölf Warnungen und zwei Hinweise. Ein Fehler heißt, Docker lehnt die Datei ab oder führt leise etwas anderes aus; eine Warnung heißt, es baut und ist trotzdem falsch; ein Hinweis ist wissenswert. Jeder Fund im Playground verlinkt auf seine Regel hier.
| Regel | Schwere | Was sie findet |
|---|---|---|
| DF001 | Fehler | Die erste Instruktion muss FROM sein |
| DF002 | Warnung | Das Basis-Image auf einen Tag pinnen |
| DF003 | Fehler | COPY --from muss eine frühere Stage nennen |
| DF004 | Warnung | Für lokale Dateien COPY nehmen, nicht ADD |
| DF005 | Warnung | Ein Remote-ADD verifizieren |
| DF006 | Warnung | WORKDIR sollte absolut sein |
| DF007 | Warnung | Update und Installation im selben RUN |
| DF008 | Warnung | Den Paket-Cache im selben Layer aufräumen |
| DF009 | Warnung | Keine Secrets in ENV oder ARG backen |
| DF010 | Warnung | Die letzte Stage nicht als root laufen lassen |
| DF011 | Warnung | WORKDIR statt cd verwenden |
| DF012 | Warnung | Keinen Download in eine Shell pipen |
| DF013 | Warnung | Ein CMD und ein ENTRYPOINT pro Stage |
| DF014 | Fehler | Die Exec-Form muss gültiges JSON sein |
| DF015 | Warnung | sudo aus Build-Schritten entfernen |
| DF016 | Hinweis | MAINTAINER ist veraltet |
| DF017 | Hinweis | Das Manifest vor den Dependencies kopieren |
Die erste Instruktion muss FROM sein
Nur ARG (und Kommentare) dürfen vor FROM stehen. Alles andere hat kein Image, auf dem es arbeiten könnte, und Docker verweigert den Build. Eine Datei ohne jedes FROM fällt unter dieselbe Regel.
Statt
RUN apt-get update
FROM debian:bookworm-slim So schreiben
FROM debian:bookworm-slim
RUN apt-get update Das Basis-Image auf einen Tag pinnen
Eine Referenz ohne Tag bedeutet :latest, und :latest wandert. Digest-Pins, scratch, Verweise auf eine frühere Stage und Tags, die aus einem nicht auflösbaren Build-Argument kommen, bleiben unangetastet.
Statt
FROM node
FROM node:latest So schreiben
FROM node:22-bookworm-slim COPY --from muss eine frühere Stage nennen
Eine Stage kann nur aus einer darüber definierten kopieren. Ein numerischer Index auf oder hinter der aktuellen Stage und ein blanker Name, der auf keine Stage passt, brechen den Build beide mit „invalid from flag value“. Alles, was Registry, Tag oder Digest trägt, gilt als externes Image und bleibt unangetastet.
Statt
FROM alpine:3.20
COPY --from=builder /out /srv So schreiben
FROM golang:1.23-alpine AS builder
FROM alpine:3.20
COPY --from=builder /out /srv Für lokale Dateien COPY nehmen, nicht ADD
ADD entpackt lokale Tar-Archive automatisch und kann URLs holen — damit verhält sich ein einfaches Kopieren anders, als die Zeile sagt. Ein Archiv zu entpacken ist die eine Aufgabe, für die ADD sich lohnt, deshalb bleibt ein ADD still, dessen Quellen alle Tarballs sind.
Statt
ADD entrypoint.sh /entrypoint.sh So schreiben
COPY entrypoint.sh /entrypoint.sh Ein Remote-ADD verifizieren
Ein Remote-ADD backt in ein Layer, was der Server gerade zurückgibt, und im Build merkt nichts, wenn sich das ändert. BuildKits --checksum= erfüllt die Regel; Herunterladen und Verifizieren in einem RUN ebenfalls.
Statt
ADD https://example.com/tool.tgz /tmp/tool.tgz So schreiben
RUN curl -fsSL https://example.com/tool.tgz -o /tmp/tool.tgz \
&& echo "<sha256sum> /tmp/tool.tgz" | sha256sum -c - WORKDIR sollte absolut sein
Ein relatives WORKDIR wird gegen das vorherige WORKDIR aufgelöst — welches Verzeichnis es also wählt, hängt von den Zeilen darüber ab und ändert sich, sobald sie umsortiert werden. Pfade, die mit einer Variablen beginnen, bleiben unangetastet.
Statt
WORKDIR app So schreiben
WORKDIR /app Update und Installation im selben RUN
Jedes RUN ist ein eigenes Layer. Ein gecachtes Update-Layer plus ein neu gebautes Install-Layer heißt: Installation aus einem Paket-Index, der Monate alt sein kann. Deckt apt-get, apt und apk ab.
Statt
RUN apt-get update
RUN apt-get install -y curl So schreiben
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* Den Paket-Cache im selben Layer aufräumen
Der Paket-Index wird mit dem Layer committet, ihn in einem späteren RUN zu löschen holt also nichts zurück. Erfüllt durch rm -rf /var/lib/apt/lists/* bei apt, --no-cache bei apk und dnf/yum clean all — und vollständig unterdrückt, wenn das RUN ein --mount=type=cache trägt, denn dann landet der Cache nie im Image.
Statt
RUN apt-get update && apt-get install -y curl So schreiben
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* Keine Secrets in ENV oder ARG backen
Jeder ENV- und ARG-Wert liegt in der Image-History und ist mit docker history für jeden lesbar, der das Image ziehen kann. Ihn in einem späteren Layer zu überschreiben entfernt ihn nicht. Gemeldet werden nur Namen, die wie ein Credential aussehen UND einen nicht leeren Wert tragen.
Statt
ARG NPM_TOKEN=npm_liveTokenValue
ENV DB_PASSWORD=hunter2 So schreiben
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci Die letzte Stage nicht als root laufen lassen
Ohne USER läuft der Prozess als uid 0. Geprüft wird nur die LETZTE Stage — Build-Stages brauchen root legitim — und darin nur das LETZTE USER, denn das ist das, als das der Container läuft.
Statt
FROM node:22-bookworm-slim
COPY . /app
CMD ["node", "/app/server.js"] So schreiben
FROM node:22-bookworm-slim
COPY --chown=node:node . /app
USER node
CMD ["node", "/app/server.js"] WORKDIR statt cd verwenden
Ein cd hält nur für das RUN, in dem es steht. Die nächste Instruktion startet wieder im WORKDIR der Stage — eine häufige Ursache für „no such file or directory“ beim folgenden COPY oder CMD.
Statt
RUN cd /src && make So schreiben
WORKDIR /src
RUN make Keinen Download in eine Shell pipen
curl … | sh führt aus, was der Server zurückgibt — ohne Signatur, ohne Checksumme. Erkannt in gefalteten RUN-Zeilen und in Heredoc-Rümpfen, und gemeldet auf der Zeile, in der der Download steht.
Statt
RUN curl -fsSL https://get.example.com/install.sh | sh So schreiben
RUN curl -fsSL https://get.example.com/install.sh -o /tmp/install.sh \
&& echo "<sha256sum> /tmp/install.sh" | sha256sum -c - \
&& sh /tmp/install.sh Ein CMD und ein ENTRYPOINT pro Stage
Docker behält das letzte in einer Stage und verwirft den Rest stillschweigend. Pro Stage gezählt — eine Build-Stage mit eigenem CMD ist also in Ordnung, und ONBUILD-gewrappte Instruktionen zählen nicht mit.
Statt
CMD ["node", "server.js"]
CMD ["node", "worker.js"] So schreiben
CMD ["node", "server.js"]
# der Worker läuft als eigener Container, nicht als zweites CMD Die Exec-Form muss gültiges JSON sein
Docker liest das Argument nur als Exec-Form-Array, wenn es als JSON parst — doppelte Anführungszeichen, kein Komma am Ende. Sonst führt es das Ganze stillschweigend als Shell-Form über /bin/sh -c aus, Klammern inklusive.
Statt
CMD ['nginx', '-g', 'daemon off;'] So schreiben
CMD ["nginx", "-g", "daemon off;"] sudo aus Build-Schritten entfernen
Ein Build-Schritt läuft schon als USER der Stage — root, solange Sie nichts geändert haben — ohne TTY, und die meisten Basis-Images liefern sudo gar nicht mit. Wechseln Sie den User stattdessen explizit.
Statt
RUN sudo apt-get install -y curl So schreiben
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
USER app MAINTAINER ist veraltet
Veraltet seit Docker 1.13, also seit 2017. Es baut weiter, aber nichts liest es — was Registries und Scanner anzeigen, ist das OCI-Label.
Statt
MAINTAINER ops@example.com So schreiben
LABEL org.opencontainers.image.authors="ops@example.com" Das Manifest vor den Dependencies kopieren
Ein COPY . früher in der Stage als die Dependency-Installation heißt: jede reine Quellcode-Änderung invalidiert das Install-Layer und installiert alles neu. Erkennt npm, yarn, pnpm, pip -r, Bundler, Composer, go mod download und cargo. Kombinieren Sie das Umsortieren mit einer .dockerignore, damit node_modules und .git gar nicht erst in den Kontext gelangen.
Statt
COPY . /app
RUN npm ci So schreiben
COPY package.json package-lock.json ./
RUN npm ci
COPY . . Die Grenze
Was er bewusst nicht meldet.
Jeder dieser Punkte wurde geprüft und verworfen, und dieselbe Liste steht als Kommentar am Anfang der Engine. Stille, die man nachlesen kann, ist mehr wert als eine Regel, die man zu ignorieren lernt.
Version-Pinning über den Tag hinaus
apt-get install curl ohne Version und pip install django ohne == sind beide legitim. DF002 verlangt einen Tag; einen Digest oder ein gepinntes Paketset verlangt er nie.
EXPOSE
Es veröffentlicht von sich aus nichts. Ein fehlendes bricht nichts und ein zusätzliches kostet nichts — reine Meinung.
HEALTHCHECK
Von Kubernetes komplett ignoriert, und dort lässt dieses Publikum seine Images laufen.
pip- und npm-Download-Caches
Real, aber klein, und die Flags wandern zwischen Tool-Versionen. DF008 deckt die System-Paketmanager ab, dort liegen die Megabytes.
update; install mit Semikolon verbunden
Das verdeckt ein fehlgeschlagenes Update, aber beide Kommandos liegen IM selben Layer, DF007 hat also nichts zu sagen. Eine eigene Regel über Shell-Operatoren wäre Lärm.
Tags, die aus einer nicht auflösbaren Variablen kommen
FROM node:$TAG wird zur Build-Zeit per --build-arg entschieden. Zu raten, worauf das auflöst, wäre eine selbstsicher falsche Antwort.
COPY --from auf ein Registry-Image
Alles mit Slash, Doppelpunkt oder @digest ist eine legale externe Referenz, also bleibt DF003 still.
Tiefe Shell-Semantik
Hier gibt es keinen Shell-AST: kein set -e-Schließen, keine Exit-Code-Analyse, kein Variablen-Tracking. Der Scanner ist quote-bewusst und hört dort auf. Er liest auch das argv eines Exec-Form-RUN nicht als Shell-Text.
Nicht terminierte Heredocs, unbalancierte Quotes und # syntax=-Dialekte
Docker meldet das selbst, und ein Teil-Parse darf darauf keine Regel erfinden.
Alles außerhalb der Datei
Es wird kein Image gezogen, keine Registry befragt und keine .dockerignore gelesen — nichts davon steht in dem Text, den Sie eingefügt haben.
Grenzen, genannt statt versteckt: Der Linter scannt bis zu 200.000 Zeichen, behält höchstens 20 Funde pro Regel und 200 insgesamt und nennt die echte Anzahl, sobald eine Grenze greift.
Nächster Schritt
Den Report ins Review einfügen.
„Copy report“ gibt Ihnen den ganzen Lauf als Klartext — eine Zeile pro Fund, mit Zeilennummer, Regel-ID und Fix. Und dann weiter: das docker run-Kommando in einen Compose-Service verwandeln oder prüfen, ob die Variablen, die das Image erwartet, wirklich existieren.
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
Fragen, beantwortet.
Tippen Sie auf eine Frage, um die Antwort aufzuklappen.
Was prüft der Dockerfile Linter?
Siebzehn Regeln, DF001 bis DF017, jede davon ein Fehler, der trotzdem baut: ein Basis-Image ohne Tag oder mit :latest, ein COPY --from auf eine Stage, die es noch nicht gibt, ADD dort, wo COPY hingehört, ein unverifizierter Remote-Download, ein relatives WORKDIR, ein apt-get update allein in seinem eigenen Layer, ein im Image zurückgelassener Paket-Cache, ein per ENV oder ARG in die Image-History geschriebenes Secret, eine letzte Stage, die als root läuft, cd statt WORKDIR, curl in eine Shell gepipet, ein zweites CMD, das stillschweigend gewinnt, eine JSON-Exec-Form, die kein gültiges JSON ist, sudo in einem Build-Schritt, MAINTAINER und ein COPY ., das den Cache der Dependency-Installation zerstört. Jede Regel hat auf dieser Seite ihren eigenen Unterabschnitt mit Vorher-Nachher-Snippet.
Ist das hadolint?
Nein, und es versucht es auch nicht zu sein. hadolint ist eine Binary, die Sie installieren, und es kombiniert eigene Regeln mit allem, was ShellCheck über die Shell in Ihren RUN-Zeilen sagt — ausgezeichnet in der CI und die richtige Antwort, wenn Sie eine Toolchain vorschreiben können. Dies hier ist der Fall, den hadolint nicht abdeckt: ein Dockerfile, das Sie jetzt geprüft haben wollen, aus dem Browser, auf einer Maschine, auf der Sie nichts installieren dürfen, ohne die Datei an einen Server zu schicken. Siebzehn Regeln statt eines langen Schwanzes, jede mit einem Fix, und weiter unten eine ausdrückliche Liste dessen, was der Linter bewusst nicht meldet.
Verlässt mein Dockerfile jemals meinen Browser?
Nein. Der Parser und alle siebzehn Regeln sind JavaScript, das in Ihrem Tab läuft — es gibt keinen Server, keinen API-Aufruf und kein Logging, also werden 0 Bytes hochgeladen. Das zählt hier mehr als bei den meisten Tools: Dockerfiles enthalten interne Registry-Hostnamen, private Paket-Indizes, Build-Argumente und gelegentlich ein Credential, das niemand hätte committen sollen.
Warum ist apt-get update allein ein Problem?
Weil jedes RUN ein eigenes Layer mit einem eigenen Cache-Eintrag ist. Wenn Sie später nur die Install-Zeile ändern, verwendet Docker das gecachte Update-Layer weiter — es kann Wochen oder Monate alt sein — und führt die Installation gegen diesen veralteten Paket-Index aus. Das Ergebnis ist entweder eine unerwartete Version oder ein 404 für ein Paket, das inzwischen ersetzt wurde. Beides in einem RUN zu verbinden lässt Index und Installation denselben Cache-Eintrag teilen, sodass sie sich nie widersprechen können.
Was ist am :latest-Tag falsch?
Nichts — bis der Publisher pusht. :latest ist ein wandernder Zeiger, und ein FROM ohne Tag bedeutet :latest. Das Basis-Image, das Ihr Build letzten Monat verwendet hat, und das von heute Nacht können also unterschiedliche Major-Versionen sein, ohne dass Sie etwas geändert haben — und nichts in der Datei hält fest, welche Sie tatsächlich getestet haben. Pinnen Sie einen Tag, den Sie getestet haben. Wenn Sie byte-identische Rebuilds brauchen, ergänzen Sie den Digest (@sha256:…). Dieser Linter verlangt einen Tag und nie einen Digest: diese Abwägung bleibt Ihre.
Warum ist CMD ['nginx'] ein Fehler und keine Warnung?
Weil Docker es nicht ablehnt — es tut stillschweigend etwas anderes. Die Exec-Form wird nur verwendet, wenn das Argument als JSON parst, und JSON verlangt doppelte Anführungszeichen. Mit einfachen Anführungszeichen fällt das Ganze auf die Shell-Form zurück, Docker führt also /bin/sh -c "['nginx']" aus, und Klammern und Anführungszeichen werden Teil des Kommandos. Zur Build-Zeit warnt nichts; der Container startet einfach nicht — oder startet eine Shell, die nicht der Prozess ist, den Sie als PID 1 gemeint haben.
Kann er mir sagen, ob ich eine .dockerignore brauche?
Nicht direkt, und er sagt das lieber, als zu raten. Eine .dockerignore ist eine separate Datei, ein Linter, der nur das Dockerfile liest, kann also nicht wissen, ob node_modules, .git und Ihre Build-Artefakte in den Build-Kontext wandern. Was er kann: das Muster erkennen, das eine fehlende .dockerignore teuer macht — ein COPY . vor der Dependency-Installation, das ist DF017 — und den .dockerignore-Hinweis in den Fix-Text dieser Regel legen.
Warum meldet er kein fehlendes EXPOSE oder HEALTHCHECK?
Beides wurde geprüft und bewusst verworfen. EXPOSE ist Dokumentation: es veröffentlicht von sich aus nichts, ein fehlendes bricht also nichts und ein zusätzliches kostet nichts. HEALTHCHECK wird von Kubernetes komplett ignoriert, und dort laufen diese Images in der Praxis — es zu melden würde dieses Publikum darauf trainieren, den Linter zu ignorieren. Die vollständige Liste dessen, was still bleibt, mit Begründung für jeden Punkt, steht im Abschnitt „Was er bewusst nicht meldet“ über den FAQ.
Wie groß darf ein Dockerfile sein?
Bis zu 200.000 Zeichen — grob viertausend Zeilen dichter Text, also zwei Größenordnungen mehr als jedes echte Dockerfile. Darüber verweigert er mit einer Meldung, statt Ihren Tab einzufrieren, denn eine so große Eingabe ist ein Build-Log oder ein gepacktes Archiv, kein Dockerfile. Auch die Funde sind begrenzt, auf 20 pro Regel und 200 insgesamt, und das Panel nennt die Grenze und die echte Anzahl, sobald eine greift.
More free, private DevOps tools.
Der Dockerfile Linter ist ein Tool in OpsCanopy — einem wachsenden Blätterdach browserbasierter Validatoren, Konverter und Tester, die keinen Server berühren.
39 free tools, every one offline-capable — opscanopy.com works with no signup and nothing uploaded.
Verwandt: Docker Run zu Compose verwandelt ein Container-Kommando in eine Service-Datei, Env Example Checker findet die Variablen, die ein Image erwartet, der GitHub Actions Validator und der GitLab CI Validator prüfen die Pipeline, die das Image baut, und der JSON ↔ YAML Konverter hilft, wenn die Konfiguration daneben umgeformt werden muss — oder stöbern Sie im vollständigen Tools-Verzeichnis.
Ohne Gewähr zur Bequemlichkeit bereitgestellt; prüfen Sie kritische Konfiguration immer gegen das Werkzeug, das sie später verarbeitet. OpsCanopy ist kostenlos und offen.