Initializing Enclave...

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_reservation in 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.yml or docker run command — 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:

  1. 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.
  2. 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.
  3. 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 --memory flag 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.
  4. 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.
  5. 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.

Related Diagnostics

"Part of the Performance Utility Matrix."

View all 219 Performance Tools →