Docker: Container Exited with Code 137 (OOMKilled)

Quick answer

Exit code 137 = 128 + 9 = the process received SIGKILL. It's usually the kernel's OOM killer, but not always — confirm it first with docker inspect <container> --format='{{.State.OOMKilled}}'. If true: raise the memory limit if the workload genuinely needs it, fix runtime heap sizing (Java/Node), or track down a real memory leak with docker stats over time before just bumping the number.

The exact signal

$ docker ps -a
CONTAINER ID   IMAGE       STATUS
a1b2c3d4e5f6   my-api      Exited (137) 3 minutes ago

$ docker inspect a1b2c3d4e5f6 --format='{{.State.OOMKilled}}'
true

137 is Docker's convention of 128 + signal number, and signal 9 is SIGKILL — an unconditional, un-catchable kill with no chance for the process to clean up. The OOM killer always uses SIGKILL, which is why 137 is strongly associated with memory exhaustion. But it is not the only thing that sends SIGKILL, which is exactly why the confirmation step above matters before you spend time chasing a memory leak that doesn't exist.

Troubleshooting at a glance

The single OOMKilled flag splits this error into two completely different worlds. Start here:

Exit code 137 OOMKilled == true? Yes Memory (OOM killer) Container cgroup limit (-m) Whole-host out of RAM + swap Memory leak (climbs over time) No SIGKILL — other cause docker stop timed out → kill Failed healthcheck Orchestrator / manual kill

Step 1: confirm it was actually the OOM killer

Don't assume — check. This single flag tells you definitively whether memory was the cause:

docker inspect <container> --format='{{.State.OOMKilled}}'
# true  -> the kernel's OOM killer terminated this container specifically
# false -> SIGKILL came from somewhere else (see "other causes" below)

You can also confirm it at the kernel level. On the Docker host (not inside the container), the OOM killer logs every kill it performs:

dmesg | grep -i "killed process"
# or on systemd hosts:
journalctl -k | grep -i "out of memory"

cgroup v1 vs v2: most current Linux distributions (and Docker Desktop) now default to cgroup v2 rather than the older v1. This doesn't change anything you do on this page — Docker reports OOMKilled: true and the kernel logs the kill identically regardless of the cgroup version. If docker inspect shows the flag, you can trust it either way.

Step 2: figure out which scope ran out — container or host

The same OOM killer operates at two different scopes, and the fix is different for each:

ScopeWhat happenedWhere to look
Container cgroup limitYour container hit its own -m/mem_limit ceiling; only that container is killeddocker inspect --format='{{.HostConfig.Memory}}'
Whole hostNo limit was set (or it was fine), but physical memory + swap on the machine itself ran out; the kernel picks a victim anywhere on the boxfree -h, docker stats across all containers, not just this one

On macOS and Windows the "host" is the Docker Desktop VM, which has its own separate ceiling — see the next section.

Docker Desktop (macOS/Windows): the VM has its own memory ceiling

On macOS and Windows there is no Linux "host" in the usual sense — the daemon and every container run inside a single Docker Desktop VM, and that VM is allocated a fixed slice of your machine's memory. Even a container with no -m limit of its own can't exceed what the VM has, so a memory-hungry container gets OOMKilled the moment the whole VM fills up. Raise the VM's allocation here:

Docker Desktop → Settings → Resources → Advanced → Memory limit

This is one of the most common reasons a container that runs fine in CI or on a Linux server gets OOMKilled only on a developer's Mac or Windows laptop — the default Docker Desktop memory allocation is simply smaller than what the workload needs.

Fix 1: the workload genuinely needs more memory — raise the limit

If usage sits at a stable, predictable working set that's simply higher than your original budget, raising the limit is the correct, complete fix:

# docker run
docker run -m 1g my-api

# docker-compose.yml
services:
  api:
    mem_limit: 1g

# Kubernetes
resources:
  limits:
    memory: "1Gi"

Before raising it, watch the trend with docker stats for a while — a stable plateau near the limit supports "just needs more"; a line that keeps climbing without leveling off supports "leak" (Fix 3), and a higher limit there just delays the identical crash.

Fix 2: runtime heap sizing that ignores the container limit

Several language runtimes historically sized their default heap from the host's total memory, not the container's cgroup limit — a container capped at 512MB but running on a 64GB host would happily try to grow a multi-gigabyte heap and get OOMKilled almost immediately.

# Java (JDK 10+ has UseContainerSupport on by default, but pin it explicitly):
java -XX:MaxRAMPercentage=75.0 -jar app.jar
# leaves ~25% headroom for thread stacks, metaspace, and off-heap buffers

# Node.js — the V8 heap default also isn't container-aware:
node --max-old-space-size=384 server.js   # for a 512MB container limit

# Python — no heap flag as such, but cap worker count so total RSS fits:
gunicorn --workers=2 --max-requests=1000 --max-requests-jitter=100 app:app

The Node and Java flags above are deliberately set below the container's memory limit, not equal to it — thread stacks, native buffers, and the runtime's own bookkeeping all need headroom outside whatever number you hand to -Xmx or --max-old-space-size.

Fix 3: it's a genuine memory leak

If memory climbs steadily over hours or days instead of leveling off at a working set, no limit is generous enough — you're watching a leak, and it will eventually hit any ceiling you set. Confirm the trend and then look at what's actually accumulating:

# watch live, container-scoped memory over time
docker stats <container>

# Node: a leak often shows as a growing heap even after forced GC —
# take two heap snapshots an hour apart and diff them in Chrome DevTools

# Python: common culprits are a growing in-memory cache/list with no eviction,
# or objects held by closures/listeners that are never unregistered

A pragmatic stopgap while you track down the real leak: recycle worker processes periodically (gunicorn --max-requests, PM2's max_memory_restart) so accumulated memory gets released on a schedule instead of crashing unpredictably in production.

How this looks in Kubernetes

Same mechanism, different vocabulary. A pod hitting its memory limit shows up like this:

$ kubectl describe pod my-api-7d4f9c
Last State:   Terminated
  Reason:     OOMKilled
  Exit Code:  137

Kubernetes distinguishes this from eviction: when the whole node is under memory pressure, the kubelet can proactively remove entire pods (lowest-priority/most-over-limit first) before the kernel OOM killer would even trigger. That shows Reason: Evicted, not OOMKilled — a node-level capacity problem rather than one pod exceeding its own limit, and it needs more cluster capacity or better resource requests/limits across pods, not just a bigger number on this one deployment.

When 137 is NOT about memory

Because 137 only encodes "received SIGKILL," a few other things produce it identically — and OOMKilled: false from Step 1 tells you decisively that you're in one of these cases instead:

Debugging checklist

Frequently Asked Questions

What does exit code 137 actually mean?

137 is Docker/Linux's convention of 128 + signal number, and 9 is SIGKILL — so 137 means the container's main process was forcibly killed and had no chance to shut down cleanly. The OOM killer sends SIGKILL, which is why 137 is strongly associated with 'out of memory', but a manual docker kill, a failed healthcheck killing the container, or an orchestrator forcefully stopping it also produce the exact same code.

How do I confirm it was actually the OOM killer?

Run docker inspect <container> --format='{{.State.OOMKilled}}'. If it prints true, the kernel's OOM killer specifically terminated this container for using too much memory. If it prints false, the process still received SIGKILL for another reason — a manual kill, a healthcheck failure, or the orchestrator stopping it — and you're debugging the wrong thing if you assume memory.

Is this the container's memory limit, or the whole host running out?

Both trigger the same OOM killer, at different scopes. If you set -m/mem_limit on the container, exceeding that cgroup limit gets only that container killed. If no limit is set (or the limit is fine) but the host itself runs out of physical memory and swap, the kernel's global OOM killer picks a victim process anywhere on the machine — which might be this container, a different one, or even something unrelated.

Why does my Java app get OOMKilled even though -Xmx is set below the container limit?

The JVM heap is only part of a Java process's total memory — thread stacks, metaspace, the JIT's code cache, and native/off-heap buffers all sit outside -Xmx and count against the container's cgroup limit too. Setting -Xmx close to the container limit leaves no room for that overhead. On modern JDKs (10+, with UseContainerSupport on by default), prefer -XX:MaxRAMPercentage (e.g. 75%) so the JVM sizes itself with headroom automatically.

How does this look different in Kubernetes?

Same underlying mechanism, different vocabulary. kubectl describe pod shows Last State: Terminated, Reason: OOMKilled, Exit Code: 137 when a container exceeds its configured resources.limits.memory. Kubernetes also distinguishes this from an eviction, where the kubelet proactively removes a whole pod under node memory pressure before the kernel OOM killer would even trigger — that shows a different reason ('Evicted'), not OOMKilled.

Should I just raise the memory limit and move on?

Sometimes — if the workload genuinely needs more memory than you originally budgeted, raising the limit is the correct fix. But if usage climbs steadily over hours or days rather than sitting at a stable working set, that's a memory leak, and a higher limit only delays the same crash. Track memory over time with docker stats or a metrics tool before deciding which situation you're in.

Does exit code 137 always mean memory?

No. 137 is just 'this process received SIGKILL' — it says nothing about why. A docker stop that times out and escalates to SIGKILL, a failed Docker/Kubernetes healthcheck killing an unresponsive container, or an operator running docker kill manually all produce the identical exit code 137 with OOMKilled reported as false. Always check the OOMKilled flag before assuming memory.

More backend & build errors

Browse the full reference for Node.js, Python, Docker, and database errors — exact message, cause, and fix.

All Error References Docker: Cannot connect to the daemon Node: JavaScript heap out of memory
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.