Dockerfile Linter

Paste a Dockerfile and get the problems that show up later: images that cannot be reproduced, containers running as root, credentials baked into a layer you cannot remove, and the one-character mistake that stops your application ever receiving a shutdown signal.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Lint. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

A secret baked into the image

ENV values are in the image layers forever, readable by anyone who can pull it

FROM alpine:3.19
ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
CMD ["/bin/sh"]

:latest and root

An unpinned base and no USER, which is the default state of most first-draft Dockerfiles

FROM node:latest
RUN npm install
CMD npm start

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Putting a credential in ENV or ARG

    Both end up in the image layers and in `docker history`. Deleting the file in a later layer does not remove it, because layers are additive.

    Instead:Use BuildKit secret mounts for build time, and inject runtime values from the orchestrator.

  2. Using :latest as a base

    The build is not reproducible, and a rebuild months later can pull a different major version. It also defeats any image scanning that pins to a digest.

    Instead:Pin to a specific tag, and to a digest where the registry supports it.

  3. Leaving the container running as root

    Without a USER directive the process is root inside the container, which matters the moment a volume is mounted or a kernel escape exists.

    Instead:Create a user and switch to it before CMD.

What it checks

Not style. The rules here are the ones where getting it wrong costs an afternoon, a CI budget, or a credential.

The signal-handling bug nobody knows about

CMD written as a plain string is wrapped in /bin/sh -c, so sh becomes PID 1 and your process is a child of it. sh does not forward SIGTERM. docker stop and Kubernetes both wait the entire grace period and then kill the container, so in-flight requests are dropped and cleanup never runs. The JSON array form fixes it and almost nobody knows to look.

Credentials that survive deletion

Every layer is kept. A password set in ENV, or passed as ARG, is readable with docker history by anyone who can pull the image, and removing it in a later instruction does not remove it from the earlier layer. Any secret-looking name with a real value is reported as critical, because if it has been pushed it has leaked.

Layer ordering that costs CI minutes

Copy the whole context and then install dependencies, and any change to any source file invalidates the install layer. That is a full dependency install on every commit, forever. The tool spots the ordering and shows the fix, which is usually two lines moved.

Reproducibility

An untagged base image means :latest, so two builds a week apart can differ and the one that broke is not the one you tested. apt-get upgrade has the same effect from the other direction. Both are flagged with what to pin instead.

How a Dockerfile becomes an image

Each instruction produces a layer, and layers are stacked and cached. Almost every Dockerfile problem worth knowing about follows from that one fact, including why deleting a file does not shrink the image, and why the order of your instructions decides how long your builds take.

Layers are immutable and additive

A layer records the filesystem changes made by one instruction. Deleting a file in a later layer marks it absent, but the bytes are still in the earlier layer and still in the image you push. This is why cleanup has to happen in the same RUN that created the mess, and why a secret written in one instruction cannot be removed by the next.

RUN wget big.tar.gz && tar xf big.tar.gz    layer 1: +200 MB
RUN rm big.tar.gz                          layer 2: marks it gone

the image is still 200 MB larger

RUN wget big.tar.gz && tar xf big.tar.gz && rm big.tar.gz
                                           one layer, nothing left behind

The cache invalidates forward, never backward

The build walks the instructions and reuses a cached layer while the instruction and its inputs are unchanged. The first thing that differs invalidates that layer and every layer after it. This is the whole reason to copy a dependency manifest and install before copying source: the manifest changes rarely, the source changes constantly, and putting them in that order is the difference between a five-second and a five-minute build.

COPY package*.json ./     changes rarely  -> cache holds
RUN npm ci                changes rarely  -> cache holds
COPY . .                  changes always  -> invalidated here
RUN npm run build         rebuilt, correctly

Shell form and exec form are genuinely different

Written as a string, CMD and ENTRYPOINT are wrapped in /bin/sh -c, so the shell is PID 1 and your process is its child. PID 1 has special duties: it receives the signals and it reaps orphans. sh does neither on your behalf, so SIGTERM never reaches your application and it dies to SIGKILL after the grace period. Written as a JSON array, your process is PID 1 and gets the signal directly.

CMD node server.js            -> /bin/sh -c "node server.js"
                                 sh is PID 1, node never sees SIGTERM

CMD ["node", "server.js"]     -> node is PID 1, receives SIGTERM

variable expansion needs a shell; if you need both, use an init like tini

Multi-stage builds exist to leave things behind

A second FROM starts a new stage with a clean filesystem, and you COPY only the artefacts you want out of the previous one. Compilers, build tools, source code and package caches stay behind. This is the difference between shipping a 1.2 GB image containing your entire toolchain and a 40 MB one containing a binary, and it is also a security boundary, since what is not in the image cannot be exploited in it.

FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN go build -o /app

FROM gcr.io/distroless/static      no shell, no package manager
COPY --from=build /app /app        only the binary crosses over
USER nonroot
ENTRYPOINT ["/app"]

More docker tools

Docker Container Image Reference Parser Is that a registry or a Docker Hub user? docker run to Kubernetes YAML And the flags that have no equivalent Docker Compose to Kubernetes And why depends_on has no equivalent Docker Logging Driver Generator json-file never rotates Dockerfile Non-Root USER Generator Numeric, or Kubernetes rejects it Docker OCI Image Labels Generator One of them actually does something Docker Hub Pull Rate Limit Calculator Counted per IP, not per person Docker Registry Storage Calculator Deleting a tag frees nothing Docker Build Context Analyzer All of it uploads before anything runs Docker Compose to docker run Converter Names stop resolving Docker Compose Version Migrator version: is obsolete now Docker Compose Network and Port Viewer Everything reaches everything Docker Command Explainer Which flags hand over the host Docker BuildKit Cache Mount Generator apt deletes the cache you just mounted Docker BuildKit Secret Mount Generator ARG is in docker history forever Docker .dockerignore Pattern Tester *.log does not match logs/app.log Docker Image Manifest Viewer unknown/unknown is not broken Docker Image Config Viewer Anyone who can pull can read your Env Docker Registry Token Decoder Check the scope before the credentials Docker HEALTHCHECK Generator Docker does nothing when it fails Docker Network Subnet Calculator 31 networks, then it stops Docker History Analyzer The rm did not save anything Docker Run to Compose Converter Some flags have no equivalent Docker CMD and ENTRYPOINT Tester Why docker stop takes ten seconds Docker Port Mapping Tester -p reaches past your firewall Docker -v to --mount Converter -v invents missing directories Docker Restart Policy Simulator The backoff resets after ten seconds Docker Exit Code Explainer 137 is two different problems Docker CPU Limit Converter --cpu-shares limits nothing Docker Memory Limit Calculator -m 512m allows a gigabyte Docker Compose Interpolation Tester An unset variable is an empty string Docker Environment Variable Precedence Tester Your shell does not reach the container Docker Compose Override Merge Previewer An override can add a port, never remove one Dockerfile Multi-Stage Build Analyzer Which stages reach the image Dockerignore Validator It is not .gitignore

Elsewhere on the site