Run containers as a non-root user
DockerfileRUN addgroup -g 10001 app && adduser -u 10001 -G app -D appUSER 10001:10001Or at runtime, for images you don’t control:
docker run --user 10001:10001 someimageservices: app: user: "10001:10001"Why it matters
Section titled “Why it matters”Containers run as root unless you say otherwise. UID 0 inside the container is UID 0 outside it — the kernel sees one number. Namespaces change what a process can see, not who it is.
That matters the moment anything is shared:
docker run -v /srv/data:/data myimage # container root owns /srv/dataRoot in that container can chown, chmod and rewrite everything under
/srv/data on the host, because it is the same UID 0 the host uses. Add a
careless mount — -v /etc:/etc in a debugging session — and it edits your
passwd file.
It also raises the value of every other bug. A remote code execution in your app gives an attacker root inside the container, which means capabilities to work with and a much shorter path to a container escape. Running as UID 10001 means the same RCE lands as a user with nothing.
Pick a high, explicit UID
Section titled “Pick a high, explicit UID”USER 10001:10001Two details that matter:
Use a number, not a name. USER app requires the name to resolve inside the
container, and Kubernetes’ runAsNonRoot check cannot verify a name — it needs a
numeric UID to know the container isn’t root. A numeric USER works everywhere.
Use a high one. Low UIDs collide with real host accounts, so a container
running as UID 100 writes files owned by whatever host user is 100 — often
systemd-network or similar. Something in the 10000+ range collides with
nothing.
userns-remap is the real fix, and it has real costs
Section titled “userns-remap is the real fix, and it has real costs”Docker can map container UIDs to unprivileged host UIDs, so container root becomes an ordinary host user:
{ "userns-remap": "default"}Container UID 0 then maps to something like host UID 296608. Root in the container becomes root over nothing.
Be honest about the cost, because it’s why adoption is low:
- Existing volumes break. Files owned by real UID 0 become unreadable to the remapped root. Every existing named volume and bind mount needs its ownership fixed.
--privileged,--pid=host,--network=hostare incompatible with it and will fail.- It’s daemon-wide. Every container is remapped or none is; there’s no per-container opt-out.
For a new deployment it’s excellent. For an existing one it is a migration, not a setting. Rootless mode achieves more and is often the better place to spend the same effort.
Related
Section titled “Related”- Rootless mode — solves this more completely.
- Drop capabilities — narrow what root-in-container could do anyway.