Enforce non-root Docker users across every repo in your org

How to Enforce Non-Root Docker Users Across Every Repo in Your Org

Mandy Singh

Mandy Singh

June 10, 2026 · // 8 min read

A security review flags forty Dockerfiles running as root. None of them were written that way on purpose. Root is Docker’s default, the USER directive is easy to forget, and nobody circles back to add it once the container works. Now a compliance deadline is attached to the finding, and someone has to fix forty repos, verify none of them break on deploy, and prove it happened.

What does “non-root Docker user” mean, and why does it matter?

A non-root Docker user is a dedicated, low-privilege user created inside the image and set with the USER directive so the container process never runs as root. If an attacker compromises a container running as root, they get root access to that container’s filesystem and, depending on the runtime configuration, a path toward the host. SOC 2, FedRAMP, and PCI-DSS all expect non-root containers, and Kubernetes Pod Security Standards enforce it at the cluster level. Without the USER directive, a Dockerfile runs as root by default, whether or not anyone intended it.

Why root containers are still everywhere

Root containers aren’t a knowledge gap. Most engineers already know the anti-pattern, and the reason it sticks around comes down to placement rather than awareness. The USER directive has to go after package installation and file ownership are set, but before CMD or ENTRYPOINT. Put it in the wrong spot and the container fails to start, an app can’t write to its own directory, or a multi-stage build breaks in the stage that actually needs root to compile.

That failure mode is why teams patch the Dockerfile they’re actively working on and leave the other 200 alone. Fixing one Dockerfile is a ten-minute task. Verifying the fix across every service that uses a slightly different base image, build process, or entrypoint is where the work actually lives.

The method: three additions, in a specific order

The fix itself is small and repeatable. It has four parts, and the order is what makes it safe.

  1. Create a system group and user. addgroup --system --gid 1001 appgroup && adduser --system --uid 1001 --ingroup appgroup appuser creates a low-privilege user with no login shell and a fixed UID/GID, which matters for file permission consistency across environments.
  2. Set ownership of the application directory. chown -R appuser:appgroup /app gives the new user write access to the files it needs, run after the app is copied in but before the user switch.
  3. Add the USER directive before CMD or ENTRYPOINT. Everything after this line runs as appuser, not root.
  4. In multi-stage builds, only touch the final runtime stage. The build stage often needs root to compile dependencies or run install scripts, so adding USER there breaks the build. The non-root user only belongs in the runtime stage, the one that actually ships.

Skip any Dockerfile that already sets a non-root USER. Re-running the fix on a compliant file wastes a review cycle and risks introducing a duplicate user creation step.

Here’s what the change looks like applied to a Node service:

 COPY package*.json ./
 RUN npm ci --omit=dev
 COPY --from=builder /app/dist ./dist
+
+RUN addgroup --system --gid 1001 appgroup && \
+    adduser --system --uid 1001 --ingroup appgroup appuser
+RUN chown -R appuser:appgroup /app
+USER appuser
+
 CMD ["node", "dist/index.js"]

That’s four lines, added in the right place and the right order, and it’s the entire fix per Dockerfile.

A prompt you can run against your own Dockerfiles

If you’re using an AI coding agent to make this change, precision in the prompt matters more than length. Vague instructions like “add a non-root user” produce inconsistent output across a fleet of Dockerfiles with different base images and build stages. This is the exact prompt we use:

Update all Dockerfiles to run as a non-root user. For each Dockerfile:

1. Add a non-root user creation step after all package installations:
   RUN addgroup --system --gid 1001 appgroup && \
       adduser --system --uid 1001 --ingroup appgroup appuser
2. Set appropriate file permissions for the application directory:
   RUN chown -R appuser:appgroup /app
3. Add USER appuser before the CMD or ENTRYPOINT instruction
4. If the Dockerfile uses a multi-stage build, only add the USER directive
   in the final stage (the build stage can remain root)
5. Do not modify the CMD, ENTRYPOINT, or any application logic

If the Dockerfile already has a USER directive set to a non-root user,
skip that file.

Run it against one repo first, check the diff, and confirm the container still starts before turning it loose on everything else.

Edge cases worth checking before you run this everywhere

  • UID/GID standards. Some orgs pin specific UID/GID values for compliance or to align with host-level permissions. Update 1001 to match your policy before running the fix broadly.
  • Alpine-based images. Alpine’s addgroup and adduser use -S instead of --system. If your fleet mixes Debian-based and Alpine-based images, the prompt needs to branch on base image, or you run it in two passes.
  • Volume mounts. If a container writes to a mounted volume, the new non-root user needs write access to that mount. Check permissions on the host side, not just inside the image.

Why this gets stuck at repo forty, not repo one

The Dockerfile change is defined. The validation is simple: does the container still start, can it still write where it needs to write. The part that stalls is doing this correctly, in the right order, across every service that has a Dockerfile, without breaking any of them on the first try.

A platform engineer we work with put it plainly: Maintenance Agent is well suited to rolling out CVE patches across every service and then prompting each team to merge their pull request. The non-root user fix is the same shape of problem. One change, applied correctly everywhere, tracked until every repo actually merges it, not just until every repo has an open PR.

How Tidra runs this across your fleet

Tidra is an AI coding agent for both implementation and coordination of code changes across your organization. Applied to this fix, that means:

Tidra scans every repo for a Dockerfile, checks whether a non-root USER is already set, and skips the ones that are already compliant. For the rest, it generates the user creation, ownership, and USER directive changes in the correct order, detects multi-stage builds automatically, and only modifies the final stage. Nothing is pushed until you review the diff. Once you’re satisfied, PRs go out across every affected repo in bulk, and you track merge status, owner, and blockers from one dashboard instead of a spreadsheet someone updates by hand.

You still own every line that ships. The plan is reviewable before any code is touched, and the generated diffs are reviewable before any PR opens. What Tidra removes is the manual work of applying the same four-line fix to the fortieth Dockerfile the same way you applied it to the first.

Where this leaves your next compliance audit

The fix for non-root containers has been known for years. What decides whether an audit finding closes in a week or sits open for two quarters isn’t the Dockerfile change. It’s whether someone has to open, edit, test, and merge forty to four hundred pull requests by hand, or whether that work runs once and gets tracked to completion on its own. Which one describes your last security remediation?


Run this initiative across your repos: tidra.ai/initiatives/enforce-docker-non-root-user


FAQ

What is the non-root Docker user best practice? Create a dedicated system user and group inside the image, give that user ownership of the application directory, and add a USER directive before CMD or ENTRYPOINT so the container process never runs as root.

Does adding a non-root user break multi-stage Docker builds? Only if you add the USER directive to the wrong stage. The build stage often needs root to install dependencies or compile code. Add the non-root user only in the final runtime stage that actually ships.

Which compliance frameworks require non-root containers? SOC 2, FedRAMP, and PCI-DSS all expect containers to run as non-root. Kubernetes Pod Security Standards enforce non-root execution at the cluster level.

Why doesn’t a Dockerfile run as non-root by default? Docker defaults to root because it requires no extra configuration to work. The USER directive is opt-in, so any Dockerfile written without one runs as root unless the base image sets a non-root user itself.

Can I automate this fix across hundreds of repos at once? Yes. The fix is a repeatable, well-defined change, which makes it a good candidate for an AI coding agent to generate and open as pull requests across every repo that needs it, with a human reviewing each diff before it merges.