How to Avoid: Container Runs as Root
July 14, 2026 · 9-minute read · Fairy
The short answer
To prevent container runs as root in AI-generated code, add an explicit USER directive specifying a non-root user in your Dockerfile, and set readOnlyRootFilesystem: true in your Kubernetes security context. AI models often omit these because training data skews toward minimal working examples that prioritize function over security hardening.
The Direct Answer: Add a Non-Root USER Directive
To prevent containers from running as root in AI-generated code, add an explicit USER directive to your Dockerfile specifying a non-root user, and configure readOnlyRootFilesystem: true in your Kubernetes security context. AI models consistently omit these security controls because their training data—public GitHub repositories—overwhelmingly contains development-oriented Dockerfiles where security hardening is absent.
Here's what the fix looks like:
# Before: AI-generated Dockerfile (runs as root)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# After: Production-hardened Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Create non-root user and set ownership
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -s /bin/sh -D appuser && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
This single addition—creating a dedicated user and switching to it—eliminates one of the most common container security failures in AI-generated infrastructure code.
Why AI Produces This Failure Mode
Understanding why AI generates insecure containers helps you anticipate where else it might cut corners on security.
Training Data Reflects Development Practices, Not Production Standards
The vast majority of public Dockerfiles on GitHub are written for local development, tutorials, or quick prototypes. These contexts don't require security hardening—developers are running containers on their own machines, not in production environments where breach containment matters. AI models learn from this distribution and replicate it.
When you prompt an AI to "write a Dockerfile for a Node.js application," it produces what it has seen most often: a minimal configuration that works. Security constraints like USER directives appear in a small minority of training examples, so the model treats them as optional rather than essential.
Minimal Working Examples Omit Security by Design
AI optimizes for the most common interpretation of your request. A Dockerfile that runs the application successfully is "correct" from the model's perspective. Security constraints add complexity without affecting whether the container starts, so they get pruned in favor of simplicity.
This pattern repeats across AI-generated infrastructure code. The model produces configurations that function correctly on the happy path while omitting defense-in-depth measures that only matter when something goes wrong.
The Model Lacks Production Context
AI doesn't know that your container will run in a shared Kubernetes cluster alongside other tenants, or that a vulnerability in your application could cascade into a cluster-wide compromise. It generates code for an abstract environment where these considerations don't exist.
Security hardening requires understanding the deployment context, threat model, and organizational risk tolerance—information that isn't present in a typical prompt. Without explicit guidance, the model defaults to the least constrained configuration.
The Blast Radius Problem: What's Actually at Risk
Running containers as root isn't just a compliance checkbox. It directly affects how much damage an attacker can do after gaining initial access.
Container Escape Becomes Easier
When a container runs as root, exploiting kernel vulnerabilities or container runtime bugs becomes significantly easier. Privilege escalation inside the container is trivial—you're already root. The attacker's next step is escaping to the host, and root access inside the container removes friction from that path.
Filesystem Manipulation Expands Attack Options
A root container can modify any file in its filesystem: inject malicious code into application binaries, tamper with configuration files, or install additional tools for lateral movement. A non-root container with a read-only filesystem limits the attacker to what's already present and executable.
Credential Theft and Secrets Access
Containers often have access to secrets mounted as files or environment variables. A root attacker can read any file in the container, dump process memory, and extract credentials that a restricted user couldn't access. Combined with network access to other services, stolen credentials enable lateral movement.
How to Detect Container-Runs-as-Root in Your Codebase
Before you can fix this failure mode, you need to find it. Here are the detection methods in order of implementation complexity.
Static Analysis: Scan Dockerfiles
Search your codebase for Dockerfiles lacking a USER directive:
# Find Dockerfiles without USER directive
find . -name "Dockerfile*" -exec grep -L "^USER " {} \;
This catches the obvious cases. For more sophisticated detection, tools like Hadolint will flag missing USER directives and suggest fixes:
hadolint Dockerfile
# Output: DL3002 warning: Last USER should not be root
Runtime Inspection: Check Running Containers
For containers already deployed, verify they're not running as root:
# Check user in running container
docker exec <container_id> id
# Desired output: uid=1001(appuser) gid=1001(appgroup)
# Problematic output: uid=0(root) gid=0(root)
In Kubernetes, inspect the security context of running pods:
kubectl get pod <pod_name> -o jsonpath='{.spec.containers[*].securityContext}'
Policy Enforcement: Prevent Deployment
The most reliable detection happens at admission time. Configure your cluster to reject pods that run as root:
# Kubernetes Pod Security Standard: restricted
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
With policy enforcement in place, AI-generated manifests that run as root fail to deploy rather than silently running in production.
The Correct Pattern: Non-Root Container with Read-Only Filesystem
Here's the complete hardened pattern for a production container:
Dockerfile with Non-Root User
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine
WORKDIR /app
# Copy built assets from builder
COPY --from=builder /app/node_modules ./node_modules
COPY . .
# Create non-root user with specific UID/GID
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -s /bin/sh -D appuser && \
chown -R appuser:appgroup /app
# Switch to non-root user
USER 1001
EXPOSE 3000
CMD ["node", "server.js"]
Note the use of numeric UID (1001) in the USER directive rather than the username. This prevents issues when the user doesn't exist in /etc/passwd in some runtime environments.
Kubernetes Security Context
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
containers:
- name: app
image: your-app:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
The readOnlyRootFilesystem: true setting prevents filesystem modifications, while the emptyDir volume provides a writable /tmp for applications that need temporary storage.
Handling Applications That Need Write Access
Some applications legitimately need to write files. Here's how to accommodate them without running as root.
Use emptyDir for Temporary Files
volumeMounts:
- name: cache
mountPath: /app/.cache
- name: tmp
mountPath: /tmp
volumes:
- name: cache
emptyDir: {}
- name: tmp
emptyDir: {}
Set Directory Ownership at Build Time
If your application writes to specific directories, ensure they're owned by the non-root user:
RUN mkdir -p /app/data /app/logs && \
chown -R appuser:appgroup /app/data /app/logs
Use Init Containers for Setup
For cases where files must be written once at startup:
initContainers:
- name: setup
image: busybox
command: ['sh', '-c', 'cp /config-template/* /config/']
volumeMounts:
- name: config
mountPath: /config
Integrating Detection Into Your AI Code Review Process
Finding container security issues before they reach production requires embedding checks into your workflow.
Pre-Commit Hooks
Run Hadolint or similar tools before code is committed:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
CI Pipeline Gates
Block merges when Dockerfiles fail security checks:
# GitHub Actions example
- name: Lint Dockerfile
run: |
hadolint Dockerfile --failure-threshold error
Expert Verification for AI-Generated Infrastructure
AI-generated Dockerfiles and Kubernetes manifests benefit from human review specifically because the security failures are silent. The container runs, the application works, and the vulnerability sits dormant until exploitation.
Fairy for Code provides expert verification for AI-generated infrastructure code, catching container security issues and other failure modes before deployment. When AI produces a minimal Dockerfile, expert reviewers flag the missing security controls and provide the corrected pattern.
Beyond USER: Other Container Security Controls AI Omits
The container-runs-as-root failure is one instance of a broader pattern: AI generates functional configurations while omitting security hardening. Other commonly missing controls include:
Capability dropping: AI rarely includes capabilities: drop: [ALL] even though most containers don't need any Linux capabilities.
Network policies: AI generates deployments without corresponding NetworkPolicy resources that restrict pod-to-pod communication.
Resource limits: Containers without CPU/memory limits can starve other workloads or amplify denial-of-service attacks.
Seccomp profiles: Most AI-generated pods run with the default Docker seccomp profile rather than a restricted profile that blocks dangerous syscalls.
Each of these follows the same pattern: the control adds security without affecting happy-path functionality, so AI omits it.
Building Institutional Memory for Container Security
Catching the same failure mode repeatedly is inefficient. The better approach is building organizational knowledge that prevents recurrence.
Document Your Container Security Standards
Create an internal reference that specifies your required security controls:
## Container Security Requirements
All production containers MUST:
- Run as non-root (UID ≥ 1000)
- Use readOnlyRootFilesystem unless documented exception
- Drop all capabilities
- Specify resource limits
- Include health check endpoints
Provide AI-Friendly Templates
When prompting AI for container configurations, include your security requirements in the prompt:
Write a Dockerfile for a Node.js application with these requirements:
- Multi-stage build
- Non-root user with UID 1001
- Production dependencies only
- Health check endpoint
Use Continuous Oversight
Production infrastructure drifts. Containers that were secure at deployment may regress when AI regenerates configurations or when team members copy non-hardened examples. Fairy Intelligence provides grounded Q&A on your codebase, letting you ask questions like "which Dockerfiles are missing USER directives?" and get accurate answers based on your actual code.
Summary: The Non-Negotiable Controls
To prevent container runs as root in AI-generated code:
- Add a USER directive specifying a non-root user with a numeric UID
- Set runAsNonRoot: true in your Kubernetes security context
- Enable readOnlyRootFilesystem with emptyDir volumes for legitimate write paths
- Drop all capabilities and only add back specific ones if required
- Enforce at admission using Pod Security Standards or admission controllers
- Verify AI output before deployment, either through automated linting or expert review
AI produces containers that run as root because its training data does. Until that changes, treating AI-generated infrastructure as a draft requiring security hardening—rather than production-ready code—keeps your blast radius small when something goes wrong.
Frequently asked questions
Why do AI code generators create containers that run as root?
AI models are trained on public repositories where most Dockerfiles omit USER directives because they're written for local development, not production. The models learn to produce minimal working configurations, not secure ones.
What is the security risk of running containers as root?
A container running as root gives attackers who compromise the application full control over the container filesystem and, in some container escape scenarios, potentially the host system. It expands the blast radius of any vulnerability.
How do I check if my container runs as root?
Run 'docker exec <container> whoami' on a running container. If it returns 'root', the container lacks a non-root USER directive. You can also inspect the Dockerfile for the presence of a USER instruction.
Can I use readOnlyRootFilesystem with all applications?
Most stateless applications work fine with readOnlyRootFilesystem. Applications that need to write temporary files can use emptyDir volumes mounted at specific paths like /tmp. Some legacy applications may require refactoring.
Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.
More resources