PS Product SecurityKnowledge Base

๐Ÿณ Dockerfile Security Best Practices

Intro: A Dockerfile is not just packaging. It is a repeatable security decision about what enters runtime, how deterministic the build is, which privileges remain, and whether the resulting image can be trusted and patched without chaos.

What this page includes

  • a stronger baseline Dockerfile
  • reproducibility and provenance guidance
  • patch and rebuild strategy
  • practical review questions for runtime safety

Principles that matter most

  1. use multi-stage builds;
  2. start from a trusted, intentionally chosen base image;
  3. pin versions where determinism matters;
  4. run as a non-root user;
  5. keep the runtime image minimal;
  6. keep secrets out of image layers;
  7. pair Dockerfile review with scan, signing, and promotion policy.

Strong baseline example

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS build

WORKDIR /build
COPY requirements.txt .

# Use secret mount so credentials do not land in layers.
RUN --mount=type=secret,id=pipconf,target=/etc/pip.conf \
    pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

COPY app/ /build/app/

FROM python:3.12-slim AS runtime

# Create a dedicated low-privilege runtime user.
RUN groupadd --gid 10001 app && \
    useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin app

WORKDIR /app
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY --chown=10001:10001 app/ /app/

USER 10001:10001
CMD ["python", "main.py"]

What this baseline is doing well

  • keeps compilers and build tooling out of runtime;
  • uses a separate runtime user;
  • avoids baking build secrets into layers;
  • copies only the runtime payload;
  • keeps the final image smaller and easier to scan.

Deterministic build guidance

A Dockerfile should get as close as possible to reproducible builds.

Good habits:

  • do not use floating latest unless you truly want drift;
  • pin the base image tag intentionally;
  • pin language package versions where practical;
  • record image labels for source revision, build date, and owner;
  • generate an SBOM in CI;
  • sign the promoted image, not just the source repo.

Useful labels

LABEL org.opencontainers.image.title="payments-api"
LABEL org.opencontainers.image.source="https://git.example.com/payments/api"
LABEL org.opencontainers.image.revision="${VCS_REF}"
LABEL org.opencontainers.image.version="${APP_VERSION}"

.dockerignore is part of the security story

.git
.env
.env.*
*.pem
*.key
*.p12
node_modules/
__pycache__/
dist/
build/
coverage/

This reduces both accidental leakage and noisy build context.

Build secrets: what not to do

Do not do this:

ARG NPM_TOKEN
ENV NPM_TOKEN=${NPM_TOKEN}

That pattern is easy to misuse and often leaks sensitive material into image history or CI logs. Prefer runtime secret injection or secret mounts during build.

Patch and rebuild strategy

Images should be treated as replaceable artifacts, not hand-patched snowflakes.

Recommended update loop:

  1. identify the vulnerable base image or package;
  2. rebuild dependent images without cache where required;
  3. rescan the rebuilt image;
  4. republish with new digest;
  5. redeploy the workload;
  6. remove or stop promoting the old image.

Review questions that catch real problems

  • does the runtime image contain package managers, shells, or admin tooling that production does not need?
  • could the application run without root if the image were built correctly?
  • does the image rely on mutable behavior at startup that should have been done at build time?
  • are secrets, certificates, or tokens copied into the image or injected at runtime?
  • is the image signed, scanned, and promoted through a controlled registry path?

Bad vs better patterns

Bad

FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]

Problems:

  • floating base image;
  • full-repo copy too early;
  • dependency and source layers mixed together;
  • likely root runtime;
  • weak build determinism.

Better

FROM node:20-bookworm-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:20-bookworm-slim AS runtime
RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin app
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=10001:10001 . .
USER 10001
CMD ["node", "server.js"]

Top review checks

  1. no floating tag without explicit justification;
  2. no root runtime user;
  3. no secrets in ARG, ENV, or copied files;
  4. no oversized runtime image with build tools left behind;
  5. no package-manager cache or unnecessary shell tooling left in runtime;
  6. .dockerignore present;
  7. labels and traceability present;
  8. rebuild and patch strategy defined;
  9. image scan in CI;
  10. signing and promotion controls in place.

Footer

Where Docker security tools fit

A Docker security program is easier to operate when each tool has a clear stage.

Stage Question Typical tools
Dockerfile review is the image built safely? Hadolint, code review, policy checks
Image composition what packages and layers are inside? Syft, docker history, registry metadata
Known-vuln scan which known issues exist now? Trivy, Grype, registry-native scanners
Configuration / best-practice review are there unsafe image defaults? Dockle-style checks, custom policy, admission rules
Host and runtime review are the engine and runtime safe? Docker Bench, runtime detection, host hardening controls

Practical rule of thumb

  • use Hadolint or review guidance for Dockerfile structure;
  • use Syft when you want stable component evidence;
  • use Trivy or Grype for vulnerability evaluation;
  • use a registry policy or admission control for promotion decisions;
  • keep host and daemon hardening separate from image scanning.

Compliance and baseline checks for images

Security teams sometimes ask whether an image is โ€œcompliant.โ€
Usually that really means one of these:

  • the base image and packages follow a hardening baseline;
  • risky defaults are absent;
  • unsupported or banned packages are not present;
  • metadata, ownership, and promotion rules are satisfied.

A useful pattern is:

  1. build image;
  2. generate SBOM;
  3. evaluate against vulnerability and configuration rules;
  4. sign or attest the artifact;
  5. promote only the passing digest.