On this page
  1. Why a default Dockerfile is risky
  2. Step 1 — Pin the base image
  3. Step 2 — Create a non-root user
  4. Step 3 — Use a multi-stage build
  5. Step 4 — Keep secrets out of the image
  6. Step 5 — Add a health check and a minimal surface
  7. Where AI coding assistants get this wrong
  8. Checklist
  9. FAQ
    1. Why not just run as root in a container?
    2. What is a distroless image?
    3. Can a non-root container bind port 80?
  10. Related topics
  11. Sources
tutorial

How to Write a Secure Dockerfile

Write a secure Dockerfile: run as non-root, pin base images, use multi-stage builds, and never bake secrets in. A practical, copy-pasteable guide.

Quick answer

  • Run your container as a non-root user and pin base images to exact digests or versions.
  • Use multi-stage builds so compilers and secrets never end up in the final image.
  • Copy only what you need, and pass secrets at runtime — never with COPY or ENV.

Why a default Dockerfile is risky

An AI-generated Dockerfile almost always starts from FROM python:3 and never creates a non-root user, so the app runs as root inside the container. If the app is compromised, the attacker already has root in that container. Default images also float with latest tags, so a “working” build can change underneath you.

Step 1 — Pin the base image

Replace floating tags with a pinned version and prefer slim variants:

FROM python:3.12-slim

How to verify it worked: rebuild after a cache clear and confirm the same image layers are used. Better still, pin to a digest once you know it: FROM python:3.12-slim@sha256:....

Step 2 — Create a non-root user

RUN groupadd --system app && useradd --system --gid app --create-home app
USER app

Everything after USER app runs without root privileges. If the app needs a privileged action at startup (like binding port 80), use a port above 1024 instead.

Step 3 — Use a multi-stage build

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /out/app ./cmd/app

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

The first stage compiles; the second copies only the compiled binary into a minimal, non-root base image. The compiler and source code never ship.

Step 4 — Keep secrets out of the image

Do not COPY .env or ENV API_KEY=.... Pass secrets at runtime:

docker run --env-file .env.production myapp

How to verify it worked: docker history myapp should show no secret values, and no .env file should be in the image layers.

Step 5 — Add a health check and a minimal surface

HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1

Copy only your built artifact and required files, not the whole repository. Fewer files means fewer places for secrets and vulnerabilities to hide.

Where this bites vibecoders

This is a documented, specific failure pattern: default AI Dockerfiles run as root and copy the entire project directory, .env included. The result looks professional — it builds and runs — while quietly shipping secrets and maximum privilege. Treat “does it run as non-root?” and “does the image contain my .env?” as non-negotiable checks before any push.

Where AI coding assistants get this wrong

  • Defaulting to root with no USER directive.
  • Using latest or unpinned base images.
  • COPY . . that pulls in .env, keys, and .git.
  • Baking secrets with ENV or COPY instead of runtime injection.
  • Skipping multi-stage builds, leaving compilers and source in the image.

Checklist

  • Run as a non-root user via USER.
  • Pin base images to a version, ideally a digest.
  • Use multi-stage builds and copy only artifacts into the final stage.
  • Inject secrets at runtime; never COPY or ENV them.
  • Inspect docker history and scan the image before shipping.

FAQ

Why not just run as root in a container?

If the application is compromised, running as root lets an attacker escalate to more of the container’s capabilities and makes host escapes easier. A non-root user limits the blast radius, which is defense in depth at nearly zero cost.

What is a distroless image?

A “distroless” image contains only the app and its runtime dependencies — no shell, package manager, or system utilities. Fewer components means fewer vulnerabilities and less for an attacker to use. If your app never needs a shell, distroless is a strong default.

Can a non-root container bind port 80?

No, not without special configuration, because ports below 1024 are privileged. Bind a higher port like 8080 inside the container and map it to 80 on the host if needed.

Sources

Share: