---
# === IDENTITY ===
id: software/debugging/docker-oomkilled/2026
canonical_question: "How do I diagnose and fix Docker OOMKilled errors?"
aliases:
  - "Docker OOMKilled"
  - "Docker exit code 137"
  - "Docker out of memory"
  - "Docker container killed OOM"
  - "Docker memory limit exceeded"
  - "Docker SIGKILL 137"
  - "Docker cgroup out of memory"
  - "Docker container OOM killer"
entity_type: software_reference
domain: software > debugging > docker_oomkilled
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.94
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Docker Engine 29 (2026) — cgroup v1 formally deprecated (support continues until May 2029); default nofile ulimit dropped from 1,048,576 to 1,024 (containerd v2.1.5)"
  next_review: 2026-11-13
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Never use --oom-kill-disable on cgroups v2 hosts — the flag is silently discarded in Docker Engine 27+; the container will still be OOM-killed"
  - "JVM heap (-Xmx) must be set to 70-80% of the container memory limit — setting it equal to --memory guarantees OOMKilled because metaspace, threads, and GC buffers need the remaining 20-30%"
  - "On Docker Desktop (Mac/Windows), total container memory cannot exceed the VM allocation (default ~50% of host RAM) regardless of per-container --memory flags"
  - "Exit code 137 does not always mean OOM — always confirm with docker inspect --format='{{.State.OOMKilled}}' before assuming a memory issue"
  - "The --kernel-memory flag is discarded on cgroups v2 — do not rely on it for kernel memory limits on modern Linux hosts"
  - "On Kubernetes 1.28+ with cgroups v2, the OOM killer terminates ALL processes in the cgroup, not just the one that exceeded the limit — this differs from Docker standalone behavior"
  - "Docker Engine 29 (2026) formally deprecates cgroup v1 (support until May 2029) — migrate hosts to cgroup v2 before upgrading or memory limits may behave differently than expected"
  - "Docker Engine 29 dropped the default nofile (open-files) ulimit from 1,048,576 to 1,024 — programs that pre-allocate buffers per FD can now consume far less memory; override with --ulimit nofile=... if you actually need the old limit"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Container exits with code 137 but OOMKilled is false"
    use_instead: "Check for docker kill, docker stop timeout, or orchestrator-initiated SIGKILL — this is not an OOM issue"
  - condition: "Kubernetes pod is OOMKilled (not standalone Docker)"
    use_instead: "software/debugging/kubernetes-crashloopbackoff/2026"
  - condition: "Application is slow but not crashing with exit 137"
    use_instead: "CPU or I/O profiling — memory is not the bottleneck"

# === AGENT HINTS ===
inputs_needed:
  - key: runtime_environment
    question: "Is the container running on Docker Engine (Linux), Docker Desktop (Mac/Windows), or Kubernetes?"
    type: choice
    options: ["Docker Engine (Linux)", "Docker Desktop (Mac/Windows)", "Kubernetes"]
  - key: application_runtime
    question: "What language runtime is the containerized application using?"
    type: choice
    options: ["Java/JVM", "Node.js", "Python", "Go", ".NET", "Other"]
  - key: memory_limit_set
    question: "Was a memory limit configured with --memory or in the Compose/K8s resource spec?"
    type: choice
    options: ["Yes — I know the limit", "No limit set", "Not sure"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/debugging/docker-oomkilled/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/debugging/docker-container-wont-start/2026"
      label: "Docker Container Won't Start"
    - id: "software/debugging/python-memory-leaks/2026"
      label: "Python Memory Leaks"
    - id: "software/debugging/nodejs-memory-leaks/2026"
      label: "Node.js Memory Leaks"
  solves:
    - id: "software/debugging/kubernetes-crashloopbackoff/2026"
      label: "Kubernetes CrashLoopBackOff"
  often_confused_with:
    - id: "software/debugging/docker-container-wont-start/2026"
      label: "Docker Container Won't Start (exit 137 without OOM)"

# === SOURCES (5-8 authoritative sources) ===
sources:
  - id: src1
    title: "Docker — Resource constraints (memory limits)"
    author: Docker Inc.
    url: https://docs.docker.com/engine/containers/resource_constraints/
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src2
    title: "Docker — docker stats"
    author: Docker Inc.
    url: https://docs.docker.com/reference/cli/docker/container/stats/
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src3
    title: "Docker — docker inspect"
    author: Docker Inc.
    url: https://docs.docker.com/reference/cli/docker/inspect/
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src4
    title: "StackState — Docker Exit Code 137 Explained"
    author: StackState
    url: https://www.stackstate.com/blog/understanding-and-troubleshooting-out-of-memory-error-code-137/
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src5
    title: "Netdata — Diagnosing Linux cgroups v2 Memory Throttling & OOM-Killed Containers"
    author: Netdata
    url: https://www.netdata.cloud/academy/diagnosing-linux-cgroups/
    type: technical_blog
    published: 2025-01-01
    reliability: high
  - id: src6
    title: "Linux Kernel — cgroups v2 memory controller"
    author: Linux Kernel
    url: https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
    type: official_docs
    published: 2025-01-01
    reliability: authoritative
  - id: src7
    title: "Docker Engine v27 Release Notes"
    author: Docker Inc.
    url: https://docs.docker.com/engine/release-notes/27/
    type: official_docs
    published: 2024-06-01
    reliability: authoritative
  - id: src8
    title: "The Complete Guide to Docker Resource Limits"
    author: BetterLink Blog
    url: https://eastondev.com/blog/en/posts/dev/20251218-docker-resource-limits-guide/
    type: technical_blog
    published: 2025-12-18
    reliability: high
  - id: src9
    title: "Docker Engine v28 Release Notes"
    author: Docker Inc.
    url: https://docs.docker.com/engine/release-notes/28/
    type: official_docs
    published: 2025-02-19
    reliability: authoritative
  - id: src10
    title: "Docker Engine v29 Release Notes"
    author: Docker Inc.
    url: https://docs.docker.com/engine/release-notes/29/
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
---

# How to Diagnose and Fix Docker OOMKilled Errors

## TL;DR

- **Bottom line**: OOMKilled means the Linux kernel's OOM killer terminated your container because it exceeded its memory limit (cgroup-enforced) or the host ran out of memory. Exit code 137 = 128 + SIGKILL (signal 9). Check `docker inspect --format='{{.State.OOMKilled}}'` -- if `true`, the container was killed for memory, not manually stopped.
- **Key tool/command**: `docker stats` shows live memory usage. `docker inspect <cid> --format='{{.State.OOMKilled}}'` confirms OOM. `docker run --memory=512m` sets limits. `dmesg | grep -i oom` shows kernel OOM events on the host.
- **Watch out for**: `--memory` without `--memory-swap` defaults swap to 2x memory (Docker Engine) or no swap (some platforms). Java's `-Xmx` must be set lower than `--memory` to leave room for non-heap memory. Node's `--max-old-space-size` similarly needs headroom. On cgroups v2, `--oom-kill-disable` is silently discarded.
- **Works with**: Docker Engine 20.10+ (cgroups v1/v2), Docker Desktop 4.x, Podman, Docker Compose, Kubernetes (same OOM concept with pod `resources.limits.memory`).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never use `--oom-kill-disable` on cgroups v2 hosts -- the flag is silently discarded in Docker Engine 27+; the container will still be OOM-killed [src7]
- JVM heap (`-Xmx`) must be set to 70-80% of the container memory limit -- setting it equal to `--memory` guarantees OOMKilled because metaspace, threads, and GC buffers need the remaining 20-30% [src1, src4]
- On Docker Desktop (Mac/Windows), total container memory cannot exceed the VM allocation (default ~50% of host RAM) regardless of per-container `--memory` flags [src1]
- Exit code 137 does not always mean OOM -- always confirm with `docker inspect --format='{{.State.OOMKilled}}'` before assuming a memory issue [src3, src4]
- The `--kernel-memory` flag is discarded on cgroups v2 -- do not rely on it for kernel memory limits on modern Linux hosts [src7]
- On Kubernetes 1.28+ with cgroups v2, the cgroup-aware OOM killer terminates ALL processes in the cgroup, not just the one that exceeded the limit [src6]
- Docker Engine 29 (2026) formally deprecates cgroup v1 -- support continues until May 2029, but plan host migration to cgroup v2 now [src10]
- Docker Engine 29 dropped the default `nofile` ulimit from 1,048,576 to 1,024 (containerd v2.1.5) -- programs that adjust behavior based on this limit will allocate much less memory by default [src10]

## Quick Reference

| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | Container memory limit too low | ~30% of cases | `OOMKilled: true`; app uses expected memory | Increase `--memory` limit [src1, src4] |
| 2 | Application memory leak | ~25% of cases | Memory grows linearly over time; OOM after hours/days | Profile and fix the leak (see language-specific guides) [src4, src8] |
| 3 | JVM heap exceeds container limit | ~15% of cases | Java app; `-Xmx` not set or set higher than `--memory` | Set `-Xmx` to 70-80% of container memory limit [src1, src4] |
| 4 | Large file/data processing | ~8% of cases | OOM during specific operations (CSV parsing, image processing) | Use streaming/chunked processing [src8] |
| 5 | Fork bomb / child process explosion | ~5% of cases | Memory spikes instantly; many processes | Set `--pids-limit`; fix fork logic [src1] |
| 6 | Host itself is out of memory | ~5% of cases | Multiple containers OOMKilled simultaneously; `dmesg` shows OOM | Add host RAM; reduce container count; set limits [src4, src6] |
| 7 | No memory limit + host exhaustion | ~4% of cases | No `--memory` flag; host runs out | Always set memory limits in production [src1] |
| 8 | Memory-mapped files / tmpfs | ~3% of cases | Container uses tmpfs or mmap that counts against limit | Exclude tmpfs from memory limit or increase limit [src1] |
| 9 | Swap disabled or misconfigured | ~3% of cases | Container OOMs at exactly the `--memory` value | Configure `--memory-swap` appropriately [src1] |
| 10 | Build-time OOM (docker build) | ~2% of cases | OOM during `npm install`, `pip install`, or compilation | Increase Docker Desktop memory or use `--build-arg` to limit parallelism [src1] |

## Decision Tree

> Full script: [decision-tree.txt](scripts/decision-tree.txt) (27 lines)

```
START -- Container exits with code 137
|-- Is OOMKilled true? (docker inspect --format='{{.State.OOMKilled}}')
|   |-- YES -> Out of memory (continue below)
|   +-- NO -> Not OOM -- killed by docker kill, docker stop timeout, or orchestrator
|       |-- docker stop timeout? -> App doesn't handle SIGTERM -> add signal handler
|-- Was a memory limit set? (docker inspect --format='{{.HostConfig.Memory}}')
|   |-- YES -> Container exceeded this limit
|   |   |-- Is the limit too low for expected usage? -> Increase --memory [src1]
|   |   |-- Memory grows over time (leak)? -> Profile the application [src4, src8]
|   |   +-- Java/JVM? -> Check -Xmx (set to 70-80% of --memory) [src1]
|   +-- NO -> Host ran out of memory
|       |-- Check host: free -m, dmesg | grep oom [src6]
|       +-- Set limits on all containers [src1]
|-- cgroups version? (stat -fc %T /sys/fs/cgroup)
|   |-- cgroup2fs -> cgroups v2: check memory.max, memory.events [src6]
|   +-- tmpfs -> cgroups v1: check memory.limit_in_bytes, memory.oom_control [src6]
|-- Build-time OOM? -> Increase Docker Desktop memory or reduce build parallelism [src1]
+-- Monitor with docker stats to find peak usage [src2]
    |-- Peak near limit? -> Increase limit by 20-30%
    +-- Sudden spike? -> Profile that code path
```

## Step-by-Step Guide

### 1. Confirm the OOM kill

Not every exit code 137 is OOM -- it can also be a manual `docker kill`. [src3, src4]

```bash
# Check if container was OOM killed
docker inspect <container_id> --format='{{.State.OOMKilled}}'
# true = OOM killed, false = killed by other means

# Full state info
docker inspect <container_id> --format='{{json .State}}' | python -m json.tool

# Check host kernel OOM events
dmesg | grep -i "oom\|out of memory" | tail -20

# Check specific cgroup events (cgroups v2)
cat /sys/fs/cgroup/system.slice/docker-<container_full_id>.scope/memory.events
# Look for "oom" and "oom_kill" counters

# Check specific cgroup events (cgroups v1 — legacy)
cat /sys/fs/cgroup/memory/docker/<container_full_id>/memory.oom_control
```

**Verify**: `OOMKilled: true` confirms the container was killed for exceeding memory.

### 2. Check current memory limits

Understand what limits are configured. [src1, src3]

```bash
# Check container memory limit
docker inspect <container_id> --format='Memory: {{.HostConfig.Memory}} bytes, MemorySwap: {{.HostConfig.MemorySwap}} bytes'

# Human-readable
docker inspect <container_id> --format='{{.HostConfig.Memory}}' | awk '{print $1/1024/1024 "MB"}'

# Check all running containers' limits
docker ps -q | xargs docker inspect --format='{{.Name}} Memory: {{.HostConfig.Memory}}' 2>/dev/null

# Docker Compose: check resolved config
docker compose config | grep -A5 "mem_limit\|memory"

# Check cgroups version on host
stat -fc %T /sys/fs/cgroup
# cgroup2fs = v2, tmpfs = v1
```

**Verify**: Know the exact limit (0 = unlimited, which means host memory is the only limit).

### 3. Monitor live memory usage

Use `docker stats` to see real-time memory consumption. [src2]

```bash
# Live stats for all containers
docker stats

# Specific container
docker stats <container_id>

# One-shot (no streaming)
docker stats --no-stream

# Formatted output
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.PIDs}}"

# Record stats over time (for later analysis)
while true; do
    docker stats --no-stream --format '{{.Name}},{{.MemUsage}},{{.MemPerc}}' >> memory_log.csv
    sleep 5
done
```

**Verify**: Watch memory over time -- gradual growth indicates a leak; spikes indicate load-dependent issues.

### 4. Set appropriate memory limits

Configure limits based on observed usage plus headroom. [src1]

```bash
# Run with memory limit
docker run --memory=512m --memory-swap=1g myapp

# Memory limit only (swap = 2x memory by default on cgroups v1)
docker run --memory=512m myapp

# Disable swap entirely
docker run --memory=512m --memory-swap=512m myapp

# Soft limit (reservation) -- preferred allocation, not hard cap
docker run --memory=1g --memory-reservation=512m myapp

# Docker Compose
# docker-compose.yml:
```

```yaml
services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M
    # Or legacy syntax (pre-deploy):
    mem_limit: 512m
    memswap_limit: 1g
```

**Verify**: Container runs without OOM under normal workload; `docker stats` shows usage below the limit.

### 5. Configure JVM / Node.js / Python memory

Runtime-specific memory settings must align with container limits. [src1, src4]

```bash
# Java / JVM -- Set heap to 70-80% of container memory
docker run --memory=1g myapp java -Xmx768m -Xms256m -jar app.jar

# Java 10+ (container-aware) -- automatically uses container limits
docker run --memory=1g myapp java -XX:MaxRAMPercentage=75.0 -jar app.jar

# Node.js -- Set old space to ~75% of container memory
docker run --memory=512m myapp node --max-old-space-size=384 app.js

# Python -- No built-in limit, but control with resource module
docker run --memory=1g myapp python -c "
import resource
# Soft limit at 900MB
resource.setrlimit(resource.RLIMIT_AS, (900*1024*1024, 1024*1024*1024))
"
```

**Verify**: Application starts and runs within its allocated memory budget.

### 6. Profile memory usage inside the container

Get detailed memory breakdown to find what's consuming memory. [src4, src8]

```bash
# Inside the container -- show process memory
docker exec <cid> ps aux --sort=-%mem | head -20

# Memory info (Linux)
docker exec <cid> cat /proc/meminfo | head -10

# Process-specific memory
docker exec <cid> cat /proc/1/status | grep -E "VmRSS|VmSize|VmPeak"

# Memory map of PID 1
docker exec <cid> cat /proc/1/smaps_rollup 2>/dev/null || \
docker exec <cid> cat /proc/1/status | grep Vm

# OOM score (higher = more likely to be killed)
docker exec <cid> cat /proc/1/oom_score

# cgroups v2: check memory pressure and events
docker exec <cid> cat /sys/fs/cgroup/memory.pressure 2>/dev/null
docker exec <cid> cat /sys/fs/cgroup/memory.events 2>/dev/null
```

**Verify**: Identify which process or allocation is consuming the most memory.

### 7. Set up memory alerts

Proactive monitoring to catch issues before OOM kills. [src5]

```bash
# Simple monitoring script
#!/bin/bash
THRESHOLD=80  # Alert at 80% memory usage

while true; do
    docker stats --no-stream --format '{{.Name}} {{.MemPerc}}' | while read name pct; do
        pct_num=$(echo "$pct" | tr -d '%')
        if (( $(echo "$pct_num > $THRESHOLD" | bc -l) )); then
            echo "WARNING $(date): $name at ${pct}% memory"
        fi
    done
    sleep 30
done
```

```yaml
# Docker Compose with healthcheck memory awareness
services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          memory: 512M
    healthcheck:
      test: ["CMD-SHELL", "test $(cat /sys/fs/cgroup/memory.current 2>/dev/null || cat /sys/fs/cgroup/memory/memory.usage_in_bytes 2>/dev/null) -lt 450000000"]
      interval: 30s
      retries: 3
```

**Verify**: Alerts fire before OOM kills occur, giving time to investigate.

## Code Examples

### Container memory diagnostics script

> Full script: [container-memory-diagnostics-script.sh](scripts/container-memory-diagnostics-script.sh) (43 lines)

```bash
#!/bin/bash
# Input:  Container ID or name
# Output: Complete memory diagnostic report
CID="$1"
if [ -z "$CID" ]; then echo "Usage: $0 <container_id>"; exit 1; fi
# ... (see full script)
```

### Docker Compose with proper memory management

> Full script: [docker-compose-with-proper-memory-management.yml](scripts/docker-compose-with-proper-memory-management.yml) (62 lines)

```yaml
# Input:  Multi-service app with OOM issues
# Output: Compose file with memory limits, reservations, and health checks
version: "3.8"
services:
  api:
# ... (see full script)
```

### Memory leak detection in running container

> Full script: [memory-leak-detection-in-running-container.py](scripts/memory-leak-detection-in-running-container.py) (50 lines)

```python
#!/usr/bin/env python3
# Input:  Container name to monitor
# Output: Alert when memory growth exceeds threshold
import subprocess
import json
# ... (see full script)
```

## Anti-Patterns

### Wrong: No memory limit in production

```bash
# BAD -- container can consume all host memory and OOM other containers [src1]
docker run -d myapp
# No --memory flag = unlimited = host OOM risk
```

### Correct: Always set memory limits

```bash
# GOOD -- constrained to 512MB, protects other containers and host [src1]
docker run -d --memory=512m --memory-swap=1g myapp
```

### Wrong: JVM heap equals container memory

```bash
# BAD -- JVM needs memory for non-heap too (metaspace, threads, GC) [src1, src4]
docker run --memory=1g myapp java -Xmx1g -jar app.jar
# Container OOMs because JVM uses 1g heap + ~200-400MB non-heap
```

### Correct: JVM heap at 70-80% of container memory

```bash
# GOOD -- leaves 25-30% for non-heap memory [src1, src4]
docker run --memory=1g myapp java -XX:MaxRAMPercentage=75.0 -jar app.jar
# Or explicitly: -Xmx768m leaves ~256MB for metaspace, threads, buffers
```

### Wrong: Restarting OOM containers without fixing root cause

```yaml
# BAD -- container restarts, leaks again, OOMs again -- infinite loop [src4]
services:
  app:
    restart: always  # Keeps restarting a leaking container
```

### Correct: Limit restarts and investigate

```yaml
# GOOD -- stops after 5 failures, forcing investigation [src4]
services:
  app:
    restart: on-failure:5
    deploy:
      resources:
        limits:
          memory: 512M
    logging:
      driver: json-file
      options:
        max-size: "10m"  # Prevent log files from consuming memory too
```

### Wrong: Using --oom-kill-disable on cgroups v2

```bash
# BAD -- flag is silently discarded on cgroups v2 (Docker 27+) [src7]
docker run --memory=512m --oom-kill-disable myapp
# Container WILL still be OOM-killed; the flag gives false confidence
```

### Correct: Set appropriate limits and monitor instead

```bash
# GOOD -- right-size the limit and monitor proactively [src1, src5]
docker run --memory=512m --memory-reservation=384m myapp
# Use docker stats or Prometheus/cAdvisor to alert before OOM
```

## Decision Logic

### If `docker inspect --format='{{.State.OOMKilled}}'` returns `true`
--> Container was OOM-killed. Proceed with this unit's Step-by-Step Guide; start by checking the configured `--memory` limit and live `docker stats` peak usage. [src1, src3]

### If exit code is 137 but `OOMKilled` is `false`
--> Not an OOM issue. The container was killed by `docker kill`, a `docker stop` timeout, or an orchestrator-initiated SIGKILL -- skip this unit and check signal handlers + termination policies. [src3, src4]

### If application is Java/JVM and `--memory` is set
--> Set `-XX:MaxRAMPercentage=75.0` (Java 10+) OR `-Xmx` to ~75% of `--memory`. Setting `-Xmx == --memory` guarantees OOMKilled because metaspace, threads, and GC buffers need the remaining 20-30%. [src1, src4]

### If host is running Docker Engine 29+
--> Verify the host is on cgroup v2 (`stat -fc %T /sys/fs/cgroup` returns `cgroup2fs`). cgroup v1 is deprecated in Engine 29; if still on v1, plan migration before May 2029 to avoid losing access to `memory.high`, `memory.pressure`, and the cgroup-aware OOM killer. [src10]

### If container OOMs at exactly the `--memory` value with no growth
--> Swap is disabled or `--memory-swap == --memory`. The container has no swap headroom. Either right-size the limit upward by 20-30% or set `--memory-swap` higher than `--memory` to allow paging (note: swap is slow -- prefer right-sizing). [src1]

### If `docker stats` shows gradual memory growth over hours/days
--> Application memory leak. Profile with language-specific tools (`pprof` for Go, heapdump for Node, `tracemalloc` for Python, JFR/`jcmd` for JVM). Do NOT keep raising `--memory` -- the leak will eventually exhaust any limit. [src4, src8]

### If multiple containers are OOM-killed simultaneously
--> Host itself ran out of memory, not individual containers. Check `dmesg | grep -i oom` and `free -m`. Fix by setting `--memory` limits on every container (so the OOM killer targets the offender, not random victims) and adding host RAM. [src4, src6]

### If running on Docker Desktop (Mac/Windows) and `docker run --memory` is ignored
--> The Docker VM has a fixed allocation (default ~50% of host RAM). Total container memory cannot exceed the VM allocation regardless of per-container `--memory` flags. Increase the allocation in Docker Desktop Settings > Resources > Memory. [src1]

## Common Pitfalls

- **Exit 137 != always OOM**: Exit code 137 means SIGKILL, which can come from `docker kill`, `docker stop` timeout, or the OOM killer. Always check `OOMKilled` flag with `docker inspect`. [src3, src4]
- **`--memory` without `--memory-swap`**: On Docker Engine (Linux), swap defaults to 2x the memory limit. On Docker Desktop, swap may be disabled. Set `--memory-swap` explicitly to control behavior. [src1]
- **Java container-awareness**: JVM before Java 10 didn't respect cgroup memory limits and would try to use host memory. Use Java 10+ with `UseContainerSupport` (on by default) or set `-Xmx` explicitly. [src1, src4]
- **`docker stats` shows container memory, not cgroup limit**: The percentage shown is relative to the container's limit. If no limit is set, it's relative to host memory. [src2]
- **tmpfs counts against memory limit**: Docker's `--tmpfs` mounts and shared memory (`/dev/shm`) count against the container's memory limit by default. Large temporary files can trigger OOM. [src1]
- **Build-time OOM**: `docker build` runs in a container too. `npm install`, `pip install`, and compilation can spike memory. Increase Docker Desktop's memory allocation or use `--jobs 1` to reduce parallelism. [src1]
- **`--oom-kill-disable` and `--kernel-memory` discarded on cgroups v2**: Docker Engine 27+ silently ignores these flags on cgroups v2 hosts. Do not rely on them for modern deployments. [src7]
- **cgroups v2 memory pressure**: cgroups v2 can throttle memory-intensive workloads via `memory.high` before triggering OOM at `memory.max`. This causes performance degradation (slowness) without an OOM kill -- check `memory.pressure` for stall indicators. [src5, src6]

## Diagnostic Commands

> Full script: [diagnostic-commands.sh](scripts/diagnostic-commands.sh) (25 lines)

```bash
# === Confirm OOM ===
docker inspect <cid> --format='OOMKilled: {{.State.OOMKilled}}, ExitCode: {{.State.ExitCode}}'
dmesg | grep -i "oom\|killed process" | tail -10
# === Memory Usage (Live) ===
docker stats                                        # All containers, live
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}"
# === Memory Limits ===
docker inspect <cid> --format='Memory: {{.HostConfig.Memory}}, Swap: {{.HostConfig.MemorySwap}}'
# === Inside Container ===
docker exec <cid> cat /proc/meminfo | head -5
docker exec <cid> cat /proc/1/status | grep VmRSS
docker exec <cid> ps aux --sort=-%mem | head -10
# === cgroups v2 ===
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.events
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.pressure
# === cgroups v1 (Legacy) ===
cat /sys/fs/cgroup/memory/docker/<id>/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/docker/<id>/memory.limit_in_bytes
# === Host Memory ===
free -m
vmstat 1 5
# === cgroups version check ===
stat -fc %T /sys/fs/cgroup   # cgroup2fs = v2, tmpfs = v1
```

## Version History & Compatibility

| Version | Behavior | Key Changes |
|---|---|---|
| Docker 29 (2026) | Current | cgroup v1 deprecated (support until May 2029); default `nofile` ulimit dropped from 1,048,576 to 1,024 (containerd v2.1.5); `SwapBytes` + `MemorySwappiness` added to Swarm service resources [src10] |
| Docker 28 (2025) | Stable | `OOMScoreAdj` added to `docker service create` and `docker stack` (28.2.0); `writable-cgroups=true` SecurityOpt for cgroup writes; deprecated `KernelMemoryTCP` accounting [src9] |
| Docker 27+ (2024) | Stable | OOMScoreAdj for services; `--oom-kill-disable` and `--kernel-memory` discarded on cgroups v2; containerd default runtime [src7] |
| Docker 25-26 | Stable | Improved memory metrics; containerd integration; Docker Desktop 4.x memory leak fixes [src1] |
| Docker 24 | Stable | cgroups v2 fully supported; BuildKit default [src1, src6] |
| Docker 23 | Stable | Compose V2 default; better resource reporting [src1] |
| Docker 20.10 | LTS-like | cgroups v2 support; `--memory` works with both v1/v2 [src1, src6] |
| Docker 19.03 | Legacy | `--memory-swap` behavior standardized [src1] |
| cgroups v2 | Modern Linux | `memory.max` replaces `memory.limit_in_bytes`; `memory.events` for OOM count; `memory.pressure` for stall tracking; `memory.high` for soft throttling [src6] |
| cgroups v1 | Legacy Linux | `memory.limit_in_bytes` + `memory.oom_control`; `--oom-kill-disable` works [src6] |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Container exits with code 137 and OOMKilled=true | Exit 137 but OOMKilled=false | Check for `docker kill` or stop timeout |
| Memory usage grows over time in `docker stats` | Memory is constant but app is slow | CPU profiling, not memory |
| Java/Node/Python process needs right-sizing | Build step fails with OOM | Increase Docker Desktop memory or reduce build parallelism |
| Multiple containers compete for host memory | Single container with plenty of headroom | Look for other issues (CPU, IO, network) |
| Container keeps restarting with exit 137 | Container runs but performance degrades | Check cgroups v2 `memory.pressure` for throttling [src5] |

## Important Caveats

- On Docker Desktop (Mac/Windows), the Docker VM has a fixed memory allocation (default ~50% of host RAM since Docker Desktop 4.x). Even with `--memory` limits, the total across all containers cannot exceed the VM's allocation. Adjust in Docker Desktop Settings > Resources. Docker Desktop 4.38 introduced a known regression causing higher baseline memory consumption.
- The `OOMKilled` flag in `docker inspect` only indicates that the OOM killer killed the container's main process. If a child process was OOM-killed but PID 1 survived, `OOMKilled` may be `false` even though an OOM event occurred. Check `dmesg` for child process kills.
- `docker stats` memory usage includes page cache. Just because a container shows 400MB/512MB doesn't mean it's about to OOM -- some of that is reclaimable cache. The kernel evicts cache before triggering OOM.
- Kubernetes uses the same cgroup mechanism but with different configuration (`resources.limits.memory` in pod spec). On K8s 1.28+ with cgroups v2, the cgroup-aware OOM killer terminates ALL processes in the cgroup, not just the offending one. A `singleProcessOOMKill` kubelet flag is being developed to restore the old behavior.
- In swap-enabled environments, a container may appear to use more than `--memory` without being OOM-killed (because it swaps). This causes performance degradation (swap is slow) without an OOM kill. Monitor swap usage separately.
- Docker Engine 27.0.3 (2024) had a documented memory leak bug that could trigger host-level OOM killing of multiple containers simultaneously -- upgrade to 27.1+ if affected. [src7]
- **Docker Engine 29 (2026) deprecates cgroup v1.** Support continues through at least May 2029, but on cgroup v1 hosts you cannot rely on newer memory features (`memory.high` soft throttling, `memory.pressure` stall tracking, the cgroup-aware OOM killer). Plan host migration to cgroup v2 before upgrading workloads. [src10]
- **Docker Engine 29 changes the default `nofile` ulimit from 1,048,576 to 1,024** (containerd v2.1.5). This is unrelated to OOMKilled in most cases, but processes that pre-allocate per-FD memory (some HTTP servers, database connection pools) can now show very different memory profiles after upgrading. Override with `--ulimit nofile=1048576` or set `default-ulimits` in `/etc/docker/daemon.json` if you depend on the old default. [src10]
- **Docker Engine 28 added `OOMScoreAdj`** for Swarm services (28.2.0, May 2025) -- prefer adjusting OOM score over disabling the OOM killer to bias which container the kernel kills under host-wide pressure. [src9]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Docker Container Won't Start](/software/debugging/docker-container-wont-start/2026)
- [Kubernetes CrashLoopBackOff](/software/debugging/kubernetes-crashloopbackoff/2026)
- [Python Memory Leaks](/software/debugging/python-memory-leaks/2026)
- [Node.js Memory Leaks](/software/debugging/nodejs-memory-leaks/2026)
