A Dockerfile with secure defaults pins its base image, builds in one stage and ships from another, installs dependencies from a lockfile, runs as a numeric non-root user, starts the app in exec form and keeps secrets out. Here is a small example with the reason for each choice.
The file
FROM node:22-bookworm-slim@sha256:REPLACE_WITH_DIGEST AS build
WORKDIR /src
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-bookworm-slim@sha256:REPLACE_WITH_DIGEST
WORKDIR /app
ENV NODE_ENV=production
# COPY with the chown option set to 10001:10001
COPY dist ./dist
USER 10001:10001
EXPOSE 8080
HEALTHCHECK CMD node dist/health.js
CMD ["node", "dist/server.js"]
Replace the placeholder with the digest of the tag you tested, and check that the Node release is still supported before you copy it.
Decision by decision
| Line | Why |
|---|---|
| Base with tag and digest | Docker says digest pinning supports supply chain integrity, at the cost of automatic fixes, so schedule updates |
| Two stages | Build tools stay out of the final image |
| Copy package files first | Dependency layer is cached until the lockfile changes |
| npm ci | Installs exactly the lockfile contents |
| NODE_ENV production | The omit setting defaults to dev only in this case |
| Numeric USER | Docker says image user IDs are not deterministic, so be explicit |
| EXPOSE | Documents the port only, it does not publish it |
| HEALTHCHECK | One per Dockerfile, exit code 0 healthy, 1 unhealthy |
| Exec form CMD | The app receives signals directly |
What is deliberately absent
No secrets in ARG or ENV, since both can be read from the image. If the build needs a private token, use a secret mount for one step. No ADD from URLs. No sudo, which Docker says has unpredictable terminal and signal behavior. No latest tag.
Companion files
A dockerignore file keeps the repository history, local env files and editor folders out of the build context. See what dockerignore keeps out of images. Run the container with a read only root file system and dropped capabilities where your app allows it.
Verify the result
- Build the image and run the identity command inside it to confirm it is not root.
- Run docker stop and check the shutdown time.
- Run docker history with the no truncate option and read it as a stranger would.
- Scan the image and read the findings by severity.
- Rebuild next week with a fresh base and compare.
Adapting it
For Python, install from a hashed lock file into a virtual environment in the build stage and copy it forward. For compiled languages, copy the binary into a minimal runtime stage. The pattern is the same: build in one place, ship from another, run with the least privilege that works.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
Is this enough to call an image secure?
No. It is a hygiene baseline. You still need dependency scanning, base image updates and run time controls.
Do I need the HEALTHCHECK if my orchestrator has probes?
Often not. Use whichever your platform honors, and keep one source of truth.
Safeguard
Safeguard does SBOM and dependency scanning, and its policies and gates can run in CI so a bad image does not reach production. You can try a free scan before deciding anything.
Sources
Last reviewed: 19 October 2026.