Skip to content
koducathome

Dockerfile Linter

Check a Dockerfile against documented best-practice rules for image size, caching, reproducibility and security.

How to use this tool

Steps for using the Dockerfile Linter.

  1. Paste your Dockerfile into the editor, or load Sample with issues or Clean sample.
  2. Click Lint Dockerfile. The file is parsed in your browser — continuation lines, comments, parser directives, heredocs and multi-stage builds are understood.
  3. Read each finding: its severity, rule ID (DF001–DF022), line number, what is wrong and how to fix it.
  4. Use the severity filter to focus on errors and warnings first, and the summary counts to track progress.
  5. Fix the Dockerfile, lint again until you see No issues found, and Copy or Download the report for a code review.

Example

A typical first-draft Dockerfile

Dockerfile
FROM ubuntu
RUN apt-get update
RUN apt-get install -y curl
USER root
Findings
Line 1: [WARNING] DF003 Base image uses latest or no tag — "ubuntu" has no tag, so Docker uses latest.
  Fix: Pin a specific version tag, e.g. FROM node:22-alpine or FROM python:3.12-slim.
Line 2: [WARNING] DF007 apt-get update in a separate RUN — apt-get update runs without an install in the same RUN, so its cached layer goes stale.
  Fix: Combine them: RUN apt-get update && apt-get install -y … && rm -rf /var/lib/apt/lists/*.
Line 3: [INFO] DF006 apt-get install without --no-install-recommends — By default apt also installs recommended packages, which often adds tens of megabytes you do not need.
  Fix: Use apt-get install -y --no-install-recommends <packages>.
Line 3: [INFO] DF008 apt package lists not removed — apt-get update downloads package indexes into /var/lib/apt/lists. Leaving them in the layer adds size without benefit at runtime.
  Fix: End the same RUN with && rm -rf /var/lib/apt/lists/*.
Line 4: [WARNING] DF013 Container runs as root — The last USER in the final stage is "root".
  Fix: Create an unprivileged user and add USER <name> (or USER 10001) near the end of the final stage.

Common use cases

  • Reviewing a Dockerfile in a pull request without installing anything locally.
  • Shrinking an image by finding missing cache cleanups and unnecessary recommended packages.
  • Hardening a container before production: non-root user, pinned base images, no secrets in ENV.
  • Teaching Dockerfile best practices with concrete, line-by-line feedback.
  • Spotting why a container ignores docker stop and is killed after the timeout (shell-form CMD).

What the Dockerfile linter checks

The linter first splits the file into instructions the way Docker does: parser directives such as # syntax= and # escape= are read from the top, lines ending in the escape character (\, or a backtick on Windows) are joined, comment lines inside continuations are dropped, heredoc bodies (RUN <<EOF … EOF) are attached to their instruction, and instruction names are case-insensitive. Each FROM starts a new stage, and FROM … AS name registers a stage name so later FROM name and COPY --from=name references are not mistaken for images.

Errors — DF001 unknown instruction (often a typo or a missing \ on the previous line). DF002 FROM missing or not first (only ARG may precede it). DF022 invalid EXPOSE port (must be 1–65535, optionally a range and /tcp, /udp or /sctp).

Warnings — DF003 base image with no tag or latest. DF005 apt-get install without -y, which stops the build at a prompt. DF007 apt-get update in a RUN without an install, so the cached index goes stale. DF009 apk add without --no-cache. DF011 ADD for plain local files where COPY is clearer. DF013 the final stage runs as root (no USER, or the last USER is root/0). DF014 CMD/ENTRYPOINT in shell form, where /bin/sh becomes PID 1 and does not forward SIGTERM. DF015 more than one CMD or ENTRYPOINT in a stage — only the last counts. DF016 cd inside RUN instead of WORKDIR. DF017 relative WORKDIR. DF018 sudo in RUN. DF020 deprecated MAINTAINER. DF021 ENV or ARG names that look like secrets (PASSWORD, SECRET, TOKEN, API_KEY, PRIVATE_KEY, ACCESS_KEY, CREDENTIALS), which end up readable in the image or its history.

Info — DF004 base image pinned by tag but not by @sha256: digest. DF006 apt-get install without --no-install-recommends. DF008 apt lists not removed with rm -rf /var/lib/apt/lists/* (skipped when a BuildKit cache mount is used). DF010 pip install without --no-cache-dir. DF012 ADD with a remote URL or archive — fine when intended, but verify downloads with --checksum. DF019 COPY . . (or any copy of the whole context) before installing dependencies, which defeats layer caching. The rules are inspired by widely used best practices from Docker's documentation and hadolint, but the IDs and wording are this tool's own.

Limitations

  • Checks are heuristic and pattern-based. This is not a replacement for hadolint, a real docker build or an image vulnerability scanner.
  • Complex RUN scripts are not parsed as shell: commands are split on &&, ||, ;, | and newlines, so unusual quoting or functions can cause false positives or misses.
  • Variables ($VAR) in image names, ports and paths are not expanded; those values are skipped.
  • The root-user check cannot see the base image's default user — images such as distroless nonroot already run unprivileged.

Frequently asked questions

Is this the same as hadolint?

No. It runs a smaller set of 22 documented rules in your browser with its own IDs. hadolint parses shell scripts with ShellCheck and has many more rules; use it in CI for full coverage.

Why should I avoid the latest tag?

latest points to whatever was pushed most recently, so the same Dockerfile can produce different images on different days. Pin a version tag, and a digest if you need fully reproducible builds.

Why is shell-form CMD a problem?

CMD npm start runs as /bin/sh -c "npm start". The shell becomes PID 1 and does not forward SIGTERM, so your app never gets a chance to shut down gracefully. CMD ["npm", "start"] runs the process directly.

Is my Dockerfile uploaded?

No. Parsing and linting run entirely in your browser; nothing is sent to a server.

How do I pass secrets during a build safely?

Use BuildKit secret mounts: RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci with docker build --secret id=npmrc,src=.npmrc .. The secret is never written to a layer. Keep runtime secrets in your orchestrator; the Env Formatter can help prepare them.