User Namespace Remapping, UID/GID Mapping, Non-Root Images, Filesystem Ownership, and Least Privilege: Concepts, Architecture, and Mental Model
Trace identity from Dockerfile USER and runtime --user through container UID/GID, optional user namespaces, subordinate host IDs, and filesystem permission checks.
Learning objectives
-
Explain the identity chain from image
USERthrough runtime overrides, container numeric UID/GID, optional user namespaces, and host filesystem checks. -
Distinguish names such as
appfrom the numeric IDs that Linux permission checks actually use. -
Compare ordinary rootful,
userns-remap, and rootless UID/GID mapping without conflating their daemon privilege models. - Inspect image, container, mount, subordinate-range, and ownership state before attempting any permission change.
1. The practical problem: “non-root” is not one identity
Dockerfiles, containers, user namespaces, and host files all use the
word “user,” but Linux ultimately authorizes filesystem access with
numeric UIDs, GIDs, supplementary groups, capabilities, ACLs, and
security policy. A container may display the username
app while the host sees only UID 10001—or
a remapped high-number UID such as 231073.
The operational goal is therefore not “make the prompt stop saying permission denied.” It is to establish a traceable identity contract: which numeric identity the process has, how that identity maps to the host, which path it must write, and which paths it must not write.
2. Mental model: image identity → runtime identity → optional namespace translation → inode check
USER in a Dockerfile writes a default user into the
image config. docker run --user or Compose
user: can override it at runtime. The resulting process
has numeric UID/GID values inside its container namespace. If user
namespaces are disabled, those numeric IDs exist directly in the
initial host user namespace. If userns-remap or rootless mode is
active, Linux translates them into other host IDs before the kernel
evaluates host filesystem ownership.
A bind mount does not magically “belong to the container.” The underlying host inode still has an owner, group, mode bits, optional ACLs, and possibly SELinux/AppArmor policy. Those checks happen after any namespace translation.
flowchart TD
A[Dockerfile USER / image config] --> B[Runtime --user / Compose user override]
B --> C[Container numeric UID:GID + supplementary groups]
C --> D{User namespace?}
D -->|No| E[Same numeric IDs in initial host user namespace]
D -->|userns-remap| F[Container IDs translated through remap subuid/subgid]
D -->|rootless| G[UID 0 -> host user; other IDs -> subordinate range]
E --> H[Bind/volume inode ownership + mode/ACL/LSM checks]
F --> H
G --> H
H --> I[Read/write allowed or denied]
3. Names are labels; numeric IDs are the durable evidence
| Layer | Useful evidence | Common mistake |
|---|---|---|
| Image config | docker image inspect ... .Config.User |
Assuming a username has the same numeric ID in every image/host. |
| Container process | id, /proc/self/status |
Looking only at the shell prompt or username string. |
| Runtime override | docker inspect ... .Config.User |
Forgetting --user/Compose can override image
USER.
|
| Supplementary groups | id -G, --group-add evidence |
Checking UID/GID but ignoring shared group permissions. |
| User namespace | /proc/self/uid_map, gid_map |
Calling root inside a namespace equivalent to host root. |
| Host path | stat -c %u:%g:%a |
Changing mode before proving the mapped numeric owner. |
| Volume/bind metadata | docker inspect mounts |
Confusing Docker object ownership with filesystem inode ownership. |
4. Three mapping models you must keep separate
Ordinary rootful daemon without userns-remap: container UID 10001 is host UID 10001 for host filesystem checks.
Rootful daemon with userns-remap: container UID 0 maps to the first subordinate UID assigned to the remap user; container UID n maps to subordinate-start + n. The daemon itself still runs as root.
Rootless Docker: container UID 0 maps to the host UID of the user running rootless Docker. Container IDs ≥1 map into that user’s subordinate range using Docker’s documented offset rule. Both daemon and containers run without host-root privilege.
5. Read-only baseline inspection
docker version
docker context show
docker info --format 'Security={{json .SecurityOptions}} Root={{.DockerRootDir}}'
# Image configured identity (read-only)
docker image inspect alpine:3.22.1 --format 'User={{json .Config.User}}' 2>/dev/null || true
# Host identity/ranges on Linux
id
cat /etc/subuid 2>/dev/null | head
cat /etc/subgid 2>/dev/null | head
Do not infer daemon mode from a context name.
docker info security options plus socket/data-root
evidence are stronger proof.
6. Dockerfile USER and COPY --chown are different controls
USER chooses the default identity for later build
instructions that honor it and, critically, the default runtime
process. COPY --chown sets ownership on files added to
the image. They solve different problems: one controls process
identity; the other prepares filesystem state.
Docker resolves named --chown values through the
image’s /etc/passwd and /etc/group.
Numeric IDs avoid name lookup and are useful when you need an
explicit cross-image contract.
7. DevOps contract: identity must survive rebuild, replacement, and restore
A reproducible deployment records at least: image digest, configured runtime user, any runtime override, writable paths, volume/bind ownership contract, user-namespace mode, and the expected numeric IDs. If a restored volume suddenly needs root or world-write access, that is evidence that the identity contract was never captured or was changed.
Knowledge check
Why is a username not enough evidence for filesystem access?
Linux permission checks ultimately use numeric UID/GID values, supplementary groups, ACLs, and policy. The same username can map to different numbers in different images or on the host.
What does Dockerfile USER control?
It sets the image default user for runtime and for later
Dockerfile instructions where applicable; runtime
--user or Compose user can override
it.
How does userns-remap map container UID 0?
To the first subordinate UID assigned to the remap user; UID n maps to subordinate-start + n.
How does rootless mapping differ for container UID 0?
Container UID 0 maps to the real host UID of the user running rootless Docker; IDs 1+ map into the subordinate range.
Why does a bind mount often reveal identity mistakes?
Because the mounted inode keeps host-side numeric ownership and permissions; the container process must pass those checks after any namespace mapping.
Official references and version notes
- User namespace remapping — current prerequisites, mapping behavior, daemon configuration, limitations, and migration cautions.
- Rootless UID/GID mapping — precise difference between userns-remap and rootless host-ID translation.
- Rootless mode — daemon-level non-root execution and subordinate-ID prerequisites.
- Dockerfile reference — USER and COPY --chown semantics, including numeric versus name lookup.
- Compose services reference — service user override and user namespace configuration surface.
- Bind mounts — host-path ownership, read/write authority, and daemon-host path semantics.
- Volumes — Docker-managed persistent storage and container mount behavior.
- Docker Engine 29 release notes — current Engine baseline and user-namespace-related fixes/changes.
Docker Engine 29.8.1 is the current Engine release. ' Docker
documents two different mappings: in userns-remap,
container UID/GID 0 maps to the first subordinate ID and ID
n maps to subordinate-start + n; in rootless mode,
container UID/GID 0 maps to the host user and IDs ≥1 map into the
subordinate range with an offset. ' Docker recommends enabling
userns-remap on a new daemon because existing Docker
objects become masked by the remapped storage layout, and it warns
that host-mounted filesystem ownership must be arranged for the
mapped IDs. ' COPY --chown accepts names or numeric
IDs; names require matching /etc/passwd//etc/group
inside the build rootfs, while numeric IDs do not. ' Compose
user overrides the image-configured user. Every lab
therefore records the actual Engine/context/security mode and
numeric UID/GID evidence instead of assuming usernames imply
equivalent authority.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.