DEV Community

Cover image for Your Spring Boot App Works Locally but Gets OOM Killed in Docker. Why?
Md Jamilur Rahman
Md Jamilur Rahman

Posted on

Your Spring Boot App Works Locally but Gets OOM Killed in Docker. Why?

You built a Spring Boot app. It runs perfectly on your laptop. You put it in a Docker container, deploy it, and boom. The container crashes. No error log. No stack trace. Just gone.

The operating system killed your app because it used too much memory. This is called OOM Killed (Out Of Memory Killed).

This happens every day to Java developers who deploy to containers. And it is one of the best interview questions to test if someone understands how Java and Docker really work together.

Here is the full breakdown.

What Is OOM Killed?

When a process uses more memory than the system allows, the Linux kernel kills it. No warning. No graceful shutdown. The process just stops.

In Docker, you can set a memory limit. For example, docker run -m 512m myapp means the container can only use 512 MB of RAM. If your Java app tries to use more than that, the kernel kills it.

You can check if your container was OOM killed by running:

docker inspect <container_id> --format='{{.State.OOMKilled}}'
Enter fullscreen mode Exit fullscreen mode

If this returns true, that is your answer.

Why Does Java Use More Memory in Docker?

This is the core problem. There are three reasons.

1. The JVM Does Not Know About Container Limits

On your laptop, the JVM sees all your RAM. Let us say you have 16 GB. By default, the JVM sets its maximum heap size to about 1/4 of total RAM. That is 4 GB.

But in a Docker container with a 512 MB limit, the JVM still looks at the host machine's total RAM, not the container limit. So it might try to use 4 GB inside a 512 MB container. The kernel kills it.

This was fixed in Java 8u191 and later, but only if you use the right flags.

2. Heap Memory Is Not the Only Memory

Most developers only think about heap memory. That is what -Xmx controls. But the JVM uses a lot more memory than just the heap:

  • Heap: Where your objects live (controlled by -Xmx)
  • Metaspace: Where class definitions are stored
  • Thread stacks: Each thread gets its own stack (about 1 MB per thread)
  • Direct buffers: Used by NIO and Netty
  • JIT compiler: Compiles bytecode to machine code
  • GC data: Garbage collector needs memory to track objects

A typical Spring Boot app with -Xmx256m might actually use 400-500 MB total. The heap is only part of the story.

3. Spring Boot Uses a Lot of Memory by Default

A fresh Spring Boot app with just spring-boot-starter-web starts with about 200-250 MB of total memory. Add a database connection pool (HikariCP default is 10 connections), some caching, and you are at 350-400 MB easily. And you have not even started your business logic yet.

The Fix: Three Things You Must Do

Fix 1: Use Container-Aware Memory Settings

Stop using -Xmx with fixed values. Instead, use this flag:

java -XX:MaxRAMPercentage=75.0 -jar myapp.jar
Enter fullscreen mode Exit fullscreen mode

This tells the JVM: "Use 75% of whatever memory is available in this container." If the container has 512 MB, the JVM heap will be about 384 MB. If the container has 1 GB, the heap will be about 768 MB.

This is much better than -Xmx256m because it automatically adjusts to the container limit.

Why 75% and not 100%? Because the JVM needs the remaining 25% for non-heap memory (metaspace, threads, GC, etc.).

Fix 2: Set the Docker Memory Limit Higher Than You Think You Need

If your app needs 256 MB of heap, do not set the container to 256 MB. The JVM will need at least 30-40% more for non-heap memory. Set it to at least 400 MB.

# docker-compose.yml
services:
  myapp:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M
    environment:
      - JAVA_OPTS=-XX:MaxRAMPercentage=75.0
Enter fullscreen mode Exit fullscreen mode

Fix 3: Reduce Spring Boot's Memory Appetite

Here are the most effective ways to cut memory usage:

Reduce thread count:

# application.yml
server:
  tomcat:
    threads:
      max: 50  # default is 200
Enter fullscreen mode Exit fullscreen mode

Each Tomcat thread uses about 1 MB of stack memory. 200 threads = 200 MB just for stacks. Most apps do not need 200 threads.

Reduce connection pool:

spring:
  datasource:
    hikari:
      maximum-pool-size: 5  # default is 10
Enter fullscreen mode Exit fullscreen mode

Each database connection uses memory for buffers and tracking. 5 is usually enough for a small app.

Turn off things you do not use:

spring:
  jmx:
    enabled: false  # saves ~20 MB
  devtools:
    restart:
      enabled: false  # never use devtools in production
Enter fullscreen mode Exit fullscreen mode

How to Debug Memory Issues in Docker

If your container keeps getting OOM killed and you do not know why, here is a step-by-step approach.

Step 1: Check if it really is OOM

docker inspect <container_id> --format='{{.State.OOMKilled}}'
Enter fullscreen mode Exit fullscreen mode

Step 2: Check actual memory usage

docker stats <container_id>
Enter fullscreen mode Exit fullscreen mode

This shows you how much memory the container is actually using. If it is close to the limit, you found your problem.

Step 3: See what the JVM is doing

docker exec <container_id> jcmd 1 VM.native_memory summary
Enter fullscreen mode Exit fullscreen mode

This shows a breakdown of all JVM memory usage: heap, metaspace, threads, JIT, everything.

You need to enable Native Memory Tracking at startup:

java -XX:NativeMemoryTracking=summary -XX:MaxRAMPercentage=75.0 -jar myapp.jar
Enter fullscreen mode Exit fullscreen mode

Step 4: Check heap usage

docker exec <container_id> jcmd 1 GC.heap_info
Enter fullscreen mode Exit fullscreen mode

This tells you how much heap is used vs. how much is allocated.

A Real Example

Let us say you have a Spring Boot app deployed on a VPS with 1 GB RAM. You run it in Docker with a 512 MB limit.

With default settings:

  • JVM sees 1 GB host RAM, sets heap to ~256 MB
  • Metaspace: ~80 MB
  • Thread stacks (200 default): ~200 MB
  • Other JVM overhead: ~60 MB
  • Total: ~596 MB in a 512 MB container
  • Result: OOM Killed

With our fixes:

  • -XX:MaxRAMPercentage=75.0: Heap = ~384 MB
  • Reduce threads to 50: ~50 MB
  • Reduce connection pool: saves ~30 MB
  • Turn off JMX: saves ~20 MB
  • Total: ~464 MB in a 512 MB container
  • Result: Runs fine with 48 MB headroom

What About Bangladesh VPS Deployments?

Most BD developers deploy to cheap VPS instances with 1-2 GB RAM. They run the database, the app, and sometimes Redis on the same machine. Memory is tight.

Here is a practical setup for a 1 GB VPS running Spring Boot + PostgreSQL:

  • PostgreSQL: ~200 MB
  • Spring Boot with our fixes: ~450 MB
  • OS and other processes: ~200 MB
  • Remaining buffer: ~150 MB

This works. Without the fixes, Spring Boot alone would try to use 600+ MB and the whole system would grind to a halt.

Interview Answer Template

If you get this question in an interview, structure your answer like this:

  1. Explain what OOM Killed means: The Linux kernel kills processes that exceed their memory limit.

  2. Explain why Java is special: The JVM uses much more memory than just the heap. Heap is only 50-60% of total JVM memory.

  3. Explain the container problem: By default, the JVM looks at host memory, not container limits. So it allocates more than the container allows.

  4. Give the fix: Use -XX:MaxRAMPercentage=75.0 instead of -Xmx. Set container limits 30-40% higher than heap size. Reduce thread count and connection pool.

  5. Show you can debug it: Mention docker stats, jcmd VM.native_memory, and OOMKilled inspection.

This shows the interviewer you understand the full picture: Java, Docker, Linux, and production reality.

Quick Reference

Setting Default Recommended for Containers
Max heap 1/4 of host RAM -XX:MaxRAMPercentage=75.0
Tomcat threads 200 50 (or less for small apps)
HikariCP pool 10 5 (or less for small apps)
JMX enabled disabled in production
DevTools auto always disabled in production

Sources

Top comments (0)