Fix Docker Exit Code 137 (Container Killed / OOM)
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.
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:
$ 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:
$ docker inspect --format '{{json .State}}' myapp | jq
{
"Status": "exited",
"OOMKilled": true,
"ExitCode": 137,
"Error": ""
}
Then confirm on the host kernel side with dmesg:
$ 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:
$ 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:
$ 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:
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:
# 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:
$ 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 guide covers the neighboring exit codes. And the container-orchestrator version of this exact problem shows up as Kubernetes 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.
Azure Workload Identity Federation Without Secrets
We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer. Here's the exact setup and the claims that trip people up.
Terraform vs Pulumi — HCL vs Real Code
Terraform's declarative HCL and Pulumi's real programming languages solve the same problem in very different ways, and the right pick depends on your team.
More from DevOps
Explore more articles in this category
GitHub Copilot CLI Sandboxing Is GA: What Actually Changed
GitHub made local sandboxing generally available for Copilot CLI on October 7, so the default now restricts the agent instead of trusting it.
The Tensorlake npm Compromise Targeted Your AI Tool Configs
A compromised npm release went after cloud credentials as expected, and local AI coding-assistant config files, which most teams never treated as secrets.
GitLab's New Rate Limits: What to Fix Before Oct 19
GitLab is capping unauthenticated API calls at 60 an hour starting October 19, and the preview windows land before most teams will have noticed.
You might have missed
Evergreen posts worth revisiting.