Fix 'container has runAsNonRoot and image will run as root' (2026)
This guide is for anyone whose pod is stuck in CreateContainerConfigError with "container has runAsNonRoot and image will run as root", or who just turned on Pod Security "restricted" and watched half their workloads fail. By the end you will have an image that runs as a numeric non-root UID, a securityContext that passes the Restricted standard, and fixes for the errors that usually follow: permission denied on volumes, read-only file system writes, and binding port 80. Everything here is checked against Kubernetes v1.37.2 source and docs, containerd 2.x and runc 1.2+. You need kubectl access to a test namespace and the ability to rebuild the image. If you want the background on what each field does, read the companion post, Kubernetes securityContext Explained.
Step 1: Read the exact error message
The kubelet emits three different messages from the same check, and each has a different fix. Start by getting the precise text.
# Replace web and the namespace with your own
kubectl -n shop get pods -l app=web
kubectl -n shop describe pod -l app=web | sed -n '/State:/,/Events:/p;/Events:/,$p'
Expected output for the most common case:
State: Waiting
Reason: CreateContainerConfigError
...
Warning Failed 12s (x3 over 30s) kubelet Error: container has runAsNonRoot and image will run as root (pod: "web-6c9f8d7b5-x2k4p_shop(3f1c...)", container: web)
The strings come straight from the kubelet's verifyRunAsNonRoot, and its logic is simple:
| Message | What the kubelet saw | Go to |
|---|---|---|
image will run as root |
runAsUser unset, image user is UID 0 (or unset) |
Step 3 |
image has non-numeric user (node), cannot verify user is non-root |
runAsUser unset, image user is a name |
Step 4 |
container's runAsUser breaks non-root policy |
runAsUser: 0 with runAsNonRoot: true |
Remove runAsUser: 0 |
image has an invalid user id |
UID outside 1 to 2147483647 | Use a normal UID |
If you see nothing like that and the pod was never created at all, the rejection came from Pod Security Admission instead; jump to Step 7.
Step 2: Find out which user the image runs as
The kubelet only looks at the User field in the image config. It never reads /etc/passwd inside the image, which is why names fail.
# With Docker (pulls the image if needed)
docker image inspect --format '{{.Config.User}}' node:24-bookworm-slim
# (empty line = root)
# Without a daemon, using the registry API via regctl or crane if you have them
# crane config node:24-bookworm-slim | jq -r '.config.User'
# Or ask the running container, if it starts under a permissive namespace
kubectl -n scratch run whoami --rm -it --restart=Never \
--image=node:24-bookworm-slim -- id
# expected: uid=0(root) gid=0(root) groups=0(root)
An empty User means root. A value like node or appuser is a name and will trigger the non-numeric error. A value like 1000 or 10001:10001 is what you want.
Step 3: Fix "image will run as root" with a numeric USER
The durable fix belongs in the image, so every environment gets it, not just the namespace where someone remembered the manifest. Create a user with a fixed UID and switch to it by number.
# Dockerfile: Python service, non-root by UID
FROM python:3.13-slim-bookworm
# Fixed UID/GID above the usual system and login ranges
RUN groupadd --gid 10001 app \
&& useradd --uid 10001 --gid 10001 --no-create-home --shell /usr/sbin/nologin app
WORKDIR /app
COPY --chown=10001:10001 requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=10001:10001 . .
# Numeric, so the kubelet can prove it is not root
USER 10001:10001
EXPOSE 8080
CMD ["python", "-m", "gunicorn", "-b", "0.0.0.0:8080", "app:app"]
On Alpine the equivalent is addgroup -g 10001 app && adduser -u 10001 -G app -S -D app. If you build on distroless, the :nonroot tags ship a user with UID 65532, so use USER 65532:65532.
If you cannot rebuild the image today (a vendor image, say), set the UID in the manifest instead. Check first that the app does not need to write anywhere owned by root.
# deployment.yaml (fragment): override the image user
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001 # without this the primary GID stays 0
Then roll it out and confirm:
kubectl -n shop apply -f deployment.yaml
kubectl -n shop rollout status deploy/web --timeout=120s
kubectl -n shop exec deploy/web -- id
# expected: uid=10001 gid=10001 groups=10001
Step 4: Fix "image has non-numeric user, cannot verify user is non-root"
This one catches official images that do the right thing in a way Kubernetes cannot check. The Node.js images, for example, create a node user with UID 1000, and their docs tell you to add USER node. That name is exactly what triggers the error.
FROM node:24-bookworm-slim
WORKDIR /app
COPY --chown=1000:1000 . .
RUN npm ci --omit=dev
# was: USER node (name, kubelet cannot verify)
USER 1000:1000
CMD ["node", "server.js"]
Or leave the image alone and set runAsUser: 1000 in the pod. Either works, because the kubelet checks runAsUser before it looks at the image.
Step 5: Fix permission denied on volumes with fsGroup
Once the process is non-root, a PersistentVolume formatted and owned by root rejects writes: EACCES, Permission denied, or Postgres complaining that its data directory has wrong ownership. Set fsGroup at pod level so the kubelet gives the group ownership of the volume at mount time.
spec:
securityContext:
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch # skip the recursive chown if the root already matches
containers:
- name: web
volumeMounts:
- name: data
mountPath: /var/lib/app
volumes:
- name: data
persistentVolumeClaim:
claimName: web-data
kubectl -n shop exec deploy/web -- ls -ld /var/lib/app
# expected (illustrative): drwxrwsr-x 3 root 10001 4096 Nov 10 09:12 /var/lib/app
If ownership did not change, the volume type is not one the kubelet adjusts. For CSI volumes, check the driver's policy:
kubectl get csidriver -o custom-columns=NAME:.metadata.name,FSGROUP:.spec.fsGroupPolicy
None means the driver never applies fsGroup. The default, ReadWriteOnceWithFSType, only applies it when the PV has an fsType and a ReadWriteOnce access mode, so many RWX file shares are skipped (CSI docs). hostPath is never adjusted. For those, fix ownership on the server side or, as a last resort, use an init container that runs chown as root (which Restricted will not allow, so it needs its own namespace policy).
Step 6: Fix "Read-only file system" and ports below 1024
With readOnlyRootFilesystem: true, any write outside a volume fails with errno 30, for example OSError: [Errno 30] Read-only file system: '/tmp/...'. Find the paths the app writes to and give each one an emptyDir.
containers:
- name: web
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: cache, mountPath: /home/app/.cache }
volumes:
- name: tmp
emptyDir: { sizeLimit: 64Mi }
- name: cache
emptyDir: { sizeLimit: 256Mi }
To find the paths quickly, run the container once without the flag and diff the filesystem: docker container diff <id> lists every file the process created or changed.
Port 80 is the other classic. A non-root process gets bind: permission denied on ports below 1024 unless the kernel's net.ipv4.ip_unprivileged_port_start has been lowered. Adding NET_BIND_SERVICE to capabilities.add usually does not help a non-root process, because Kubernetes does not set ambient capabilities (kubernetes/kubernetes#56374, still open). Your options, best first:
# 1. Listen on 8080 and let the Service expose 80
apiVersion: v1
kind: Service
metadata: { name: web, namespace: shop }
spec:
selector: { app: web }
ports:
- port: 80
targetPort: 8080
---
# 2. Or lower the unprivileged port floor per pod (a "safe" sysctl, allowed by Baseline and Restricted)
spec:
securityContext:
sysctls:
- name: net.ipv4.ip_unprivileged_port_start
value: "0"
On containerd 2.x you may not need either: its CRI plugin sets enable_unprivileged_ports = true by default, which applies that sysctl to non-host-network pods. The containerd CRI config docs note it defaulted to false before 2.0, and CRI-O leaves it alone unless you add it to default_sysctls. That is why the same image binds port 80 on one cluster and fails on another.
Step 7: Fix "violates PodSecurity restricted:latest"
If the namespace enforces Restricted, the API server rejects the pod before the kubelet sees it. For a Deployment, the Deployment itself is accepted (with a warning only if the namespace also has a warn label) and the ReplicaSet fails to create pods, so look at its events. The reasons are fixed strings from the pod-security-admission checks:
Error from server (Forbidden): pods "web" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "web" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "web" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "web" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "web" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
The block that clears all four:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: web
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
Before enforcing, test a namespace without breaking it. A server-side dry run of the label shows every existing pod that would fail:
kubectl label --dry-run=server --overwrite ns shop \
pod-security.kubernetes.io/enforce=restricted
# expected: Warning: existing pods in namespace "shop" violate the new PodSecurity enforce level "restricted:latest"
For policy-as-code instead of labels, see How to Enforce Pod Security Standards with Kyverno.
Step 8: When the image really needs root, use hostUsers: false
Some images cannot be made non-root cheaply: they apt install at start, or chown files at boot. Since v1.36, user namespaces are GA. Root inside the pod maps to an unprivileged, unique UID range on the node, and Pod Security Admission skips the runAsNonRoot and runAsUser checks for these pods, per the user namespaces docs.
spec:
hostUsers: false # pod spec field, not inside securityContext
securityContext:
seccompProfile: { type: RuntimeDefault }
containers:
- name: legacy
image: registry.example.com/legacy:3.2.0
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
Requirements: Linux 6.3+ on the node, containerd 2.0+ or CRI-O 1.25+, runc 1.2+ or crun 1.9+, and no hostNetwork, hostPID or hostIPC. Check from inside: kubectl exec ... -- cat /proc/self/uid_map shows a mapping like 0 1835008 65536 (the host start is always a multiple of 65536) instead of 0 0 4294967295.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
CreateContainerConfigError, "image will run as root" |
Image has no USER, runAsUser unset |
Numeric USER in Dockerfile or runAsUser |
| "image has non-numeric user (node)" | USER node or another name |
USER 1000:1000 or runAsUser: 1000 |
| "runAsUser breaks non-root policy" | runAsUser: 0 with runAsNonRoot: true |
Remove the 0 or use hostUsers: false |
Permission denied writing to a PVC |
Volume owned by root | fsGroup plus fsGroupChangePolicy: OnRootMismatch |
fsGroup set, ownership unchanged |
CSI fsGroupPolicy: None, RWX volume, or hostPath |
Fix ownership server-side or change driver policy |
[Errno 30] Read-only file system |
readOnlyRootFilesystem: true |
emptyDir on each write path |
bind: permission denied on port 80 |
Non-root, port floor 1024, no ambient caps | Use 8080 behind the Service, or the sysctl |
| Pod binds 80 on one cluster, fails on another | containerd 2.x vs CRI-O or containerd 1.7 defaults | Set the sysctl explicitly in the pod |
violates PodSecurity "restricted:latest" |
Missing drop ALL, escalation, seccomp, non-root | Add the four fields from Step 7 |
hostUsers: false pod rejected |
Combined with host namespaces or volumeDevices | Remove them or keep the pod on host users |
FAQ
What does "container has runAsNonRoot and image will run as root" mean?
It means the pod or container sets runAsNonRoot: true, no runAsUser is set, and the image's configured user is UID 0 or empty. The kubelet refuses to create the container and the pod sits in CreateContainerConfigError. Set a numeric non-root USER in the image or a runAsUser in the pod.
Why does USER appuser fail when appuser is not root?
The kubelet only reads the User string from the image config and does not resolve names against /etc/passwd in the image. A name cannot be proven non-root, so it fails with the non-numeric user error. Use the numeric UID instead.
Does fsGroup work on every volume type?
No. It works on most block-backed PVCs, emptyDir, Secrets and ConfigMaps, but CSI drivers can opt out with fsGroupPolicy: None, the default policy skips volumes without an fsType or a ReadWriteOnce access mode, and hostPath is never changed.
Can I just add NET_BIND_SERVICE to bind port 80?
For a container running as root with dropped capabilities, yes. For a non-root process it usually has no effect, because Kubernetes does not set ambient capabilities. Listen on a high port, or rely on net.ipv4.ip_unprivileged_port_start.
When a pod fails like this in production, the first thing I check is docker image inspect --format '{{.Config.User}}' on the exact digest that is deployed, because the usual story is that the image changed and the manifest did not. Then I fix it in the Dockerfile rather than the manifest, so the next chart or the next agent-generated Deployment cannot forget it. Official references: Configure a Security Context, Pod Security Standards, User Namespaces and Seccomp.
Related: How to Fix Kubernetes CrashLoopBackOff: Causes and Commands (2026), How to Enforce Pod Security Standards with Kyverno 1.19 (2026) and Hardened Container Images in 2026: DHI Free, Bitnami Gone, 515 CVEs to 0.
Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Lock Down an SGLang or vLLM Inference Server (2026)
Step-by-step hardening for self-hosted SGLang and vLLM after the NEAR AI breach: bind to loopback, set separate API and admin keys, allowlist endpoints at a proxy, isolate NCCL traffic, and the auth table I generated from SGLang 0.5.23's own code.
11 minOct 31, 2026How to Encrypt Kubernetes Secrets in Git with SOPS and age (2026)
Step-by-step SOPS 3.13.3 and age 1.3.2 setup: generate age keys, write .sops.yaml creation rules, encrypt and edit Secrets, decrypt with Flux kustomize-controller or Argo CD via KSOPS, rotate keys safely, and fix MAC mismatch and no identity matched errors.
11 minOct 31, 2026How to Fix Kubernetes RBAC Forbidden: User Cannot List Resource (2026)
Fix the Kubernetes error 'Error from server (Forbidden): User cannot list resource' step by step: parse the message, check identity with kubectl auth whoami and can-i --as, write a least-privilege Role and RoleBinding, fix service account tokens, EKS access entries, GKE IAM and AKS.
13 min