Dockerfile Linter
DevOps engineers reviewing a teammate's Dockerfile, platform teams standardizing base images, and security reviewers checking for hardcoded secrets all need a fast second opinion before a build ships to the registry. This linter checks base image tags, ADD versus COPY, root user, apt cleanup, pip version pins, ENV secrets, and more. Everything runs locally, so internal registry paths and proprietary build steps never leave the browser.
How the Linter Parses Dockerfile Instructions
The linter reads the file line by line, joining continuation lines that end with a backslash into a single logical instruction. Comments and blank lines are skipped. Each instruction is matched against a known set of Dockerfile keywords and dispatched to a rule handler that inspects its arguments. The handlers track state such as the number of FROM instructions, whether a USER instruction appears, and which ENV keys have been set, so cross-instruction checks work correctly.
Rule checks are pattern based: FROM references are split on colons to extract the tag, ADD arguments are tested for remote URL prefixes, RUN arguments are scanned for apt-get and pip subcommands, and ENV arguments are matched against a key-value regex. Findings carry a severity of error, warning, or info so you can prioritize blocking issues over suggestions.
Severity levels
Errors cover reproducibility and security: FROM latest, ADD remote URLs, USER root, and ENV secrets. Warnings cover image size and best practices: missing --no-install-recommends, unpinned pip installs, npm install instead of npm ci, and duplicate ENV keys. Info items are suggestions such as adding a HEALTHCHECK.
Multi-stage awareness
Each FROM starts a new stage context. Rules that depend on stage state, such as the presence of a USER instruction, reset at each FROM. COPY --from references are recognized and skipped for the ADD-versus-COPY heuristic, so multi-stage builds do not produce false positives.
Before and after: fixing a bloated Dockerfile
FROM node:latest
MAINTAINER dev@example.com
WORKDIR /app
RUN apt-get update
RUN apt-get install -y curl
RUN pip install flask
ENV API_KEY=abc123secret
ADD https://example.com/data.tar.gz /tmp/
COPY . .
CMD ["node", "server.js"] FROM node:20.11-alpine
LABEL org.opencontainers.image.source="https://github.com/example/app"
WORKDIR /app
RUN apk add --no-cache curl
RUN pip install --no-cache-dir flask==3.0.2
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN wget -qO /tmp/data.tar.gz https://example.com/data.tar.gz
USER app
EXPOSE 3000
HEALTHCHECK CMD wget -qO- http://localhost:3000/ || exit 1
CMD ["node", "server.js"] Use Cases
| Developer | Production | Workflow |
|---|---|---|
| Check a new service Dockerfile before the first commit | Block FROM latest in a registry policy check | Paste a teammate's PR Dockerfile for review |
| Verify pip and npm pinning in a data science image | Scan for hardcoded API keys before a public push | Standardize base image tags across microservices |
| Confirm multi-stage builds copy only needed artifacts | Audit image size contributors in a build pipeline | Onboard a new engineer to Dockerfile conventions |
Common Mistakes and Troubleshooting
The most common false positive is the pip-unpinned warning on pip install -r requirements.txt. The linter cannot inspect the requirements file, so it flags any pip install without == as a warning. If your requirements file pins versions, ignore that specific warning or inline the pins directly. The second common issue is apt-get update without install in the same RUN: the linter warns because the update layer is cached separately and goes stale.
If a FROM latest error points at a line that uses a digest reference such as image@sha256:abc..., the linter accepts digests as fully pinned and does not flag them. For private registry images that include a port or namespace, the tag extraction still works because the split is on the last colon, not the first.
Best Practices for Dockerfiles
Pin every base image to a specific version and variant. Use COPY for local files and RUN curl for remote downloads. Always end apt-get install chains with rm -rf /var/lib/apt/lists/* and pass --no-install-recommends. Pin pip and npm dependencies. Add a non-root USER before CMD, and include a HEALTHCHECK so orchestrators can detect unhealthy containers. Never hardcode secrets in ENV; use build secrets or runtime injection instead.
Run this linter in CI alongside hadolint for defense in depth. Pair it with a Dockerfile generator for boilerplate and a docker-compose generator for multi-service stacks. For image size optimization, compare the linter's findings with a registry scanner that reports layer sizes.
Dockerfile Linter FAQ
What rules does this Dockerfile linter check?
The linter runs checks for FROM tags that resolve to latest, ADD with remote URLs, USER root, apt-get without --no-install-recommends, apt lists not cleaned, pip installs without version pins, npm install instead of npm ci, ENV variables that look like secrets, duplicate ENV keys, missing HEALTHCHECK, missing USER, and deprecated MAINTAINER instructions. Each finding carries a severity and a plain-language fix.
How is this different from hadolint?
Hadolint is the standard Dockerfile linter but it is a Go binary you must install and run in a terminal or CI runner. This tool runs in the browser with zero setup, which makes it useful for quick checks during development, code reviews of pasted Dockerfiles, and teams that cannot install binaries on managed laptops. It covers the highest-impact hadolint rules with the same DL-style identifiers where applicable.
Why is FROM node:latest flagged as an error?
The latest tag is mutable: the image content can change between builds without any change to your Dockerfile. That breaks reproducibility, makes rollbacks unreliable, and can silently introduce breaking changes. Pin a specific version and variant such as node:20.11-alpine so the same bytes build every time.
Why should I use COPY instead of ADD?
ADD has two extra behaviors that COPY does not: it can fetch remote URLs and auto-extract local tar archives. Both behaviors are surprising in a Dockerfile because they make the build non-obvious and the cached layer invalidation harder to reason about. Use COPY for files, and use RUN curl or wget when you genuinely need a remote fetch.
Does the linter detect hardcoded secrets in ENV?
Yes. ENV keys whose names match password, secret, token, api_key, private_key, or access_key are flagged as errors when they carry a non-empty value. Real credentials should come from build secrets, runtime environment injection, or a secrets manager, never from a Dockerfile that ends up in a public or shared registry.
Can I lint a multi-stage Dockerfile?
Yes. The linter walks every instruction including multiple FROM lines. Stage aliases such as FROM builder AS build are parsed, and checks like apt cleanup and pip pinning apply per stage. The USER check looks for any non-root USER instruction in the final stage.
Is my Dockerfile uploaded to a server?
No. The entire lint pass runs in the browser tab. Proprietary base images, internal registry paths, and build arguments stay on your machine. There is no account, no history, and no network request in the linting path.
Related Tools
Generate a clean Dockerfile from scratch
Build multi-service compose files
Generate .gitignore and .dockerignore patterns
Validate environment files used by Docker builds
Build IAM policies for container registries
Compute file permission bits for COPY --chmod