RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Docker Overlay2 Inode Exhaustion Monitor
02. Root Cause Vector

Docker's overlay2 storage driver exhausts filesystem inodes before disk space runs out, killing container spawns and image builds silently.

03. Rapid Prescription

Sort lock acquisition order deterministically, or configure explicit session timeouts.

Zero-Trust Client IDE/ Performance

Docker Overlay2 Inode Exhaustion Monitor

Docker's overlay2 storage driver exhausts filesystem inodes before disk space runs out, killing container spawns and image builds silently.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(79L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# ============================================================================
# legacy-deploy.sh - Deployment script for the PCI-secure-zone legacy API.
# TODO: Refactor this mess after the Q4 migration. Current workaround for the
# overlay2 inode issue is to manually prune images every 2 weeks. - JIRA: OPS-4421
# ============================================================================

# ----------------------------------------------------------------------------
# CONFIGURATION BLOCK (DO NOT MODIFY - managed by Ansible, probably)
# ----------------------------------------------------------------------------
# Old registry for the monolith. New one is my-registry.internal/cloud-native/v2
LEGACY_REGISTRY="my-registry.internal/legacy-api:v3.7.1-stale"
# This is the "temporary" workaround path since /var/lib/docker is full.
# VULNERABILITY: This script creates thousands of small, unmanaged container layers in a loop,
# each consuming an inode, without cleanup, leading to overlay2 inode exhaustion.
WORK_DIR="/mnt/tmp/ephemeral-storage/legacy-app-cache"

# ----------------------------------------------------------------------------
# SECRETS INJECTION (Pulled from Vault during runtime - these are DUMMY values for local dev)
# ----------------------------------------------------------------------------
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
export DB_CONNECTION_URI="postgresql://legacy_user:SuperSecretPass123!@pci-db.internal:5432/secure_zone_db?sslmode=require"

# ----------------------------------------------------------------------------
# MOCK RSA PRIVATE KEY (Embedded for SFTP backup - DUMMY KEY)
# ----------------------------------------------------------------------------
SSH_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArX8zJ8Z5vR0KjJ8bG7vJ6sT3mY6pLk9wQ2aH7fT5qW3nM7\n... (truncated for brevity) ...\n-----END RSA PRIVATE KEY-----"

# ----------------------------------------------------------------------------
# MAIN DEPLOYMENT LOGIC WITH RACE CONDITION AND LEAKY LAYERS
# ----------------------------------------------------------------------------
# This loop is supposed to spawn canary containers for health checks.
# TODO: The sleep 0.1 introduces a subtle race; combined with no cleanup, it fills inodes.
for i in {1..5000}; do
    # Create a unique, tiny config file for each container (consumes an inode).
    echo "{\"id\": $i, \"status\": \"pending\"}" > "$WORK_DIR/config_$i.json"
    
    # Legacy command to run a container with the config bind-mounted.
    # Note: Using --rm flag was removed due to "stability issues" in v2.1.
    docker run -d \
        --name "legacy-canary-$i" \
        -v "$WORK_DIR/config_$i.json:/app/config.json" \
        "$LEGACY_REGISTRY" \
        /app/start.sh --config /app/config.json
    
    # Sleep to avoid overwhelming the scheduler (causes slow accumulation).
    sleep 0.1
    
    # We should stop and remove the container after the check, but the cleanup
    # script is commented out due to a bug with the logging driver.
    # docker stop "legacy-canary-$i"  # TODO: Re-enable after fixing journald
    # docker rm "legacy-canary-$i"    # OPS-5112: Removal causes segfaults
    
    # Old logging line, kept for audit.
    # logger -t legacy-deploy "Started canary $i"
    
    if (( i % 100 == 0 )); then
        echo "INFO: Launched $i canary containers. Work dir size:" $(du -sh $WORK_DIR)
        # This df shows block space, not inodes, misleading the ops team.
        df -h /mnt/tmp/ephemeral-storage
    fi
done

# ----------------------------------------------------------------------------
# CLEANUP SECTION (NEVER EXECUTED due to early return in prod)
# ----------------------------------------------------------------------------
# This function is defined but the script exits earlier in the production flow.
cleanup_containers() {
    echo "Cleaning up containers..."
    docker ps -a --filter "name=legacy-canary-" --format "{{.Names}}" | xargs -r docker rm -f
    rm -rf $WORK_DIR/*.json
}
# cleanup_containers  # Uncomment after the Q4 release

echo "Deployment script completed (or so we think). Check /var/log/legacy-app.log for errors."
# Actual exit. The cleanup is never called, leaving layers and inodes occupied.
exit 0
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix Docker Overlay2 Inode Exhaustion: No Space Left on Device

Threat/Impact Level: CRITICAL | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–30 mins

TL;DR

  • What broke: The filesystem hosting /var/lib/docker/overlay2 has zero free inodes. Docker cannot create new layer directories, so all docker run, docker build, and container restarts fail with no space left on device even though df -h shows available disk space.
  • How to fix it: Prune dead overlay2 layers, dangling images, and stopped containers immediately; long-term, reformat the partition with a higher inode density or migrate to xfs with dynamic inode allocation.
  • CTA: Use our Client-Side Sandbox above to paste your docker info and df -i output — it will auto-diagnose whether you're inode-exhausted or block-exhausted and generate the corrected daemon.json.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw error surface — you'll see this in dockerd logs, journalctl -u docker, or directly in your CI runner output:

error creating overlay mount to /var/lib/docker/overlay2/b3f9.../merged: 
  too many open files in system

Error response from daemon: 
  failed to create rootfs: 
  mkdir /var/lib/docker/overlay2/c7a1.../ : 
  no space left on device

[containerd] failed to reserve snapshot: 
  write /var/lib/docker/overlay2/...: 
  no space left on device

The trap: df -h returns 40% free. Engineers waste 20 minutes looking at disk blocks. The real culprit is inodes:

$ df -i /var/lib/docker
Filesystem      Inodes   IUsed  IFree IUse% Mounted on
/dev/xvda1     1310720 1310720      0  100% /

IFree: 0. Every overlay2 layer directory, every file inside a container layer, every whiteout file consumes one inode. On a default ext4 partition formatted with mkfs.ext4 defaults (~1 inode per 16KB), a host running hundreds of containers with thousands of small files will hit inode exhaustion long before block exhaustion.

Immediate consequence: No new containers start. Running containers that need to write new files to their writable layer may also begin failing. docker build aborts mid-layer. Kubernetes nodes report NodeNotReady if the container runtime cannot create pod sandboxes.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is a cascading infrastructure failure, not an isolated Docker issue.

Blast radius chain:

  1. dockerd cannot mkdir new overlay2 merge directories → all docker run calls return non-zero exit codes.
  2. Kubernetes kubelet CRI calls to containerd fail → pods stuck in ContainerCreating indefinitely.
  3. Liveness/readiness probes on existing pods begin failing if those containers attempt filesystem writes (logging, temp files, pid files).
  4. CI/CD pipelines (GitHub Actions self-hosted, GitLab Runner, Jenkins agents) fail every job — your entire deployment pipeline is down.
  5. Log rotation daemons (logrotate, fluentd) fail to create new log files → logs lost or processes crash.
  6. If this host is a Kubernetes node, the node taints itself node.kubernetes.io/disk-pressure and the scheduler stops placing new pods on it — but existing pods may also be evicted depending on eviction thresholds.

Why it's insidious: Monitoring alerts on disk block usage (the standard) will show green. Only an explicit df -i check or an inode-specific alert catches this. Most teams don't have it.


How to Fix It (The Solution)

Step 0: Confirm It's Inodes, Not Blocks

# Check block usage
df -h /var/lib/docker

# Check inode usage — this is the one that's 100%
df -i /var/lib/docker

# Find the top inode consumers under overlay2
find /var/lib/docker/overlay2 -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -20

Basic Fix: Emergency Inode Reclamation

# 1. Remove all stopped containers (each container = multiple overlay2 dirs)
docker container prune -f

# 2. Remove dangling images (untagged layers)
docker image prune -f

# 3. Nuclear option if you can afford downtime — removes ALL unused objects
docker system prune -a -f --volumes

# 4. Verify inodes freed
df -i /var/lib/docker

# 5. If dockerd is still wedged, restart it
systemctl restart docker

⚠️ docker system prune -a removes ALL images not referenced by a running container. Only run this if you can re-pull images. On a CI runner, this is usually safe.

Enterprise Best Practice: Prevent Recurrence

Option A — Reformat with higher inode density (ext4)

This requires migrating /var/lib/docker to a dedicated partition:

- mkfs.ext4 /dev/xvdb
+ mkfs.ext4 -N 10000000 /dev/xvdb
# -N sets absolute inode count. For a 100GB volume, default is ~6.5M.
# Set 10M+ for Docker hosts with high container churn.

Option B — Use XFS (recommended for Docker hosts)

XFS allocates inodes dynamically from free space. It does not pre-allocate a fixed inode table.

- mkfs.ext4 /dev/xvdb
+ mkfs.xfs /dev/xfs
# XFS grows its inode table dynamically. Inode exhaustion before block
# exhaustion is practically impossible under normal Docker workloads.

Update /etc/fstab and move your Docker data:

mkfs.xfs /dev/xvdb
mount /dev/xvdb /mnt/docker-new
systemctl stop docker
rsync -aHAX /var/lib/docker/ /mnt/docker-new/
umount /mnt/docker-new
# Update /etc/fstab to mount /dev/xvdb at /var/lib/docker
systemctl start docker

Option C — Tune daemon.json to limit layer accumulation

 {
-  "storage-driver": "overlay2"
+  "storage-driver": "overlay2",
+  "storage-opts": [
+    "overlay2.size=20G"
+  ],
+  "log-driver": "json-file",
+  "log-opts": {
+    "max-size": "10m",
+    "max-file": "3"
+  }
 }

Note: overlay2.size requires the backing filesystem to support d_type (XFS with ftype=1 or ext4). This limits each container's writable layer size, preventing runaway writes that generate massive small-file counts.


💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Add Inode Monitoring to Your Alerting Stack

Prometheus + node_exporter (this is the one alert most teams are missing):

# alerts/docker-inodes.yaml
groups:
  - name: inode_exhaustion
    rules:
      - alert: FilesystemInodesExhaustionWarning
        expr: node_filesystem_files_free{mountpoint="/var/lib/docker"} / node_filesystem_files{mountpoint="/var/lib/docker"} < 0.15
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Docker overlay2 inode usage above 85% on {{ $labels.instance }}"

      - alert: FilesystemInodesExhaustionCritical
        expr: node_filesystem_files_free{mountpoint="/var/lib/docker"} / node_filesystem_files{mountpoint="/var/lib/docker"} < 0.05
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Docker overlay2 inode exhaustion imminent on {{ $labels.instance }}"

2. Scheduled Prune in CI/CD Runners

For self-hosted GitHub Actions runners or GitLab Runner hosts, add a cron job:

# /etc/cron.d/docker-prune
0 2 * * * root docker system prune -f --filter "until=24h" >> /var/log/docker-prune.log 2>&1

3. Terraform / Packer: Enforce XFS at Provisioning Time

-data_device_type = "ext4"
+data_device_type = "xfs"
 # In your Packer AMI builder or Terraform aws_instance user_data,
 # always format the Docker data volume as XFS.

4. Checkov / OPA Policy (Kubernetes)

Enforce that DaemonSets running on Docker nodes include inode monitoring sidecars or that node pools use XFS-formatted instance types via a custom OPA Gatekeeper constraint:

# opa/policies/docker_node_filesystem.rego
package kubernetes.admission

deny[msg] {
  input.request.kind.kind == "NodePool"
  not input.request.object.spec.config.diskType == "pd-ssd-xfs"
  msg := "NodePool must use XFS-formatted SSD to prevent overlay2 inode exhaustion"
}

5. Dockerfile Best Practices to Reduce Inode Pressure

-RUN apt-get update
-RUN apt-get install -y curl
-RUN apt-get clean
+# Collapse RUN commands — each RUN creates a new layer with its own inode tree
+RUN apt-get update && \
+    apt-get install -y curl && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*

Each RUN instruction in a Dockerfile creates a separate overlay2 layer directory. Collapsing them reduces the total inode footprint per image by 60–80% in package-heavy images.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does 'df -h' show free space but Docker still says 'no space left on device'?
`df -h` reports block (byte) usage. Docker's overlay2 error is about inode exhaustion, which is a separate resource. Run `df -i /var/lib/docker` to see inode usage. If IUse% is 100%, you've hit the inode limit regardless of how many gigabytes are free. ext4 pre-allocates a fixed number of inodes at format time; once they're gone, no new files or directories can be created on that filesystem. This is a fundamental architectural mismatch between Docker's layered filesystem and traditional ext4.
Q2.How do I find which containers or images are consuming the most inodes?
You can attempt to run `find /var/lib/docker/overlay2 -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head -30` to identify the overlay2 layer directories with the highest file counts. Cross-referencing these hashes manually with `docker inspect` is highly error-prone, risks deleting critical system layers, and exposes your host to security vulnerabilities if you misidentify a layer. Instead, drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that isolates and remediates the offending layer without host-level risk.
Q3.Should I use XFS or ext4 for Docker hosts in production?
XFS is the correct choice for Docker hosts with high container churn as it allocates inodes dynamically, making inode exhaustion effectively impossible. However, migrating filesystems at scale across large mono-repos or multi-cloud fleets is a compliance and operational nightmare, requiring downtime and complex data migration. For automated pull request generation with Maker-Checker review and fleet-wide policy enforcement, upgrade to the StackEngine CI/CD Automator (Team Edition), which provides auditable, automated fixes without manual host intervention.