On this page
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
COPYorENV.
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-slimHow 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 appEverything 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 myappHow 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 1Copy 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,
.envincluded. 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
USERdirective. - Using
latestor unpinned base images. COPY . .that pulls in.env, keys, and.git.- Baking secrets with
ENVorCOPYinstead 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
COPYorENVthem. - Inspect
docker historyand 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.
Related topics
- How to Scan Your Codebase for Hardcoded Secrets
- Docker vs Podman: What’s the Difference?
- What Is DevSecOps?
- What Is a Supply Chain Attack?