Dockerfile Linter
Lint a Dockerfile against 27 hadolint-compatible rules with line-anchored findings — entirely on this device.
Checks a Dockerfile against 27 hadolint-compatible best-practice rules and anchors every finding to its line — on this device, nothing uploaded. Not affiliated with or endorsed by the hadolint project.
—
Exit code —
Findings appear here after you paste a Dockerfile.
Press ? for shortcuts
Do it offline:$ hadolint Dockerfile
How to use it
- Paste or type a Dockerfile into the editor; the lint re-runs as you type, and an empty editor stays empty rather than guessing at a file.
- Read the findings list top to bottom — each row names the line, the severity, the rule ID and what the rule saw.
- Select a row to jump the editor caret to the offending line; the one-line fix that rule asks for is printed directly under the row.
- Switch to Report (JSON) when you want the same result as machine-readable data to paste into a build log or an issue.
- Check the exit code under the verdict card: 0 means no rule fired, 1 means at least one did, at any severity.
Paste a Dockerfile and this workbench checks it against 27 best-practice rules that carry hadolint-compatible IDs — DL3008, DL3059, DL4006 and the rest — then anchors every finding to the line that caused it. The lint runs in this tab, so the file never leaves the device. Devxcode is not affiliated with or endorsed by the hadolint project.
How it works
The first pass reads the file the way a builder reads it, not the way a regular expression does. Line continuations are joined before any rule runs, and the continuation character follows an `# escape=` parser directive when one is present. Comment and blank lines that fall inside a continuation are dropped, `FROM … AS name` records a stage alias, exec (JSON array) form is distinguished from shell form, and heredoc bodies — `<<EOF`, `<<-EOF`, quoted delimiters — are collected wherever they appear in the file rather than only at the end.
The second pass walks the parsed instructions. Shell-bearing RUN steps are cut into simple commands at `&&`, `;` and `|`, and each segment is word-split by the shared quote-aware tokenizer this repo already uses for its curl and compose tools. Command words are matched on their basename, so `/usr/bin/apt-get update` is examined exactly like `apt-get update`. When a segment cannot be tokenized — an unbalanced quote, usually — the shell-level rules for that one instruction are skipped and a note says so; the rest of the file is still linted.
Each rule that fires produces a record of `{code, line, column, level}` plus a plain-language sentence and the fix the rule wants. Column is always 1: findings anchor to the instruction, not to a character offset inside it. Findings are ordered by line, then by rule-table order, so two rules on the same instruction always print in the same sequence. The severity filter above the list is a view filter only. Any finding at all — including an info-level one such as DL3059 — makes the exit code 1; zero findings make it 0, and the filter never changes that number.
Known differences, stated plainly. This workbench checks 27 best-practice rules plus two shell checks, which is a subset of the upstream rule catalogue rather than all of it, so a clean result here means "none of these 27 fired". Heredocs are parsed anywhere in the file. Shell inspection is deliberately limited to two conservative cases, SC1010 and SC3014, because a false positive from a general shell analyser is worse than a missed one. Rule messages are written for this page; the IDs, severities and exit codes are what stay compatible.
Examples
FROM debian:12
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
CMD ["bash"]
line 2 warning DL3008 An apt-get install package has no version, so rebuilds can pick up a different release.
exit code 1 · 3 instructions · 1 stage · 27 rules checked
A careful-looking apt line still fails one rule: `curl` carries no version, so two builds a month apart can install two different releases. DL3014 (`-y`), DL3015 (`--no-install-recommends`) and DL3009 (the lists cleanup) are all satisfied here, which is why they are absent from the list.
FROM debian:latest
RUN echo step-one
RUN echo step-two
CMD ["bash"]
line 1 warning DL3007 This base image is pinned to latest, so the build changes whenever the tag moves.
line 3 info DL3059 This RUN follows another RUN with nothing between them, so the build makes two layers.
exit code 1 · 4 instructions · 1 stage · 27 rules checked
Two findings at two severities. DL3007 lands on the FROM line, DL3059 on the second of the two adjacent RUN steps — the line you would merge away. The info-level row is enough on its own to make the exit code 1.
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY main.go /src/main.go
RUN CGO_ENABLED=0 go build -o /out/server main.go
FROM alpine:3.19
COPY --from=build /out/server /usr/local/bin/server
USER 65534:65534
ENTRYPOINT ["/usr/local/bin/server"]
No findings.
exit code 0 · 8 instructions · 2 stages · 27 rules checked
A two-stage build that survives every rule in the set. `COPY --from=build` refers to a stage alias, not to an unpinned image, so no tag rule fires against it; the numeric `USER 65534:65534` satisfies both the root rule and the numeric-id rule; and the exec-form ENTRYPOINT keeps DL3025 quiet.
FROM debian:12
RUN curl -fsSL https://example.com/install.sh | sh
CMD ["bash"]
line 2 warning DL4006 This RUN pipes one command into another without pipefail, so a failure on the left is ignored.
exit code 1 · 3 instructions · 1 stage · 27 rules checked
The classic download-and-pipe line. Without `set -o pipefail` the shell reports only the exit status of `sh`, so a 404 on the left of the pipe produces a green build with nothing installed. The rule fires on the pipe, not on the download.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Heredocs are read wherever they sit in the file
A `RUN <<EOF` block in the middle of a Dockerfile does not stop the analysis: the body is collected up to its closing delimiter, the instructions after it are parsed normally, and the body's own commands are still checked as part of that RUN. `<<-EOF` and quoted delimiters are handled the same way.
apt-get upgrade is intentionally silent
The rule that used to complain about `RUN apt-get upgrade` was retired upstream, so it is not implemented here and that line lints clean with exit code 0. Its absence is a deliberate match, not a gap — pinning the base image tag is what governs the package set.
USER nobody is flagged; USER 65534:65534 is not
A non-numeric user name is reported as DL3066 at info level, because the name has to resolve inside the image and a host-side check of the mapping is not possible. The numeric pair `65534:65534` names the same account without the lookup, and produces no finding.
An info finding still exits 1
Severity affects how a row is labelled and whether the filter shows it; it does not affect the exit code. A file whose only finding is DL3059 at info level still reports exit code 1, which is what a CI step that treats a non-zero status as failure will see.
The JSON report drops one field on purpose
Each entry in the Report (JSON) tab carries `code`, `line`, `column` and `level` — the same four fields a JSON-format run of the command-line tool prints per finding — alongside this page's own message text and the fix. There is no `file` field, because nothing is uploaded and there is no path to name.
Above 256 KiB the tool refuses instead of truncating
The cap is 262,144 characters. A larger paste is refused with a typed message that names both the cap and the actual size, rather than being silently cut — a half-linted Dockerfile would report findings that the other half contradicts. Separately, a file that trips more than 200 findings lists the first 200 and says how many there were.
Common errors
This Dockerfile is 262147 characters, over the 262144-character (256 KiB) limit for this tool. Lint a smaller file, or split it.- The refusal names the size it measured and the cap it applies, both in characters. Nothing is linted in this state and no partial list is shown. Splitting a generated multi-stage file at a stage boundary, or linting the stage you are editing, is the usual way through.
This report lists the first 200 findings of 260; fix some and lint again to see the rest.- The list is capped at 200 rows and the note appears only when a 201st finding genuinely went unlisted, so the count it quotes is real. The exit code is still computed from the whole set, and the severity filter is a good way to work through a long list in passes.
Shell-level rules were skipped for the instruction on line 2: its quoting could not be resolved.- An unbalanced quote inside a RUN makes its word boundaries ambiguous, so the rules that read command words are skipped for that one instruction rather than guessed at. Everything else in the file is still linted, and the instruction-level rules still apply to that line.
FAQ
What do rule IDs like DL3008 and SC3014 actually mean?
They are the identifiers the widely used command-line Dockerfile linter assigns to its checks, and this page reuses them so a finding you see here matches the one your CI job reports. DL numbers are Dockerfile best-practice checks; the two SC numbers come from the shell-checking vocabulary and cover shell text written inside RUN.
Which 27 rules are checked?
The Rules checked tab lists every one of them with its severity and the fix it asks for, so the denominator is visible rather than implied. The set spans base-image tagging, package-manager hygiene for apt-get, apk, pip and npm, WORKDIR and COPY paths, CMD and ENTRYPOINT form, layer count, the root user, and pipefail — plus DL1000 for a line that cannot be parsed and the two shell checks.
Why is the exit code 1 when the only finding is info level?
Because the exit code answers "did any rule fire?", not "was it serious?". That keeps the number stable while you use the severity filter, which changes only what the list displays. If you want an info-level finding not to fail a build, exclude that rule ID in your CI configuration rather than relying on the status.
Is this hadolint running in the page?
No. The rule IDs, severities and exit-code behaviour are compatible, and they were calibrated against that tool's own output, but the checks here are an independent TypeScript implementation of a 27-rule subset and the wording of every message is this page's own. Devxcode is not affiliated with or endorsed by the hadolint project.
Is my Dockerfile uploaded anywhere?
No. Parsing and rule evaluation happen in this tab, and the page makes no network request while you lint. That matters more here than on most pages, because ARG and ENV lines routinely hold tokens and registry credentials — which is also why a share link carries only the tab and filter you chose, never the file.
Why did a rule I expected not fire?
Three common reasons: the rule is outside the 27 checked here, the rule was retired upstream and is deliberately not implemented, or the condition really is not met — the layer rule, for instance, wants two adjacent single-command RUN steps and goes quiet as soon as either side chains commands together. The Rules checked tab settles the first case in a second.
Keyboard shortcuts
- /
- Focus the Dockerfile pane
- ⌘/Ctrl + S
- Download the active tab's artifact
- ⌘/Ctrl + C
- Copy the active artifact with an empty selection
- ⌘/Ctrl + Shift + C
- Copy the active artifact
- t
- Cycle the color theme
- ? or ⌘/Ctrl + /
- Open this shortcut list
- Esc
- Close dialogs