Memory management is one of the most consequential Docker configuration decisions. A container without a memory limit can consume all host memory, triggering the kernel OOM killer across the entire host and taking down unrelated critical services. A container with a limit that is too tight gets OOM-killed during normal load spikes. Getting memory limits right — and knowing how to diagnose memory pressure and leaks in containers — is fundamental to production stability.
How Docker Memory Limits Work
Docker memory limits are enforced by Linux cgroups (control groups). When a container’s memory use exceeds the ‘–memory’ limit, the kernel OOM killer selects a process in the container to kill. If the container process dies, Docker restarts the container according to its restart policy. This produces a clean failure signal: ‘docker inspect
The ‘–memory-swap’ flag controls the combined memory+swap allocation. With ‘–memory=2g –memory-swap=4g’, the container can use up to 2g of RAM and 2g of swap. With ‘–memory=2g –memory-swap=2g’, swap is disabled — the container can only use 2g of RAM and is killed if it exceeds it. With ‘–memory-swap=-1’, unlimited swap is allowed (not recommended for production).
Memory limits in Compose are set under ‘deploy.resources.limits.memory’. In Docker Compose v3, limits require the ‘deploy:’ section. They apply when deploying with ‘docker compose up’ on recent versions of Docker Desktop and Docker Engine that support the deploy key outside of Swarm mode.
The ‘–memory-swappiness’ flag controls how aggressively the kernel swaps container memory pages. A value of 0 tells the kernel not to swap unless absolutely necessary. A value of 100 means the kernel aggressively swaps pages to free physical memory. For production containers where latency matters, set ‘–memory-swappiness=0’.
Memory Configuration and Diagnostics
# Set memory limit with swap disabled
docker run -d \
--name myapp \
--memory=512m \
--memory-swap=512m \
--memory-swappiness=0 \
myapp:latest
# Set memory limit allowing some swap (e.g., for burst tolerance)
docker run -d \
--name myapp \
--memory=512m \
--memory-swap=768m \
myapp:latest
# This allows 512m RAM + 256m swap
# Docker Compose memory limits
services:
app:
image: myapp:1.2.3
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
# Check current memory usage for all containers
docker stats --no-stream --format \
'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}'
# Check if a container was OOM-killed
docker inspect myapp --format='{{.State.OOMKilled}}'
# Check OOM kill status and restart count for all containers
for cid in $(docker ps -q); do
name=$(docker inspect --format='{{.Name}}' $cid | tr -d '/')
oom=$(docker inspect --format='{{.State.OOMKilled}}' $cid)
restarts=$(docker inspect --format='{{.RestartCount}}' $cid)
memlimit=$(docker inspect --format='{{.HostConfig.Memory}}' $cid)
[ "$memlimit" = "0" ] && limit_str="NO LIMIT" || limit_str="$((memlimit/1024/1024))M"
echo "$name: oom=$oom restarts=$restarts limit=$limit_str"
done
# Inspect memory use inside a running container
docker exec myapp cat /proc/meminfo
docker exec myapp cat /sys/fs/cgroup/memory/memory.usage_in_bytesThe cgroup file ‘/sys/fs/cgroup/memory/memory.usage_in_bytes’ shows the container’s actual memory consumption as tracked by the kernel cgroup, which may differ slightly from what ‘docker stats’ shows due to timing. This is the authoritative source for the container’s memory consumption relative to its cgroup limit.
Memory Optimisation Strategies
| Flag / Parameter | Description | Security Note |
|---|---|---|
Right-size memory limits | Set limits based on observed production memory use, not on theoretical requirements. Run the container in production with no limit (or a very high limit) for 48-72 hours while monitoring 'docker stats'. Record the peak memory use. Set the limit to peak + 25-30% headroom. Revisit limits after major application updates or traffic changes. | Setting limits too conservatively causes OOM kills under normal load. Setting them too generously wastes memory that could be used by other containers. Use 'docker stats' data from actual production traffic to set realistic limits — not development load testing results, which typically underestimate production memory use. |
Use Alpine or slim base images | Alpine Linux base images are typically 5-10MB versus 100-200MB for full Debian/Ubuntu images. The smaller base image means less memory used by the OS userspace, libraries loaded at startup, and filesystem cache. For production containers where memory is constrained, switching from 'node:18' (1GB+) to 'node:18-alpine' (~50MB) dramatically reduces the container's memory footprint. | Alpine uses musl libc instead of glibc. Most applications work correctly with musl, but some C extensions and native modules have compatibility issues. Test Alpine-based images thoroughly before switching in production. The memory savings are real, but untested Alpine images in production can cause subtle runtime bugs. |
Configure application-level memory settings | Many runtimes have their own memory management separate from the OS. JVM applications need '-Xmx' set below the container memory limit (JVM does not read cgroup limits by default in older versions). Node.js has a '–max-old-space-size' V8 heap limit. Python does not have a global memory limit but individual libraries may cache aggressively. Set runtime-level limits aligned with the container's cgroup limit. | JVM applications without '-Xmx' set will attempt to claim a fraction of the total host memory, ignoring the container's cgroup limit, until they exceed it and are OOM-killed. This is the most common reason Java containers in Docker are OOM-killed. Always set '-Xmx' (and '-Xms' for consistent behaviour) for JVM applications in containers. |
reservations vs limits | Memory reservations (soft limits) tell the scheduler how much memory to reserve for the container when scheduling decisions are made. Memory limits (hard limits) are enforced by the kernel cgroup. A container can exceed its reservation but not its limit. Reservations are advisory for scheduling; limits are enforced. In production, both should be set: reservations close to expected steady-state use, limits at peak + headroom. | Memory reservations do not prevent a container from consuming more memory than the reservation — they only influence scheduling decisions. A container with a 256M reservation and a 512M limit will use up to 512M before being OOM-killed, regardless of the reservation. |
Diagnosing Memory Problems
Investigating a Memory Leak in a Running Container
A memory leak in a container shows up as steadily increasing memory usage in ‘docker stats’ over time. The first step is confirming the growth is in the application, not in OS buffers or caches.
# Watch memory usage over time (sample every 5 seconds)
watch -n5 'docker stats --no-stream --format "{{.Name}} {{.MemUsage}} {{.MemPerc}}" myapp'
# Inside the container — check process memory
docker exec myapp ps aux --sort=-%mem | head -20
# For Node.js: check heap usage
docker exec myapp node -e 'const v8=require("v8"); console.log(v8.getHeapStatistics())'
# For Python: if heapy or tracemalloc is available
docker exec myapp python3 -c 'import tracemalloc; tracemalloc.start()'
# Check if memory growth is in shared memory (tmpfs mounts)
docker exec myapp df -h | grep tmpfs
docker exec myapp cat /proc/meminfo | grep -E '(MemTotal|MemFree|Buffers|Cached|Shmem)'
# Check cgroup memory stats for detailed breakdown
docker exec myapp cat /sys/fs/cgroup/memory/memory.statHandling OOM Kills in Production
When a container is OOM-killed, the immediate response is to increase the memory limit and then investigate the cause. Repeated OOM kills without investigation indicate a real problem that memory limit increases are only delaying.
# Confirm the container was OOM-killed
docker inspect myapp --format='
OOM Killed: {{.State.OOMKilled}}
Restart Count: {{.RestartCount}}
Exit Code: {{.State.ExitCode}}
Finished At: {{.State.FinishedAt}}'
# Check host dmesg for OOM killer messages
sudo dmesg | grep -i 'oom\|killed process' | tail -20
# Immediate fix: increase memory limit on running container
docker update --memory=1g --memory-swap=1g myapp
# If the container has already stopped, change the Compose file
# and recreate:
# Edit deploy.resources.limits.memory in docker-compose.yml
docker compose up -d --no-deps myapp
# Root cause: is this a one-time spike or a leak?
# Monitor for 24 hours after the limit increase
watch -n30 'docker stats --no-stream --format "{{.Name}} {{.MemUsage}}"'Memory Limits as a Security Control
Memory limits contain damage from malicious processes inside containers. An attacker who achieves code execution inside a container and attempts to exhaust host memory as a denial-of-service attack is limited to the container’s cgroup memory limit. With no limits, a compromised container can trigger OOM kills host-wide, taking down all co-located services.
The ‘memory-swappiness=0’ setting also has a security dimension: data written to memory (encryption keys, session tokens, plaintext credentials cached in application memory) does not get paged to swap files on disk when swappiness is 0. Swap contains cleartext copies of memory contents and can persist after container shutdown. Additional considerations apply when running Docker in production: monitoring container health metrics, setting up centralised log aggregation, implementing image signing and verification workflows, and auditing container runtime configuration against CIS Docker benchmarks. Each of these layers adds defence in depth beyond the controls described above. Review the full security posture regularly as Docker releases update default security settings and new CVEs are discovered in container runtime components.
- Docker OOM kills do not generate application-level error messages — the process is killed by the kernel without the application's error handlers running. Unclosed database connections, uncommitted transactions, and partially written files are all possible consequences. Applications in containers must be designed to handle sudden termination gracefully.
- The host kernel OOM killer may choose to kill host processes (not just container processes) when system memory is critically low, even if individual containers have limits set. Ensure total committed memory across all containers (sum of all –memory limits) does not exceed available host RAM.
- Setting '–memory-swap=-1' (unlimited swap) for a container allows it to use the entire host swap partition. On a production server with multiple containers, this can cause cascading performance problems as all containers start competing for the same swap space. Avoid unlimited swap in production.
- Memory limit changes with 'docker update' take effect immediately for new memory allocations. The container does not restart. If the container is already above the new limit, subsequent allocations will be rejected but existing allocations are not forcibly freed. The OOM kill will happen on the next allocation attempt above the new limit.
Practical Examples
Memory Trending for OOM Prevention
#!/bin/bash
# Log memory usage every minute for trend analysis
# Run as a background cron job: * * * * * /opt/scripts/mem-trend.sh
LOGFILE="/var/log/docker-memory-trend.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
docker stats --no-stream --format '{{.Name}} {{.MemPerc}} {{.MemUsage}}' | \
while read name pct usage; do
echo "$TIMESTAMP $name mem_pct=$pct mem_usage=$usage"
# Alert if any container is above 85% of its limit
PCT_NUM=${pct/\%/}
if awk "BEGIN{exit ($PCT_NUM < 85)}"; then
echo "$TIMESTAMP ALERT: $name at $pct memory usage" | \
tee -a /var/log/docker-memory-alerts.log
fi
done >> "$LOGFILE"Tracking memory usage over time lets you detect gradual leaks (increasing trend over hours/days) versus load spikes (sharp increases that then decrease). An alert at 85% of the limit gives time to investigate before the container is OOM-killed. Adjust the threshold based on your alerting lead time requirements.
Quick Reference: Key Commands and Options
# Inspect current configuration
docker inspect <container> --format='{{json .HostConfig}}' | python3 -m json.tool
# View container resource usage
docker stats <container> --no-stream
# Check container logs
docker logs <container> --tail 50 --timestamps
# Verify running processes inside container
docker exec <container> ps aux
# Check container environment variables
docker exec <container> env | sortThese diagnostic commands work across all container scenarios: ‘docker inspect’ shows the full HostConfig including all runtime parameters, ‘docker stats’ shows real-time resource consumption, and ‘docker logs’ shows output from the main process. Use these as the starting point for any container investigation before diving into more specific tooling.
Common Memory Issues
Problem: Container is repeatedly OOM-killed and increasing the limit does not help — memory use keeps growing
Solution: The container has a memory leak — the application is allocating memory it never frees. Increasing the limit only delays the next OOM kill. Profile memory use inside the container using the application’s profiling tools. Common causes: connection pool growth, unbounded in-memory caches, sessions not being expired, and circular references in garbage-collected languages. The correct fix is in application code, not in the memory limit.
Problem: docker stats shows container memory is low but the host is memory-pressured
Solution: The container’s memory limit does not account for page cache and shared memory. Docker stats shows the container’s RSS (resident set size) plus its share of kernel resources. Check the host’s actual memory with ‘free -h’ and ‘/proc/meminfo’. Also check that no containers have no limit set (‘docker ps -q | xargs docker inspect –format={{.Name}}:{{.HostConfig.Memory}} | grep :0$’).
Problem: Java container is OOM-killed even though the JVM heap is not full
Solution: The JVM uses memory beyond the heap: metaspace (class metadata), native memory, thread stacks, direct byte buffers, and the JVM itself. The container memory limit must be large enough for all JVM memory regions combined. A common rule: set the container limit to JVM -Xmx value plus 256-512MB overhead. Also set ‘-XX:MaxMetaspaceSize’ to prevent unbounded metaspace growth.
Set Limits Based on Observed Use, Disable Swap, Alert at 85%
The three key memory configuration decisions for Docker containers are: set a memory limit on every container based on observed production memory use plus headroom, disable swap with ‘–memory-swap’ equal to ‘–memory’ to produce clean failure signals instead of slow swap degradation, and monitor memory percentage so you catch containers approaching their limits before the OOM killer does. Repeated OOM kills without an application-level fix are a symptom management pattern, not a solution.
- Set memory limits on every production container. A container without a limit can consume all host memory and trigger OOM kills across the entire host, not just in the container.
- Set ‘–memory-swap’ equal to ‘–memory’ to disable swap. Swap use in Docker containers produces slow, hard-to-diagnose performance degradation. An OOM kill is a clearer failure signal than indefinite swap-degraded performance.
- Monitor memory percentage with ‘docker stats’ and alert before reaching 100%. A container at 90% of its memory limit under normal load will be OOM-killed during any traffic spike. Increase limits proactively rather than reactively.
Related Docker Topics
comprehensive CPU and memory resource limit configuration • broader Docker performance optimisation including I/O and CPU • OOM kills as a cause of container restart loops • smaller images reduce base memory footprint at startup
Containers Being OOM-Killed in Production?
INTRAM diagnoses Docker memory issues and configures correct limits for production Linux server deployments.
Get Memory Optimisation Help