Skip to content

Fix: Docker Container Exited (137) OOMKilled / Killed Signal 9

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix Docker container 'Exited (137)', OOMKilled, and 'Killed' signal 9 errors caused by out-of-memory conditions in Docker, Docker Compose, and Kubernetes.

The Error

Your Docker container stops unexpectedly. You check the status and see:

$ docker ps -a
CONTAINER ID   IMAGE     STATUS                      NAMES
a1b2c3d4e5f6   myapp     Exited (137) 2 minutes ago  myapp

Or you check the logs and find:

Killed

You inspect the container and see:

"State": {
    "OOMKilled": true,
    "ExitCode": 137
}

In Kubernetes, the pod status shows:

NAME    READY   STATUS      RESTARTS   AGE
myapp   0/1     OOMKilled   3          5m

All of these mean the same thing: your container was killed because it ran out of memory.

Why This Happens

Exit code 137 means the process received SIGKILL (signal 9). The formula is 128 + 9 = 137. When a container exceeds its memory limit, the Linux kernel’s OOM (Out of Memory) killer terminates it with SIGKILL. There’s no graceful shutdown — the process is killed immediately.

This happens for one of these reasons:

  • The container has a memory limit and exceeded it. Docker’s --memory flag or Kubernetes resources.limits.memory sets a hard cap. Once the container crosses that line, it’s killed instantly.
  • The Docker host itself is running out of memory. Even without container-level limits, the host OS has finite RAM. The kernel’s OOM killer picks the biggest memory consumer and kills it — often your container.
  • Docker Desktop has a memory ceiling. On macOS and Windows, Docker Desktop runs inside a VM with a fixed amount of RAM (default is usually 2 GB). All your containers share that allocation.
  • Your application has a memory leak. The container’s memory usage climbs over time until it hits the limit.

Version history that changes how OOM kills behave

Memory accounting has moved targets several times across Docker, Linux, and the underlying runtime stack.

  • cgroups v1 vs cgroups v2. Docker Engine 20.10 (released December 2020) added cgroups v2 support, and Docker 23+ uses v2 by default on modern distros. Cgroups v2 includes more memory categories in the limit — kernel stack, socket buffers, and page cache for some workloads — which means a container that ran fine under v1 with --memory=512m can OOMKill under v2 at the same limit. Distros that ship cgroups v2 by default: Fedora 31+, Ubuntu 21.10+, Debian 11+, RHEL 9+.
  • memory.swap.max in cgroups v2. Under cgroups v1 you set total memory + swap with --memory-swap. Under cgroups v2 the kernel exposes memory.swap.max as a separate file, and the math changed: --memory-swap=2g --memory=1g means “1 GB RAM + 1 GB swap” on v1, but Docker translates it differently on v2. If swap usage looks wrong after a host upgrade, the cgroup version flipped.
  • MaxRAMPercentage (Java 10+) vs MaxRAMFraction (Java 8u131–10). Container-aware JVMs only respect these when -XX:+UseContainerSupport is on, which is the default in Java 10+ and backported to Java 8u191. Java 8 before u191 ignores container limits entirely and allocates a heap based on host RAM. The same Dockerfile that worked on Java 11 can OOM on Java 8.0.181 with no other changes.
  • Node.js --max-old-space-size and container awareness. Node.js 14+ reads the cgroup limit when sizing the default V8 heap, but only on Linux and only when no explicit --max-old-space-size is passed. Older Node versions (12 and below) default to a heap around 1.5 GB regardless of the container limit, which silently overflows small containers.
  • Kubernetes 1.25 stable cgroups v2. Kubernetes 1.25 (August 2022) marked cgroups v2 support as stable. Clusters upgraded from 1.22-era cgroups v1 nodes to 1.25+ v2 nodes sometimes see new OOMKilled pods because the kubelet now uses v2 metrics that include slightly more memory than before.
  • oom_kill_disable removed in cgroups v2. The old --oom-kill-disable Docker flag relied on a cgroups v1 feature. On v2 hosts the flag is silently ignored. If you used it as a safety net, the safety net is gone.
  • Docker Desktop 4.x VM tuning. Docker Desktop 4.13+ switched the macOS VM backend to Virtualization.framework and added the Use Rosetta toggle for Apple Silicon. Memory accounting inside the new VM differs slightly from the old Hyperkit-based VM, and the default memory allocation on a fresh install is still 2 GB.

Fix 1: Check What’s Using Memory

Before changing any limits, find out how much memory your container actually needs.

Check current memory usage of running containers:

docker stats

This shows a live view of CPU, memory, network, and I/O for every running container. Watch the MEM USAGE / LIMIT column.

Inspect a stopped container to confirm OOM:

docker inspect <container-id> | grep -i oom

If OOMKilled is true, you’ve confirmed the cause.

Check the host’s dmesg logs for OOM events:

dmesg | grep -i "oom\|killed process"

You’ll see entries like:

Out of memory: Killed process 12345 (node) total-vm:1024000kB, anon-rss:512000kB

This tells you exactly which process was killed and how much memory it was using.

Fix 2: Increase the Container Memory Limit

If your container genuinely needs more memory, raise the limit.

Docker run:

docker run --memory=2g --memory-swap=2g myapp
  • --memory=2g sets the hard limit to 2 GB.
  • --memory-swap=2g sets the total (RAM + swap) to the same value, effectively disabling swap. If you want to allow swap, set --memory-swap higher than --memory.

Docker Compose:

services:
  myapp:
    image: myapp
    deploy:
      resources:
        limits:
          memory: 2g
        reservations:
          memory: 512m

For Compose V2 without swarm mode, you can also use the top-level mem_limit (though deploy.resources is the preferred modern syntax):

services:
  myapp:
    image: myapp
    mem_limit: 2g
    memswap_limit: 2g

Verify the limit is applied:

docker stats myapp

The LIMIT column should show your new value.

Fix 3: Increase Docker Desktop Memory

On macOS and Windows, Docker Desktop runs inside a VM with limited resources. The default is often 2 GB, which is not enough for multi-container setups.

Docker Desktop (macOS / Windows):

  1. Open Docker Desktop
  2. Go to Settings > Resources > Advanced
  3. Increase the Memory slider (4 GB or 8 GB is a reasonable starting point)
  4. Click Apply & Restart

WSL2 backend (Windows):

Docker Desktop with WSL2 uses the WSL2 VM’s memory allocation. You can configure this with a .wslconfig file:

# %USERPROFILE%\.wslconfig
[wsl2]
memory=8GB
swap=4GB

After saving, restart WSL2:

wsl --shutdown

Then restart Docker Desktop.

Pro Tip: On Docker Desktop for macOS and Windows, the default VM memory is often just 2 GB shared across all containers. Before debugging individual container limits, check your Docker Desktop memory allocation under Settings > Resources > Advanced.

Fix 4: Reduce Your Application’s Memory Usage

Sometimes the fix isn’t more memory — it’s less waste.

Node.js

Node.js defaults to a heap limit around 1.5 GB (varies by version and system). In a container with a 512 MB limit, this is a problem.

Set the max heap explicitly:

ENV NODE_OPTIONS="--max-old-space-size=384"

Or pass it at runtime:

docker run --memory=512m -e NODE_OPTIONS="--max-old-space-size=384" myapp

Why 384 and not 512? The heap limit should be ~75% of the container memory limit. The remaining memory is needed for the V8 engine overhead, native code, buffers, and the OS.

Detect Node.js memory leaks:

docker run --memory=1g -e NODE_OPTIONS="--max-old-space-size=768 --expose-gc" myapp

Use process.memoryUsage() in your code to log heap usage over time. If heapUsed grows continuously without flattening, you have a leak.

Java

Java is notorious for consuming excess memory in containers. The JVM allocates a heap that can exceed the container’s limit if not configured properly. For more on Java memory issues, see Fix: Java OutOfMemoryError: Java heap space.

Modern JVMs (Java 10+) detect container limits automatically with:

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"

For older JVMs (Java 8u191+):

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMFraction=2"

For Java 8 before u191, container detection doesn’t exist. You must set the heap explicitly:

ENV JAVA_OPTS="-Xmx384m -Xms256m"

Python

Python processes can consume large amounts of memory with data-heavy libraries (Pandas, NumPy, ML frameworks).

Reduce memory usage:

  • Use generators instead of loading entire datasets into memory.
  • Process data in chunks with pandas.read_csv(chunksize=10000).
  • Use del and gc.collect() to free large objects explicitly.
  • Set MALLOC_TRIM_THRESHOLD_ to release memory back to the OS:
ENV MALLOC_TRIM_THRESHOLD_=100000

Fix 5: Use Multi-Stage Builds to Reduce Image Size

A bloated image doesn’t directly cause OOM, but unnecessary build dependencies increase the container’s baseline memory footprint.

Before (single stage):

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]

This image includes npm, the entire Node.js development toolchain, and all devDependencies in node_modules.

After (multi-stage):

FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
CMD ["node", "dist/server.js"]

Even better, use npm ci --omit=dev in a production install step to strip devDependencies:

FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]

For more on Dockerfile issues, see Fix: Docker COPY Failed: File Not Found in Build Context.

Fix 6: Kubernetes Resource Limits

In Kubernetes, OOMKilled happens when a pod exceeds its resources.limits.memory.

Check why the pod was killed:

kubectl describe pod <pod-name>

Look for:

Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

Set appropriate resource requests and limits:

apiVersion: v1
kind: Pod
metadata:
  name: myapp
spec:
  containers:
  - name: myapp
    image: myapp:latest
    resources:
      requests:
        memory: "256Mi"
      limits:
        memory: "512Mi"
  • requests is what the scheduler uses to find a node with enough memory. Set this to your app’s normal usage.
  • limits is the hard cap. If the container exceeds this, Kubernetes kills it. Set this to your app’s peak usage plus some headroom.

Common mistake: Setting requests equal to limits. This guarantees the memory is reserved but wastes resources if the app doesn’t always use it. Only do this for critical workloads where you need guaranteed QoS.

Check node-level memory pressure:

kubectl top nodes
kubectl describe node <node-name> | grep -A 5 "Allocated resources"

If the node is overcommitted (total requested memory exceeds available), pods will get evicted even if they’re under their individual limits.

If kubectl describe pod itself fails to connect, fix the kubectl context before debugging memory limits.

Fix 7: Enable and Configure Swap

By default, Docker limits container swap to the same value as the memory limit. You can allow containers to use swap as a buffer.

docker run --memory=512m --memory-swap=1g myapp

This gives the container 512 MB RAM and 512 MB swap (1 GB total minus 512 MB memory).

To allow unlimited swap:

docker run --memory=512m --memory-swap=-1 myapp

Warning: Swap prevents OOM kills but causes severe performance degradation. Your container will slow to a crawl instead of crashing. This is a band-aid, not a fix. Use it to buy time while you find the real memory issue.

Check if swap is enabled on the host:

free -h
swapon --show

If no swap exists on the host, Docker’s swap settings have no effect. On Docker Desktop, swap is configured through the Desktop settings (see Fix 3).

Fix 8: OOM During Docker Build

If the OOM kill happens during docker build rather than at runtime, the build process is consuming too much memory. This is common with:

  • npm install / npm ci on large projects
  • Webpack / Vite / esbuild bundling
  • Java/Gradle/Maven compilation

Limit build memory:

docker build --memory=4g -t myapp .

For Node.js builds:

RUN NODE_OPTIONS="--max-old-space-size=3072" npm run build

For Java/Gradle builds:

RUN GRADLE_OPTS="-Xmx2g" ./gradlew build

Reduce build parallelism:

# Webpack
RUN NODE_OPTIONS="--max-old-space-size=2048" npx webpack --config webpack.prod.js

# Gradle
RUN ./gradlew build --max-workers=2

Fewer parallel workers means less peak memory at the cost of longer build times.

Still Not Working?

OOM kill without exceeding the visible limit

If docker stats shows the container using less memory than the limit, but it still gets OOMKilled, check for kernel memory (kmem). Kernel memory allocations (network buffers, filesystem cache, etc.) count toward the container’s limit but don’t show up in the standard memory metric.

Check the full memory breakdown:

# For cgroups v1
cat /sys/fs/cgroup/memory/docker/<container-id>/memory.kmem.usage_in_bytes

# For cgroups v2
cat /sys/fs/cgroup/system.slice/docker-<container-id>.scope/memory.current

The container restarts but you can’t catch it

If the container restarts too fast to see the OOM event, check Docker’s event stream:

docker events --filter event=oom

Run this in a separate terminal, then reproduce the issue.

Memory usage spikes during specific operations

Profile your application’s memory during the operation that triggers the OOM. Useful tools:

  • Node.js: --inspect flag with Chrome DevTools Memory tab
  • Java: jmap -heap <pid>, VisualVM, or async-profiler
  • Python: tracemalloc, memory_profiler, or objgraph
  • Go: pprof with runtime.MemStats

Cgroup v1 vs v2 differences

Docker on newer Linux distributions (Ubuntu 22.04+, Fedora 31+) uses cgroups v2, which handles memory accounting differently from v1. Check which version you’re using:

stat -f -c %T /sys/fs/cgroup/
  • cgroup2fs = cgroups v2
  • tmpfs = cgroups v1

Cgroups v2 counts additional memory (like kernel stack memory) toward the container limit that v1 did not. A container that ran fine under v1 might get OOMKilled under v2 with the same memory limit. Increase the limit by 10-20% if you recently upgraded your host OS.

Container keeps getting OOMKilled in a loop

If a container with --restart=always keeps getting killed and restarting:

docker run --restart=on-failure:5 --memory=1g myapp

The on-failure:5 limits restart attempts to 5, so you can inspect the container’s state instead of watching it crash indefinitely.

In Kubernetes, check the restart count and look at the previous container’s logs:

kubectl logs <pod-name> --previous

Host-level OOM vs container-level OOM

If no container has a memory limit set but containers are still getting killed, the host is running out of memory. The kernel’s OOM killer selects the process with the highest oom_score.

Check the OOM score of your container’s process:

# Find the container's PID
docker inspect --format '{{.State.Pid}}' <container-id>

# Check its OOM score
cat /proc/<pid>/oom_score

A higher score means the process is more likely to be killed. You can adjust this (lower = safer):

docker run --oom-score-adj=-500 myapp

Or completely disable OOM killing for a critical container (use with extreme caution — this can hang the entire host):

docker run --oom-kill-disable --memory=2g myapp

Never use --oom-kill-disable without a --memory limit. Without a limit, the container can consume all host memory and freeze the system.

Check for file watchers exhausting memory

On Linux, running many containers with file watchers (dev servers, hot-reload tools) can exhaust inotify limits, which indirectly causes memory pressure. Each watcher takes a small amount of kernel memory, and 50,000+ watchers can push a host into memory pressure that triggers OOM kills on other containers.

Page cache counted against your limit

Linux includes file-backed page cache in memory.usage_in_bytes (cgroups v1) and memory.current (cgroups v2). A container that reads or writes large files (Postgres data dir, log rotation, ML model checkpoints) can hit its memory limit on cache alone, even though the app’s actual working set is small.

Check the breakdown:

# cgroups v2
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.stat

Look for file (page cache) and anon (actual heap). If file is large, mount a tmpfs for hot scratch data or use O_DIRECT reads where possible to bypass the page cache.

runc rejecting OOM hint changes after Docker 24

Docker 24 (Engine release in 2023) updated runc to a version that enforces OCI runtime spec stricter. Some older --oom-score-adj and --oom-kill-disable combinations now error out instead of silently being applied. If docker run fails with runc create failed, drop the OOM flags and use Kubernetes-level QoS classes instead.

Init system inside the container holding memory

If you set --init or use tini to PID 1, the init process itself uses a few MB of memory that counts toward the limit. On very small containers (32–64 MB), this matters. Drop --init for short-lived containers that don’t need signal forwarding or zombie reaping.

Build kit memory ceilings

BuildKit (the default builder since Docker 23) caches layers and dependencies in memory during multi-stage builds. On a CI runner with little RAM, the BuildKit daemon itself can be OOMKilled mid-build with no obvious error in the docker build output. Check the host’s dmesg for buildkitd kills. Limit BuildKit’s parallelism:

docker buildx build --build-arg BUILDKIT_INLINE_CACHE=1 --no-cache-filter <stage> -t myapp .

Or set JOBS=1 in your toolchain (e.g., make -j1) to reduce peak memory during the build.


Related: If you’re troubleshooting Docker socket issues, see Fix: Docker Permission Denied While Trying to Connect to the Docker Daemon Socket. For inotify-based file watcher exhaustion, see Fix: ENOSPC: System Limit for Number of File Watchers Reached.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles