Safeguard
Security Guides•

What does a Dockerfile with secure defaults look like, line by line?

A small multi-stage Dockerfile with a pinned base, non-root user, exec form start and health check, explained one decision at a time. Adapt it to your app.

Safeguard Security Team
4 min read

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

LineWhy
Base with tag and digestDocker says digest pinning supports supply chain integrity, at the cost of automatic fixes, so schedule updates
Two stagesBuild tools stay out of the final image
Copy package files firstDependency layer is cached until the lockfile changes
npm ciInstalls exactly the lockfile contents
NODE_ENV productionThe omit setting defaults to dev only in this case
Numeric USERDocker says image user IDs are not deterministic, so be explicit
EXPOSEDocuments the port only, it does not publish it
HEALTHCHECKOne per Dockerfile, exit code 0 healthy, 1 unhealthy
Exec form CMDThe 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

  1. Build the image and run the identity command inside it to confirm it is not root.
  2. Run docker stop and check the shutdown time.
  3. Run docker history with the no truncate option and read it as a stranger would.
  4. Scan the image and read the findings by severity.
  5. 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.