How it works
The explanation, with examples you can run.
Who the process is
docker run --rm alpine:3.22 id# uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),... docker run --rm hello:1 id # image with USER node# uid=1000(node) gid=1000(node) groups=1000(node)Set the user in the image so every runtime uses it:
FROM node:22-alpineWORKDIR /appCOPY --chown=node:node . .USER nodeCMD ["node", "server.js"]The official node images ship a node user (UID 1000); distroless :nonroot tags run as UID 65532. Ports below 1024 traditionally need privileges, so let the app listen on 3000 or 8080 and publish whatever port you like on the host.
Capabilities and other switches
Root's power is split into capabilities. Docker's default set is much smaller than the full list, and you can drop the rest:
docker run --rm alpine:3.22 grep CapEff /proc/self/status# CapEff: 00000000a80425fb (Docker's default set)docker run --rm --cap-drop ALL alpine:3.22 grep CapEff /proc/self/status# CapEff: 0000000000000000 docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \ --user 10001:10001 alpine:3.22 sh -c 'id; touch /x'# uid=10001 gid=10001 groups=10001# touch: /x: Read-only file system--cap-drop ALL, then--cap-addonly what is needed (for exampleNET_BIND_SERVICE).--security-opt no-new-privilegesstops setuid binaries from raising privileges.--read-onlywith--tmpfs /tmpkeeps the app's files unchangeable.- Never use
--privilegedfor ordinary workloads: it gives the container all capabilities and access to host devices.
The daemon is root
The Docker daemon runs as root (unless you run it in rootless mode), and its API can mount any host path into a new container. Being in the docker group, or having /var/run/docker.sock mounted into a container, is therefore equivalent to root on that host. Treat CI jobs and tools that ask for the socket accordingly, or use rootless builders.
Two ways to make root in the container less powerful:
- User namespaces (
userns-remap) map container UID 0 to an unprivileged range on the host. - Rootless mode runs the daemon and containers as an ordinary user, at the cost of some features.
What goes into the image
- Start from a minimal, pinned base, and rebuild regularly so patched packages arrive.
- Scan images in CI (Docker Scout, Trivy or Grype) and fail on fixable critical issues.
- Keep secrets out of layers and build arguments; use BuildKit secret mounts at build time and runtime secrets (files or a secrets manager) at run time.
- Publish only the ports you need, and on servers bind internal ones to
127.0.0.1.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.