✦ For everyone, free.

Practical knowledge for real and everyday life

Home

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 su directly in a shell script for a privilege-drop pattern, introducing PID 1 and signal-handling complications that a dedicated tool like su-exec or gosu avoids.
  • Copying application files as root and adjusting ownership in a separate, later instruction rather than using --chown directly on the COPY instruction 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.