Container tooling works best as a short feedback loop: lint the Dockerfile, build it, inspect its layers, then run the resulting image. A generated Dockerfile can get you started; it cannot know your build context, runtime user, or package manager cache.
The four checks that catch different problems
| Check | Finds | Does not prove |
|---|---|---|
| Dockerfile lint | Suspicious latest, shell quoting, missing cleanup | Application correctness |
docker build | Missing files, bad stages, broken dependencies | Small layers |
docker history | Large or duplicated layers | Runtime security |
docker run | Entrypoint and port mistakes | Production resource limits |
Keep build tools out of runtime
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
This is not automatically optimal, but it makes the boundary visible: compilers and source files belong in build; only runtime artefacts cross into the final stage.
Image size is a signal, not a security score. A tiny image running as root with an exposed debug port is still a bad deployment.
Inspect what the registry will receive
docker image ls my-service
docker history --no-trunc my-service:latest
docker run --rm --read-only -p 3000:3000 my-service:latest
Treat a surprising layer as a review item. Common offenders are a missing .dockerignore, copying node_modules, and package-manager caches left behind. The best container workflow makes those mistakes visible before a scanner or a production incident has to.
