Skip to main content

Dockerfile Linter

Heuristic best-practice checks: layer order, cache busting, root user, image size.

100% client-side⌁ nothing leaves your browser⎘ instant results
Linted locally

Heuristic checks, not a full parser — multi-stage builds and heredocs are handled line-by-line. For exhaustive linting in CI, pair with hadolint.

0/100

12 findings: 3 high, 4 medium, 5 low sorted by line below.

  • highline 1unpinned-base

    Base image "node:latest" is not pinned to a version tag or digest.

    Fix: Pin a specific tag (node:22-slim) or digest (@sha256:…) so builds are reproducible and upgrades are deliberate.

  • highline 2secret-in-env

    Possible secret in ENV — values are baked into image layers and visible via docker history.

    Fix: Use BuildKit secret mounts (RUN --mount=type=secret) at build time and runtime injection (env vars, secret managers) for runtime.

  • highline 9root-user

    No USER instruction — the container runs as root.

    Fix: Create an unprivileged user (RUN useradd -r app) and add USER app before CMD/ENTRYPOINT.

  • mediumline 5cache-busting

    COPY . . appears before dependency installation — any source change invalidates the dependency layer cache.

    Fix: Copy the manifest first (COPY package*.json ./), run the install, then COPY . . afterwards.

  • mediumline 6apt-update-alone

    apt-get update in its own RUN layer gets cached separately from install, causing stale package indexes.

    Fix: Combine: RUN apt-get update && apt-get install -y --no-install-recommends … && rm -rf /var/lib/apt/lists/*

  • mediumline 7apt-recommends

    apt-get install without --no-install-recommends pulls in unneeded packages.

    Fix: Add --no-install-recommends to keep the image small and the attack surface low.

  • mediumline 7apt-cleanup

    apt lists are not cleaned in the same layer — they stay in the image forever.

    Fix: Append && rm -rf /var/lib/apt/lists/* in the same RUN. Cleanup in a later layer does not shrink the image.

  • lowline 3add-vs-copy

    ADD used where COPY suffices — ADD's auto-extraction and URL fetching are surprising behaviors.

    Fix: Use COPY for local files. Reserve ADD for tar auto-extraction only.

  • lowline 4add-vs-copy

    ADD used where COPY suffices — ADD's auto-extraction and URL fetching are surprising behaviors.

    Fix: Use COPY for local files. Reserve ADD for tar auto-extraction only.

  • lowline 5dockerignore-hint

    COPY . . copies everything not excluded — node_modules, .git and .env files ride along without a .dockerignore.

    Fix: Maintain a .dockerignore listing .git, node_modules, .env*, and build artifacts.

  • lowline 7run-chains

    3 separate RUN instructions — each creates a layer; related commands can be merged.

    Fix: Chain related commands with && in one RUN (or use heredoc syntax) to reduce layers and enable same-layer cleanup.

  • lowline 9no-healthcheck

    No HEALTHCHECK — orchestrators can only see whether the process exists, not whether it works.

    Fix: Add e.g. HEALTHCHECK --interval=30s CMD curl -f http://localhost:3000/health || exit 1 (or rely on Kubernetes probes and document that).

100%
client-side compute
0
uploads — verify in devtools
96
free tools in the directory
0
network requests per keystroke

How it works

Most Dockerfile problems are invisible until they hurt: the image that grew to two gigabytes, the build that takes eleven minutes because the cache never hits, the container running as root when the CVE lands, the API key recoverable by anyone who can pull from your registry. This linter runs a set of heuristic best-practice rules over any Dockerfile you paste — entirely in your browser — and reports each finding with a severity, the offending line number, and a concrete fix.

The rule set targets the failure modes with real production consequences. High severity covers security: base images not pinned to a tag or digest, missing USER (containers run as root by default, and a container escape from root is a much worse day), and credential-shaped values in ENV or ARG — which persist in image metadata and layer history where docker history reads them back. Medium severity covers build hygiene: apt-get without --no-install-recommends or same-layer cleanup, a lone apt-get update cached separately from its install, and the classic COPY . . placed before dependency installation.

That last rule deserves the emphasis the score gives it. Docker's layer cache invalidates everything after the first changed instruction, so copying your whole source tree before npm install means every source edit reinstalls every dependency. Reordering — manifest first, install, then the rest of the source — routinely cuts CI build times from minutes to seconds, and the linter detects the antipattern across npm, yarn, pnpm, pip, bundler, Go and cargo install commands.

The score out of 100 weights findings by severity, so one leaked secret outweighs a handful of stylistic layer merges. The prefilled example is deliberately flawed — it trips nearly every rule — so you can see the full diagnostic format before pasting your own. The tool is honest about being heuristic rather than a complete parser: multi-stage builds are checked line-by-line, exotic heredoc syntax may be skipped, and for exhaustive CI enforcement it recommends hadolint as the complementary next step. Nothing you paste leaves the page.

Frequently asked questions

Why is FROM node:latest flagged as high severity?

Because :latest makes your build non-reproducible: the same Dockerfile builds different images on different days, and a base-image major version bump lands in production without anyone deciding it should. Pinning a specific tag (node:22-slim) makes upgrades deliberate; pinning a digest (@sha256:…) makes builds bit-for-bit reproducible and immune to tag hijacking. The same applies to an image with no tag at all, which implicitly means :latest.

What is the COPY . . cache-busting problem?

Docker caches each layer and invalidates everything after the first changed instruction. If you COPY the entire source tree before running npm install, then every source edit — a one-line CSS change — invalidates the copy layer and forces a full dependency reinstall on the next build. The fix is ordering: copy only the dependency manifest first (package.json and the lockfile), install, then copy the rest. Dependency layers then survive across builds until the manifest itself changes.

Why are secrets in ENV and ARG dangerous?

Both bake the value into image metadata. ENV values persist in the final image and appear in docker inspect; ARG values are recorded in the layer history and recoverable via docker history. Anyone who can pull the image can read them — registries, CI caches, and teammates included. Build-time secrets belong in BuildKit secret mounts (RUN --mount=type=secret), which expose the value only during that single RUN step, and runtime secrets belong in injected environment variables or a secrets manager.

Why must apt cleanup happen in the same RUN layer?

Image layers are append-only: a file deleted in a later layer is hidden, not removed, and the bytes still ship in the image. Running rm -rf /var/lib/apt/lists/* as its own RUN after the install shrinks nothing. The cleanup must be chained into the same RUN as apt-get update && apt-get install so the package indexes never get committed to any layer. The same logic applies to build caches, temporary downloads and extracted archives.

Is this a replacement for hadolint?

No — it is a fast, zero-install first pass. This linter implements roughly a dozen high-impact heuristic rules covering the failures that actually cause production incidents: unpinned bases, root users, leaked secrets, broken caching and bloated layers. hadolint is a full parser with shellcheck integration and dozens of additional rules, and belongs in your CI pipeline. Use this to triage a Dockerfile in ten seconds; use hadolint to enforce policy on every commit.

Built by FORG — AI cost observability for agentic coding. Free tools, no signup, nothing leaves your browser.

Learn about FORG