DevOps Foundations · Module 5: Container basics
Non-root Execution and Container Runtime
Root inside a container is root on the host kernel with one misconfiguration of distance. This lesson makes non-root the default and explains the runtime boundary honestly.
10 min reading
Objectives
- Run containers as non-root and explain what breaks when you do not
- Set file ownership inside the image so the runtime user can work
- Describe what the runtime isolates and what it does not
- Choose rootless or user namespaces where the environment supports them
Why this matters
A container running as root escapes through a mounted Docker socket or a kernel vulnerability, and the attacker holds the host. The same workload as a numeric non-root user with no new privileges contains the identical exploit to one container. The USER line costs nothing and removes the most common escalation path; skipping it is the container equivalent of chmod 777 from M01.
Concepts
Containers share the host kernel; namespaces and cgroups isolate the view, not the kernel itself. PIDs, mounts, networks and users look private per container, but syscalls reach the same kernel, which is why a kernel exploit or an over-permissive mount (--privileged, the Docker socket, host paths writable) breaks the boundary. The runtime is a convenience boundary with real enforcement, not a security boundary on its own. Defense in depth: non-root user, read-only filesystem where possible, dropped capabilities, no new privileges, minimal mounts.
USER in the Dockerfile sets the runtime user; numeric UIDs (USER 10001) survive environments where names do not resolve. Files the app writes need ownership or group access for that UID, set with chown in the build or an entrypoint that fixes ownership before dropping privileges. A non-root image that crashes on first write was tested as root; test as the runtime user, exactly as M01 lesson 1 demands for services.
Rootless runtimes and user namespaces map container root to an unprivileged host UID, so even a container-root process is nobody outside. Podman runs rootless by default; Docker supports rootless mode. Where available, prefer it: the mapping removes the host-root-escalation class entirely instead of mitigating it per container.
Worked example
A service writes uploads and crashes as non-root. The diagnosis:
$ docker run --user 10001:10001 myapp:1.2 Error: EACCES: permission denied, mkdir '/app/uploads' $ docker run myapp:1.2 ls -ld /app/uploads drwxr-xr-x root root /app/uploads
Expected reading: the directory belongs to root with no group or other write, so UID 10001 cannot create files. The fix belongs in the image, not at runtime flags: chown 10001:10001 the writable paths in the Dockerfile (or a named volume initialized with the right ownership), rebuild, and verify by running as the numeric user with a write smoke test. Runtime --user overrides are for emergencies; baked-in ownership is the fix.
The common wrong move
Running --privileged to make a permission error go away. It disables nearly every isolation the runtime provides (devices, mounts, capabilities) for all processes in the container, permanently, for a problem one chown would fix. Privileged mode is for building runtimes, not running applications; any tutorial step containing it for app code deserves suspicion.
Lab and next step
Lab L13 requires the final image to run as non-root with proof. Next, lesson 3 connects containers to the world: volumes, networks and config.
Quick check
An optional 4-question self-check. Answers never leave your device, are not stored, and never count toward any assessment.
Lesson feedback
No published feedback yet.
Log in and complete the lesson to leave feedback.
Exercise
Run one of your images with --read-only plus --user 10001 and record what breaks. Fix the image (ownership, tmpfs or volumes for writable paths) until it passes; show the before/after run commands.
Pass criteria
Breakage reproduced and quoted; fixed image runs read-only as numeric non-root; every writable path accounted for with its mechanism.