RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Docker Desktop OOMKilled Exit 137 Tracker
02. Root Cause Vector

Container killed with exit code 137 because the Docker Desktop 2GB memory limit was breached, triggering the Linux OOM killer and causing...

03. Rapid Prescription

Increase container memory limits or optimize runtime heap/cgroup footprint.

Zero-Trust Client IDE/ Performance

Docker Desktop OOMKilled Exit 137 Tracker

Container killed with exit code 137 because the Docker Desktop 2GB memory limit was breached, triggering the Linux OOM killer and causing immediate process termination.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(41L)
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
# WARNING: Legacy PCI-compliant zone config - DO NOT MODIFY without change control approval from 'my-registry.internal/legacy-api' team
# TODO: This Dockerfile is a temporary workaround for the old monolithic app. Refactor to microservices pending budget Q4.
# FIXME: Memory settings are inconsistent with the new orchestration layer. Keep as-is for now.

FROM my-registry.internal/legacy-api:enterprise-java8-patched-2020
# Old base image with known memory leaks in the JVM garbage collector

# VULNERABILITY: The JVM heap is set to 1.8G, but the container memory limit is 2G. Under load, the JVM's overhead (metaspace, threads, native memory) pushes total usage past the limit, triggering OOMKill with exit 137.
ENV JAVA_OPTS="-Xmx1800m -Xms1800m -XX:MaxMetaspaceSize=256m -XX:+UseG1GC"
# TODO: Adjust heap for container limits; this was copied from a VM setup

# Redundant environment variables from the old deployment pipeline
ENV APP_ENV="pci-secure-zone"
ENV LEGACY_DB_URI="postgresql://prod-db.internal:5432/transactions?sslmode=require&user=svc_legacy&password=SuperSecretDBPass123!@#"
ENV AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
ENV AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# FIXME: Rotate these keys - they're from the old 'legacy-backup' IAM role

# Mixed syntax from a Helm chart migration - commented-out old configs
# memory: 2Gi
# cpu: 500m
# securityContext:
#   runAsUser: 1000

# Embedded dummy RSA private key for SSH tunnel (used by the legacy health check script)
RUN echo '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtX8C6UpL8QzQ2mEcB8NqGp/7pJkF3aXZKU1WxV... (truncated for brevity) \n-----END RSA PRIVATE KEY-----' > /etc/ssh/legacy_key.pem && chmod 600 /etc/ssh/legacy_key.pem
# TODO: Replace with a secrets manager; this is a stopgap for the PCI audit

# Stripe API token hardcoded from the old payment module
ENV STRIPE_SECRET_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"

# Legacy entrypoint with a race condition on startup that sometimes spikes memory
COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# TODO: This script has a known issue where it forks multiple JVMs on slow DNS resolution

# Redundant health check that adds overhead
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/legacy-health || exit 1
# FIXME: This endpoint is deprecated and leaks memory in the response handler
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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 above to auto-refactor your docker-compose.yml or docker run command — your config never leaves the browser.

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

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,

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

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 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. 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.

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.What is the difference between exit code 137 and exit code 1 in Docker?
Exit code 137 is a SIGKILL from the kernel's OOM killer—a hard kill with zero cleanup, indicating your container breached its memory limit. Exit code 1 is a graceful, application-controlled shutdown. The root cause is a misaligned memory policy: your container's limit is a hard ceiling, and the application's heap or process allocation didn't respect it. To diagnose, run `docker inspect` for OOMKilled, but that's just the symptom. The real fix requires recalculating memory boundaries and injecting a zero-trust policy that enforces them before the kernel intervenes.
Q2.Why does my container get OOMKilled even though Docker Desktop has 8GB RAM on my machine?
Because you're fighting a nested virtualization trap: Docker Desktop allocates a Linux VM (e.g., 2GB), and your containers share that VM's ceiling, not your host's 8GB. Manually adjusting the VM memory in Settings is a brittle, reactive fix that risks overallocation or starving other containers. Every manual edit expands the blast radius—misconfigured limits can cascade into cluster-wide OOM kills. Instead of guessing, drop your broken docker-compose.yml or Dockerfile into the StackEngine Local Sandbox above. It will automatically compile a zero-trust fix that hardens memory limits and injects guardrails to prevent boundary breaches.
Q3.Should I set oom_kill_disable: true in docker-compose.yml to prevent exit 137?
Absolutely not. Disabling the OOM killer is a catastrophic anti-pattern: it shifts the kill signal to other processes in the VM or host, including the Docker daemon, risking a total system outage. This isn't a fix—it's a compliance violation and a security hole. Manually right-sizing limits across a mono-repo is a compliance nightmare, prone to drift and audit failures. For scalable, on-prem-safe remediation, upgrade to the StackEngine CI/CD Automator (Team Edition). It automates policy enforcement across your CI/CD pipeline, ensuring every manifest is pre-hardened against OOM kills without touching runtime settings.