Dockerfile Linter: Common Best Practices, Checked Instantly
A working Dockerfile isn't necessarily a good one — unpinned base images, shell-form CMD that swallows signals, and unnecessary root access are all mistakes that build successfully but cause problems later. This linter checks your Dockerfile against the practices that matter most for reproducibility, image size, and container security.
Only ARG is allowed to appear before the first FROM.
Reproducibility: Pin Your Base Images
FROM node (no tag) or FROM node:latest both resolve to whatever :latest happens to point to at build time — which changes. A build that worked last week can fail today with no code changes. Pinning to a specific version (FROM node:20-alpine) makes builds reproducible and debuggable.
Layer Efficiency
Every RUN, COPY, and ADD instruction creates a new layer in the image. Several consecutive RUN commands that could be one logical operation (like updating and installing packages) bloat the image with intermediate layers. Combining them with && — and cleaning up caches in the same layer — keeps the final image smaller.
Signal Handling: Shell Form vs. Exec Form
CMD npm start runs through a shell (/bin/sh -c '...'), and that shell process becomes PID 1, not your actual app. Signals like SIGTERM sent by 'docker stop' go to the shell, which may not forward them, causing Docker to wait out the full stop timeout before force-killing. CMD ["npm", "start"] (exec form) runs your app directly as PID 1, so it receives signals correctly.
Practical Examples
Fixing an Unpinned Base Image
A common reproducibility issue.
- 1.FROM python
- 2.Warning: no tag — defaults to :latest
- 3.Fix: FROM python:3.12-slim
Combining Package Install Steps
Reducing layers and avoiding stale caches.
- 1.Before: RUN apt-get update, then a separate RUN apt-get install -y curl
- 2.Warning: apt-get install without apt-get update in the same RUN
- 3.After: RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
What Gets Checked
- FROM must be the first real instruction
- Image tags: flags missing tags and :latest
- Layer efficiency: consecutive RUN instructions
- CMD/ENTRYPOINT form: shell vs. exec
- USER: warns if the image never drops root
- EXPOSE: validates port syntax
Good Use Cases
- Reviewing a Dockerfile before merging a PR
- Learning Docker best practices with immediate feedback
- Auditing an inherited Dockerfile for security hardening gaps
- Quick sanity check before a CI pipeline build
Frequently Asked Questions
Does this replace hadolint or Docker's own build warnings?
No — hadolint is a much more thorough, spec-complete Dockerfile linter used in CI pipelines. This tool covers the most common, high-value checks (image pinning, layer combining, exec form, root user) for a quick browser-based review, not a full replacement.
Why does it flag uppercase instructions?
Dockerfile instructions (FROM, RUN, COPY, etc.) are case-insensitive, but the convention is to write them uppercase to visually distinguish them from arguments — Docker's own documentation and most style guides recommend this.
Why combine multiple RUN instructions?
Each RUN instruction creates a new image layer. Chaining commands with && inside a single RUN keeps related steps (like apt-get update && apt-get install) in one layer, so a later step's cache invalidation doesn't leave a stale intermediate layer, and the final image has fewer, smaller layers.
Why prefer exec form for CMD and ENTRYPOINT?
Shell form (CMD npm start) runs the command through /bin/sh -c, which becomes PID 1 and doesn't forward signals like SIGTERM to your actual process — so `docker stop` can hang until a timeout. Exec form (CMD ["npm", "start"]) runs your process directly as PID 1.
Why does it warn about running as root?
Without a USER instruction, a container runs as root by default. If an attacker compromises the process, they have root access inside the container, which increases the blast radius of any container-escape vulnerability. Adding a dedicated USER is a standard hardening step.
Is my Dockerfile sent anywhere?
No, all linting happens locally in your browser via plain text/regex analysis — no build, no Docker daemon, no upload.