Zennoxa Shield

Do popular container images drop root? We checked 25 projects

Half of the primary Dockerfiles in 25 popular infra and app projects don't set an explicit non-root USER in their final stage. A fair, caveated look at what static analysis can and can't tell you about container privilege.

July 21, 2026By Zennoxa Researchcontainersdockerdockerfiledevsecops

TL;DR — Across 25 popular infrastructure and application projects, we looked at whether their Dockerfiles set a non-root USER. Of the 16 projects with a clear primary image, exactly half (8) don't set an explicit non-root USER in the final build stage. But "no USER line" is not the same as "insecure" — this post is as much about that distinction as about the number.

Why a non-root USER matters (and when it doesn't)

If a container's final image doesn't drop to a non-root user, a process that breaks out of the app runs as root inside the container — a bigger blast radius if it then finds a kernel or runtime escape. Setting USER app (a non-root uid) is the documented defense-in-depth default in Docker's and CIS's hardening guidance.

But three honest caveats mean "no USER line" ≠ "vulnerable", and we lead with them because a benchmark that hides its limits isn't one you should trust:

  1. A static check can't see into base images. FROM some/image that already sets a non-root USER internally is invisible to a Dockerfile scanner — so a file with no USER may still run non-root at runtime.
  2. It can't see runtime privilege drops. Some images stay root in the Dockerfile but drop to a non-root user at startup via gosu/su-exec/an entrypoint (gitea does exactly this). The container isn't actually running as root.
  3. Minimal images blur it. A static binary on scratch/distroless/alpine often runs as root with a tiny attack surface, where "root" means much less.

So the precise, checkable claim here is "the Dockerfile does not set an explicit non-root USER" — a defense-in-depth smell, not a proof of vulnerability. With that framing set, here's what we found.

What we measured

  • Corpus: 25 popular infra/app repos; 22 ship at least one Dockerfile.
  • Tool: Zennoxa Shield's CONTAINER-012 — multi-stage-aware; it fires only when the final stage never sets a non-root USER (an earlier build stage doesn't count). Hand-verified against a sample (traefik/minio → alpine, no USER; prometheus → USER nobody; vault → USER ${NAME}).
  • Two cuts, because one number would mislead: all Dockerfiles (which sweeps in dev/test fixtures where root is fine), and primary image only (the top-level ./Dockerfile — the thing most likely actually shipped).

Full per-repo data in data.json.

The numbers

Cut No non-root USER Share
Primary image only (16 repos) 8 50.0%
All Dockerfiles (195 files) 110 56.4%

The "all Dockerfiles" figure is inflated by dev/test fixtures (local databases, proxies) where root is irrelevant — which is exactly why we don't lead with it. The primary-image cut is the fair headline: half of these projects' main Dockerfiles don't set a non-root USER.

Primary image sets a non-root USER: prometheus, vault, consul, posthog, mastodon, airflow, superset, netdata.

Primary image does not (final stage): grafana, moby, traefik, minio, appwrite, home-assistant, gitea, gogs — noting that some of these (e.g. gitea) drop privileges at runtime instead, per caveat #2.

What to actually do

Setting an explicit non-root user is a one-line change and removes the ambiguity — your image is provably non-root regardless of base-image behavior:

# create + switch to a non-root user in the FINAL stage
RUN adduser -D -u 10001 app
USER app

If you rely on a runtime drop (gosu) or a base image's user, that's defensible — but it's worth making explicit so a reviewer (or a scanner) can see it. Zennoxa Shield's CONTAINER-012 flags final stages with no non-root USER if you want it visible in CI.

Limitations

  • Static Dockerfile analysis only — see caveats 1–3 above; the true runtime root rate is plausibly lower than the Dockerfile-text rate.
  • 25 popular repos, not a random sample of all container projects.
  • Counts the final stage's USER; doesn't evaluate capabilities, --privileged, or seccomp/AppArmor, which also shape real privilege.
  • Point-in-time snapshot (2026-07-21).

Reproduce it

git clone --depth 1 --filter=blob:none --no-checkout https://github.com/traefik/traefik t
cd t && git sparse-checkout init --no-cone && git sparse-checkout set '**/Dockerfile' && git checkout
# a Dockerfile whose final stage has no `USER` (non-root) defaults to root:
grep -nE '^\s*(FROM|USER)\b' Dockerfile

Per-repo counts and method in data.json.


Zennoxa Research publishes reproducible security data on public projects with public tooling. No third-party scanner is involved.

Shield flags containers that run as root, so you catch the ones that never dropped privileges.

See what Shield finds in your code

Free to start, no credit card. One pass covers SAST, secrets, vulnerable dependencies, containers and IaC — ranked by what’s actually reachable, not just severity.

Scan your repo free

Prefer the terminal? The offline CLI runs with no account

More research
Do popular container images drop root? We checked 25 projects — Zennoxa Shield