17.1.3.3 Non Root Practice
A focused guide to Non Root Practice, connecting core concepts with practical Docker and container operations.
Non-root practice is the constructive side of running containers as a restricted, unprivileged user, the actual Dockerfile patterns and decisions that implement this correctly and predictably, distinct from troubleshooting the specific limitations that running as non-root introduces, focused instead on how to set it up well from the outset.
Using an existing non-root user where one is already provided
Many official base images already define a conventional, ready-to-use non-root user specifically for this purpose, and using it directly avoids redundant user creation logic:
FROM node:20-alpine
USER node
FROM node:20
USER node
The node images specifically include a pre-created node user for exactly this purpose; checking whether a given base image already provides an equivalent convenience user, rather than always creating one from scratch, both simplifies the Dockerfile and follows whatever convention that image's own maintainers established.
Creating a dedicated user explicitly when none is provided
For base images without a pre-existing convenience user, creating one explicitly, with a deliberately chosen name and ID, keeps the configuration clear and intentional rather than relying on whatever default might otherwise apply:
FROM alpine:3
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
USER appuser
FROM debian:bookworm-slim
RUN groupadd -g 1000 appgroup && \
useradd -m -u 1000 -g appgroup appuser
USER appuser
Specifying an explicit numeric UID and GID, rather than letting the system assign the next available one automatically, makes the resulting ownership predictable and documented, which matters when this same UID needs to be aligned with host-side bind mount ownership or shared across multiple services.
Numeric UID versus named user trade-offs
Referencing the user by numeric UID rather than name in the USER instruction has a specific advantage: it works correctly even if the corresponding named user does not exist in /etc/passwd at all, which is relevant for certain orchestration platforms that may run a container under an arbitrary, dynamically assigned UID regardless of what the image itself defines:
USER 1000:1000
USER appuser
For maximum compatibility across different orchestration environments, ensuring the application does not depend on being able to resolve its own UID to a username at all (some applications attempt this and fail if no matching /etc/passwd entry exists) is a more defensive, broadly compatible approach than relying on named user resolution working correctly in every possible deployment context.
Privilege drop patterns for setup-then-run sequences
Some containers genuinely need to perform a brief setup step as root, adjusting file ownership on a freshly mounted volume, for instance, before dropping to a restricted user for the actual, long-running application process; a small, dedicated tool handles this transition more reliably than attempting it directly in a shell script:
RUN apk add --no-cache su-exec
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
#!/bin/sh
chown -R appuser:appuser /app/data
exec su-exec appuser node server.js
su-exec (or the similar gosu) correctly replaces the current process rather than spawning a child, which preserves correct signal handling and avoids the PID 1 complications that a naive su invocation inside a shell script would otherwise introduce.
Using distroless's dedicated nonroot variant
Distroless images provide a separate, explicitly named nonroot variant specifically pre-configured to run as a restricted user by default, removing the need to configure this manually at all:
FROM gcr.io/distroless/nodejs20-debian12:nonroot
Choosing this variant directly, rather than the default root-running distroless image with a separately added USER instruction, is simpler and relies on the image maintainer's own, already-reviewed configuration rather than reconstructing the equivalent setup independently.
File ownership alignment for the entire application directory
Beyond just switching the active user, every file and directory the application needs to read or write needs ownership genuinely aligned with that user, set explicitly during the build rather than left at whatever default ownership a COPY instruction would otherwise apply:
COPY --chown=appuser:appuser . /app
USER appuser
WORKDIR /app
Using --chown directly on the COPY instruction is more efficient than copying as root and adjusting ownership afterward in a separate instruction, since the latter approach still requires an additional layer specifically for the ownership change, while --chown applies the correct ownership during the copy itself with no additional step needed.
Verifying the configuration works end to end
After configuring a non-root user, confirming the application actually starts and operates correctly under that restricted identity, rather than assuming correctness based on the Dockerfile's apparent structure, catches any remaining permission gaps before they surface in production:
docker build -t my-api .
docker run --rm my-api id
docker run --rm my-api node server.js
Running this verification as a deliberate step, ideally incorporated into the CI pipeline itself, ensures non-root configuration is genuinely tested rather than only assumed to be correct.
Common mistakes
- Creating a new, custom non-root user from scratch when the base image already provides a conventional, ready-to-use one for exactly this purpose.
- Not specifying an explicit, deliberate UID and GID, leaving ownership alignment with host or shared-volume conventions to chance.
- Using
sudirectly in a shell script for a privilege-drop pattern, introducing PID 1 and signal-handling complications that a dedicated tool likesu-execorgosuavoids. - Copying application files as root and adjusting ownership in a separate, later instruction rather than using
--chowndirectly on theCOPYinstruction itself. - Not actually verifying the non-root configuration works correctly through a genuine test run, assuming correctness based only on the Dockerfile's apparent structure.
Non-root practice, done well, uses whatever convenience user a base image already provides where available, specifies explicit and predictable UID and GID values when creating one from scratch, applies correct file ownership directly during the copy step, and is verified through an actual, genuine test run rather than assumed correct based on configuration alone.