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"]
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.
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.
Nothing else to flag.
No formatting problems, and nothing the rules object to. Worth remembering what that covers: this reads the file you pasted, not the account or cluster it will be applied to.
No finding matches that filter.
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.
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"]
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
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
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.
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.
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.
Not style. The rules here are the ones where getting it wrong costs an afternoon, a CI budget, or a credential.
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.
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.
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.
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.
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.
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 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 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 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"]