setupmc.com

Minecraft Docker Exit Code 137: Diagnose OOMKilled Correctly

Exit code 137 means SIGKILL, not automatically an out-of-memory event. Confirm Docker OOMKilled state, compare JVM heap with container limits, and leave non-heap headroom.

Performance
setupmc.com Team

Need a cleaner Java baseline?

Generate a Java Compose setup before you keep patching by hand

If you are still refining the Java server baseline, use the Java configurator and then return to the guides for the next issue.

Open Java configurator

Start with the signal, not the assumption

Linux convention reports a process killed by signal 9 (SIGKILL) as exit code 128 + 9 = 137. An OOM killer can send that signal, but so can docker kill, an external watchdog, or an operator.

Confirm the evidence before changing memory.

Capture Docker's state

If a restart policy is looping, disable that policy on the existing container and wait for the next natural failure so that a manual stop does not overwrite its state:

mc_container_id="$(docker compose ps -a -q mc)"
docker update --restart=no "$mc_container_id"
docker wait "$mc_container_id"
docker inspect "$mc_container_id" --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} restarts={{.RestartCount}}'
docker compose logs --tail=250 --timestamps mc

Interpret the combination:

  • exit=137 oom=true: strong evidence that Docker/cgroup memory enforcement killed the container
  • exit=137 oom=false: investigate another SIGKILL source before adding RAM
  • JVM OutOfMemoryError in logs with another exit code: the Java heap failed internally, but Docker did not necessarily kill the container

Check container and host limits

Inspect the effective Compose and runtime values:

docker compose config
docker inspect "$mc_container_id" --format 'limit={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}}'
docker stats --no-stream "$mc_container_id"
docker info

On a Linux host, look for kernel OOM records around the failure:

journalctl -k --since "-30 min" | grep -Ei 'out of memory|oom-kill|killed process'

With cgroup v2, the counters are also useful while the container is running:

docker compose exec mc sh -c 'cat /sys/fs/cgroup/memory.events 2>/dev/null'

An increasing oom_kill counter confirms kills within that cgroup.

Understand the two memory controls

In itzg/minecraft-server, MEMORY, INIT_MEMORY, and MAX_MEMORY configure the Java heap. Docker's mem_limit caps the entire container, including:

  • Java heap
  • metaspace and JIT code cache
  • native mod-loader and compression allocations
  • thread stacks
  • direct/network buffers
  • image helper and other container processes

The image documentation recommends accounting for an extra 25% beyond heap as a general baseline.

Use a limit with real headroom

This is a sensible starting relationship, not a promise that every modpack fits:

services:
  mc:
    image: itzg/minecraft-server:java25
    restart: unless-stopped
    environment:
      EULA: "TRUE"
      TYPE: PAPER
      VERSION: "26.1.2"
      MEMORY: 6G
    mem_limit: 8g
    volumes:
      - ./data:/data

The host still needs free memory for Linux, Docker, filesystem cache, backups, and other services. An 8 GB host is not an appropriate home for an 8 GB container limit.

For JVM allocation diagnostics, temporarily add:

environment:
  DEBUG_MEMORY: "TRUE"

Remove it when the issue is understood.

Fix based on the evidence

Container limit is too close to heap

Reduce MEMORY or raise mem_limit while preserving host headroom. Recreate the service with docker compose up -d mc; a plain restart does not apply Compose changes.

Host itself is exhausted

Stop competing workloads, lower the Minecraft allocation, or move to a host with more physical RAM. Swap can provide a buffer but is much slower and does not turn an undersized host into a healthy modpack server.

OOMKilled is false

Search deployment logs, automation, docker events, and operator history for an explicit kill:

docker events --since 30m --filter container="$mc_container_id"

Also check stop timeouts. A process that does not exit before the grace period can eventually receive SIGKILL.

Memory keeps growing

Do not keep increasing limits indefinitely. Reproduce without newly added plugins/mods, review pack requirements, and profile the workload. Heap pressure, a native leak, and too many loaded chunks require different fixes.

Validate after the change

docker compose up -d mc
docker compose logs -f mc
docker stats --no-stream "$(docker compose ps -q mc)"
docker inspect "$(docker compose ps -q mc)" --format 'oom={{.State.OOMKilled}} restarts={{.RestartCount}}'

Exercise the workload that previously failed: initial pack loading, world generation, peak players, or backup compression. A successful idle start is not enough.

Avoid dangerous shortcuts

  • do not set heap equal to the hard container limit
  • do not disable the OOM killer
  • do not give the container all host RAM
  • do not assume every exit 137 is OOM
  • do not hide repeated OOMs behind an unlimited restart policy

Next steps

Frequently asked questions

Short answers to the questions that usually come up while working through this topic.