CoursesAdvanced container securityHardening the container

Drop root: non-root by default

USER, numeric UIDs, and file ownership.

Advanced12 min · lesson 10 of 25

The highest-value line in any hardening effort is: do not run as root. Because container root is host root against a shared kernel, a non-root container turns a breakout from “attacker owns the host” into “attacker is an unprivileged user with almost nothing to do.” You establish it in two places — bake a non-root user into the image, and enforce non-root at run time so a rebuilt or third-party image cannot quietly go back to root.

Dockerfile
FROM alpine:3.20
# create a dedicated, fixed-UID user — do not reuse a system account
RUN addgroup -g 10001 app && adduser -u 10001 -G app -S -D app
WORKDIR /app
COPY --chown=10001:10001 . .
USER 10001:10001 # numeric UID:GID — works even with runAsNonRoot enforcement
ENTRYPOINT ["/app/server"]

Use a numeric UID, not just a name

Prefer USER 10001:10001 over USER app. A numeric UID lets the platform verify the container is non-root (Kubernetes runAsNonRoot checks the UID, and cannot tell whether the name “app” maps to 0), and it avoids surprises when /etc/passwd is absent, as on scratch and distroless. Own the app’s files to that UID at COPY time with --chown so the non-root process can actually read what it needs.

terminal
# enforce non-root at run time, independent of what the image declares
$ docker run --rm --user 10001:10001 myapp:1.0 id
uid=10001 gid=10001
# Kubernetes equivalent, which rejects a root image at admission:
# securityContext: { runAsNonRoot: true, runAsUser: 10001 }

The files a non-root process needs to write

Dropping root breaks apps that assumed they could write anywhere — a cache dir, a PID file, a socket. The fix is not to go back to root but to pre-create those paths owned by the app UID (or mount them as writable volumes/tmpfs), so the process writes exactly where it should and nowhere else. This pairs directly with the read-only root filesystem in the next lesson.

A user in the image is not enforcement
USER in a Dockerfile is a default that a docker run --user or a rebuilt image can override, and many base images still default to root. Belt and braces: set USER in the image AND enforce runAsNonRoot (or --user) at the platform, so no single mistake puts a root process back on a shared kernel.