Skip to content

Run containers as a non-root user

Severity: highApplies to: Docker Engine 27.x+Applies to: Docker Engine 29.x
The fixDockerfile
RUN addgroup -g 10001 app && adduser -u 10001 -G app -D app
USER 10001:10001

Or at runtime, for images you don’t control:

Terminal window
docker run --user 10001:10001 someimage
services:
app:
user: "10001:10001"

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:

Terminal window
docker run -v /srv/data:/data myimage # container root owns /srv/data

Root 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.

USER 10001:10001

Two 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:

/etc/docker/daemon.json
{
"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=host are 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.