How to Fix Docker Exit Code 137 OOMKilled: Diagnosing and Resolving Container Memory Kills on Docker Desktop
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–30 mins
TL;DR
- What broke: Your container's process (JVM, Node.js, Python, etc.) exceeded the 2GB Docker Desktop VM memory ceiling. The Linux OOM killer sent
SIGKILL(signal 9). No graceful shutdown. No heap dump. Just dead. - How to fix it: Set explicit
mem_limit+mem_reservationin Compose, tune runtime heap flags (-Xmx,--max-old-space-size), and verify Docker Desktop's VM memory allocation is sufficient for your workload. - Fast path: Use our Client-Side Sandbox below to auto-refactor your
docker-compose.ymlordocker runcommand — your config never leaves the browser.
The Incident (What Does Exit Code 137 Mean?)
Raw terminal output you're seeing:
$ docker run --memory=2g myapp:latest
...
Killed
$ echo $?
137
Or in docker ps -a:
CONTAINER ID IMAGE COMMAND STATUS
a3f9c1d2e8b7 myapp:latest "java -jar" Exited (137) 4 minutes ago
Or in Docker Desktop dashboard: OOMKilled: true.
Exit code 137 = 128 + 9. Signal 9 is SIGKILL. The kernel's OOM killer fired because the cgroup memory limit was breached. The process did not catch this signal — it cannot be caught. No finally block ran. No connection pool was drained. If this was a database container, you likely have a corrupt write-ahead log.
Verify with:
docker inspect <container_id> | grep -A5 '"OOMKilled"'
# Expected output:
# "OOMKilled": true,
The Attack Vector / Blast Radius
This is not just a restart. The cascading failure profile:
- Data integrity risk. If the killed container was writing to a volume (Postgres, Redis AOF, Elasticsearch index), the write is incomplete. On restart, recovery may fail or silently corrupt data.
- Dependency chain collapse. In a multi-container Compose stack, a killed app container leaves upstream nginx/HAProxy with broken upstreams. Health checks may not detect this fast enough, causing a thundering herd on restart.
- Silent memory leak masking. Docker Desktop on macOS/Windows runs containers inside a Linux VM (LinuxKit). The VM itself has a memory cap set in Docker Desktop settings. If your container's
--memoryflag is unset, the OOM killer fires at the VM level, killing whichever container is the highest-memory offender — not necessarily the one you expect. This is the most common cause of mysterious 137 exits in local dev. - JVM-specific trap. The JVM does not respect cgroup v2 memory limits by default on older versions (pre-JDK 10). It reads
/proc/meminfo(host total RAM), allocates heap accordingly, and blows straight through the container limit. - Node.js trap. Default V8 heap cap is ~1.5GB on 64-bit. If you're running multiple worker threads or have a memory leak, you hit the container limit before V8's own GC has a chance to collect.
How to Fix It
Step 1: Confirm Docker Desktop VM Memory Allocation
Docker Desktop → Settings → Resources → Memory. This is the hard ceiling for all containers combined. If this is set to 2GB and your container's --memory is also 2GB, you have zero headroom for the Docker daemon, other containers, or the VM OS itself.
Rule of thumb: Set the VM to at least 1.5× the sum of all container memory limits.
Step 2: Basic Fix — Set Explicit Limits in docker-compose.yml
version: '3.8'
services:
app:
image: myapp:latest
- # No memory constraints set
+ mem_limit: 1g
+ mem_reservation: 512m
+ oom_kill_disable: false # Keep true OOM visibility; never set to true in prod
environment:
- - JAVA_OPTS=-Xms512m
+ - JAVA_OPTS=-Xms256m -Xmx768m -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
mem_reservation is the soft limit (scheduling hint). mem_limit is the hard cgroup ceiling. Always set both.
Step 3: Runtime-Specific Heap Tuning
Java (JDK 11+):
- JAVA_OPTS=-Xmx2g
+ JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
UseContainerSupport (default on JDK 10+) reads cgroup limits instead of host RAM. MaxRAMPercentage=75.0 leaves 25% headroom for off-heap (Metaspace, direct buffers, GC overhead).
Node.js:
- CMD ["node", "server.js"]
+ CMD ["node", "--max-old-space-size=768", "server.js"]
With a 1GB container limit, cap V8 heap at ~75% = 768MB.
Python:
Python has no built-in heap cap. Use memory-profiler or tracemalloc to identify the leak. For worker pools (Gunicorn), set --max-requests to recycle workers and prevent unbounded growth:
- CMD ["gunicorn", "app:app", "-w", "4"]
+ CMD ["gunicorn", "app:app", "-w", "4", "--max-requests", "1000", "--max-requests-jitter", "100"]
Enterprise Best Practice — Resource Limits as Code
In production Kubernetes (the real fix for Docker Desktop issues that reach prod):
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: myapp:latest
resources:
- # No requests or limits defined
+ requests:
+ memory: "512Mi"
+ cpu: "250m"
+ limits:
+ memory: "1Gi"
+ cpu: "1000m"
Requests = guaranteed scheduling. Limits = hard cgroup ceiling. Without requests, the scheduler places your pod on an overloaded node. Without limits, one pod can OOM the entire node.
Also configure a LimitRange at the namespace level so no one can deploy without resource constraints:
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
spec:
limits:
- default:
memory: 512Mi
cpu: 500m
defaultRequest:
memory: 256Mi
cpu: 100m
type: Container
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — Static Analysis on Compose/Helm
pip install checkov
checkov -f docker-compose.yml --check CKV_DOCKER_7,CKV_DOCKER_13
# CKV_DOCKER_7: Ensure memory is limited
# CKV_DOCKER_13: Ensure CPU is limited
Add to your GitHub Actions pipeline:
- name: Lint Docker Compose resource limits
run: checkov -f docker-compose.yml --soft-fail
2. OPA/Conftest Policy — Block Deployments Without Limits
# policy/deny_no_memory_limit.rego
package main
deny[msg] {
container := input.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%v' has no memory limit. OOMKill risk.", [container.name])
}
conftest test k8s-deployment.yaml --policy policy/
3. Continuous Memory Monitoring in Local Dev
# Watch live container memory usage — run this while load testing
watch -n 1 'docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}"'
Set alert threshold at 80% of mem_limit. If you're consistently hitting 70%+, the limit is undersized — not a leak.
4. Heap Dump on OOM (Java) — Don't Fly Blind
- JAVA_OPTS=-Xmx768m
+ JAVA_OPTS=-Xmx768m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps/oom.hprof
Mount /dumps as a volume so the heap dump survives container death. Analyze with Eclipse MAT or jhat.