# Fix Docker Exit Code 137 (Container Killed / OOM)

URL: https://www.devopsness.com/blog/fix-docker-oom-137
Author: Kiril Urbonas
Published: 2026-07-10
Updated: 2026-07-10
Categories: DevOps
Tags: Docker, Containers, DevOps, Performance, Debugging

Summary: Exit 137 usually means the kernel shot your container for eating too much memory. Here's how to confirm the OOM kill and stop it happening again.

---
# Fix Docker Exit Code 137 (Container Killed / OOM)

A container that keeps dying with `Exited (137)` is one of those failures that looks scary and is actually pretty legible once you know how to read it. The first time it bit me I burned an hour reading application logs that had nothing to do with the problem, because the app never got a chance to log anything. It didn't crash. Something outside it pulled the trigger.

## What 137 actually means

Exit codes above 128 encode a signal. The math is `128 + signal_number`. Signal 9 is `SIGKILL`, so `128 + 9 = 137`. `SIGKILL` is the one signal a process cannot catch, block, or handle. When your container gets it, the process is gone instantly with no cleanup, no shutdown hook, no final log line.

The usual sender of that `SIGKILL` is the Linux OOM (out-of-memory) killer. When memory runs out, the kernel picks a process and kills it to reclaim RAM. In a Docker world "memory runs out" happens at two very different boundaries, and telling them apart is the whole job.

## Confirm it's actually an OOM kill

Don't guess. Ask Docker directly:

```bash
$ docker inspect --format '{{.State.OOMKilled}}' myapp
true
```

If that prints `true`, the kernel OOM-killed your container. Full stop. You can see the same thing in the raw state:

```bash
$ docker inspect --format '{{json .State}}' myapp | jq
{
  "Status": "exited",
  "OOMKilled": true,
  "ExitCode": 137,
  "Error": ""
}
```

Then confirm on the host kernel side with `dmesg`:

```bash
$ dmesg -T | grep -i -E 'oom|killed process'
[Tue Jul  7 14:22:10 2026] Memory cgroup out of memory: Killed process 48213 (node) total-vm:2891044kB, anon-rss:1048180kB
[Tue Jul  7 14:22:10 2026] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=...
```

That `constraint=CONSTRAINT_MEMCG` is the tell. `MEMCG` means the kill came from a memory cgroup hitting its limit, which is the container's own `--memory` cap. If instead you see `constraint=CONSTRAINT_NONE`, the whole host ran out of memory and the kernel went hunting for the biggest target. Same exit code, completely different fix.

## Two kinds of OOM, two fixes

**The container hit its own limit.** You told Docker `--memory=512m` and the process wanted more. The cgroup limit is doing exactly its job. Check what it's really using:

```bash
$ docker stats --no-stream
CONTAINER   NAME    CPU %   MEM USAGE / LIMIT     MEM %
a1b2c3d4    myapp   0.42%   509.8MiB / 512MiB     99.57%
```

Sitting at 99% against the limit right before it died is your smoking gun. Either the limit is too low for honest workload, or the app is leaking.

**The host ran out.** No per-container limit, or limits that sum to more than physical RAM, and everything competes. Here the container that gets killed often isn't the one at fault, it's just the fattest. Check the host:

```bash
$ free -m
               total        used        free      shared
Mem:           7820        7511         120         210
```

120MB free on an 8GB box means you're overcommitted and living on borrowed time.

## Right-size the limit from real numbers

The lazy fix is to bump `--memory` until the kills stop. Don't. Measure first, then set a limit with headroom over the real steady-state peak:

```bash
docker run -d --name myapp \
  --memory=1g \
  --memory-swap=1g \
  myapp:latest
```

Setting `--memory-swap` equal to `--memory` disables swap for the container, which is what you usually want in production. If you leave `--memory-swap` unset it defaults to double `--memory`, quietly letting the container swap and hiding the real footprint until swap thrashing tanks your latency instead. Be deliberate about it.

## Make the runtime aware of the cgroup

This is the trap that catches people who "already set a limit." Older runtimes look at the host's total RAM, not the container's cgroup limit, and size their heap for a machine that doesn't exist. A JVM on an 8GB host inside a 1GB container will happily plan a 2GB heap and get shot the moment it grows into it.

Tell the runtime the truth:

```bash
# JVM: size the heap as a fraction of the CGROUP limit, not the host
java -XX:MaxRAMPercentage=75.0 -jar app.jar

# Node: cap the old-space heap under your container limit
node --max-old-space-size=768 server.js
```

For a 1GB container, `--max-old-space-size=768` leaves room for the non-heap overhead (buffers, native modules, the V8 baseline) that lives outside the old space and still counts against your cgroup. Set it to the full 1024 and you've reserved zero headroom for everything else.

## When it's a real leak

If usage climbs steadily and never plateaus, no limit will save you, it just changes how long until the kill. Watch the slope over time:

```bash
$ docker stats myapp
CONTAINER   MEM USAGE / LIMIT
myapp       412MiB / 1GiB      # 10:00
myapp       587MiB / 1GiB      # 10:30
myapp       761MiB / 1GiB      # 11:00
```

A line that only goes up is a leak. Fix the code. The limit is a seatbelt, not a cure.

## Don't overcommit the host

If you run several containers on one box, their limits should sum to comfortably less than physical RAM, with slack for the kernel, page cache, and the Docker daemon itself. Overcommitting on the promise that "they won't all peak at once" works right up until they do, and then the host-level OOM killer makes the choice for you, badly.

## One more thing: 137 isn't always the kernel

If `OOMKilled` is `false` but the exit code is still 137, something sent `SIGKILL` by hand. A `docker kill myapp` (which sends `SIGKILL` by default), an orchestrator killing a container that ignored `SIGTERM` during shutdown, or a `docker stop` that timed out and escalated. Same number, no OOM involved. That's exactly why you check `.State.OOMKilled` first instead of trusting the code alone.

If you're chasing a broader set of failures, our [Docker troubleshooting](/blog/docker-error-troubleshooting-guide) guide covers the neighboring exit codes. And the container-orchestrator version of this exact problem shows up as [Kubernetes OOMKilled](/blog/fix-oomkilled), where the pod status says it plainly and the cgroup mechanics underneath are identical.

## The call we'd make

Run `docker inspect --format '{{.State.OOMKilled}}'` before you touch anything else. If it's `true`, check `dmesg` for `CONSTRAINT_MEMCG` versus `CONSTRAINT_NONE` to know whether it was the container's limit or the whole host. Set an explicit `--memory` with `--memory-swap` equal to it, size it from `docker stats` peaks plus headroom, and make the runtime cgroup-aware with `MaxRAMPercentage` or `--max-old-space-size`. If memory only ever climbs, stop tuning limits and go find the leak.

