Fairy
Resources

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:

  1. Add a USER directive specifying a non-root user with a numeric UID
  2. Set runAsNonRoot: true in your Kubernetes security context
  3. Enable readOnlyRootFilesystem with emptyDir volumes for legitimate write paths
  4. Drop all capabilities and only add back specific ones if required
  5. Enforce at admission using Pod Security Standards or admission controllers
  6. 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