← All posts
OperationsOct 28, 2026· 7 min read

How to monitor Docker memory usage

Learn to read docker stats, set memory limits, check cgroup files, and know exactly when a container is about to OOM-kill.

By The Opservo team

A container that eats memory silently can OOM-kill your app at 3 am, restart your database mid-transaction, or starve every other service on the host. Most teams notice only after the crash. This guide walks through every useful way to watch Docker memory in real time and over time, what the numbers actually mean, and what thresholds are worth alerting on.

What docker stats shows you (and what it leaves out)

Run `docker stats` with no arguments and Docker prints a live table refreshed every second. The columns that matter for memory are MEM USAGE, LIMIT, and MEM %.

# Live table, all running containers
docker stats

# Example output:
# CONTAINER ID   NAME         MEM USAGE / LIMIT     MEM %
# a3f1b2c4d5e6   api-prod     412MiB / 2GiB         20.12%
# 9e8d7c6b5a4f   redis        88.3MiB / 512MiB      17.24%
# 1a2b3c4d5e6f   postgres     310MiB / 4GiB         7.57%

# One snapshot (no live refresh), all containers
docker stats --no-stream

# Sort by memory (highest first) using a format and sort
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" \
  | sort -t$'\t' -k3 -rh

# Watch a single container
docker stats --no-stream my_container

The `docker stats sort by memory` trick above uses `--format` to get tab-separated output and then sorts on the MEM % column. That is the fastest way to find your worst offender across a fleet of containers.

One important gap: `docker stats` shows RSS plus cache together as MEM USAGE. That can look higher than the real pressure. Cache memory is reclaimed under load, so the number the kernel cares about is RSS (resident set size) alone. More on reading the raw cgroup file below.

How to monitor Docker performance beyond the stats table

Memory alone rarely tells the whole story. A container can sit at 80 % memory and be completely healthy, or it can sit at 40 % and be thrashing swap. Pair memory with two other signals to understand real performance:

  • CPU throttling: check `docker stats` CPU % against the container's CPU quota; if it is consistently at the quota ceiling, the app is CPU-starved not memory-starved.
  • Page faults: high major page faults in `/sys/fs/cgroup/memory/docker/<id>/memory.stat` mean the kernel is reading data from disk -- a sign of memory pressure even when the headline number looks low.
  • Restart count: `docker inspect --format '{{.RestartCount}}' <name>` tells you how many times the container has been killed and restarted; any non-zero value after a fresh deploy is a red flag.

For a broader look at host-level pressure that Docker contributes to, the checklist in Why is my server slow? A 10-minute checklist walks through CPU, memory, disk, and network in one pass. It is worth keeping open alongside this guide.

To watch per-core CPU across all containers on the host, open htop and press F2, then add the `CGROUP` column so you can see which cgroup each process belongs to. That links a runaway process back to its container without needing container IDs.

Reading cgroup memory files directly

The kernel exposes container memory through cgroup v1 or v2 files. These are the ground-truth numbers Docker stats reads from. Checking them directly is useful when you need scriptable output or when you want the RSS figure without cache.

# Find the container's full ID
CID=$(docker inspect --format '{{.Id}}' my_container)

# --- cgroup v1 (most pre-2022 hosts) ---
# Current usage in bytes
cat /sys/fs/cgroup/memory/docker/${CID}/memory.usage_in_bytes

# Limit in bytes (-1 means no limit set)
cat /sys/fs/cgroup/memory/docker/${CID}/memory.limit_in_bytes

# RSS only (excludes page cache)
grep ^rss /sys/fs/cgroup/memory/docker/${CID}/memory.stat
# rss 423231488  <- bytes of real in-use RAM

# OOM kill count (non-zero = container has been OOM-killed before)
cat /sys/fs/cgroup/memory/docker/${CID}/memory.oom_control
# oom_kill_disable 0
# under_oom 0
# oom_kill 3      <- this container has been OOM-killed 3 times

# --- cgroup v2 (Ubuntu 22.04+, Debian 12+, Amazon Linux 2023) ---
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.current
cat /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.max
grep anon /sys/fs/cgroup/system.slice/docker-${CID}.scope/memory.stat
# anon 423231488  <- equivalent of RSS in v2

The `oom_kill` counter is the most actionable number here. If it is greater than zero, the container has already been silently restarted by the kernel. Many teams never see these kills because Docker restarts the container immediately and the log shows nothing obvious.

Setting and checking memory limits

A container without a memory limit borrows from the whole host. One leak takes everything down. Set limits in your Compose file or run command:

# In docker-compose.yml (Compose v3)
services:
  api:
    image: my-api:latest
    deploy:
      resources:
        limits:
          memory: 512m
        reservations:
          memory: 256m

# Or with docker run
docker run -d --memory=512m --memory-reservation=256m my-api:latest

# Confirm the limit is applied
docker inspect --format '{{.HostConfig.Memory}}' my_container
# 536870912   <- 512 MiB in bytes; 0 means no limit

The `--memory-reservation` (soft limit) lets the kernel reclaim from the container under host pressure without killing it. The hard `--memory` limit is what triggers the OOM kill. Both are worth setting in production.

If you run Laravel queue workers or Horizon inside a container, set a generous limit and watch it over a few days before tightening it. Laravel workers accumulate memory across jobs if `--max-memory` is not set on the queue command. See monitor Laravel in production for what to watch specifically.

Redis in a container also deserves a hard limit. If Redis has no `maxmemory` directive in its config and no cgroup limit, it will consume all available RAM on the host. Check current memory use with `docker exec my_redis redis-cli info memory | grep used_memory_human`. More detail on Redis memory pressure is covered in how to monitor Redis memory and eviction.

Docker monitor logs: finding OOM and memory errors

When a container OOM-kills, the event shows up in two places: the kernel ring buffer and the Docker daemon logs.

# Kernel OOM events (look for 'Killed process' or 'oom-kill')
sudo dmesg | grep -iE 'oom|killed process' | tail -20

# Docker daemon log (systemd installs)
sudo journalctl -u docker.service | grep -iE 'oom|kill' | tail -20

# Container's own stdout/stderr since last restart
docker logs --since 1h my_container 2>&1 | tail -50

# Follow logs in real time
docker logs -f my_container

# Events stream: catch OOM kills as they happen
docker events --filter event=oom

The `docker events --filter event=oom` command is the cleanest way to watch for kills in real time. Pipe it to a log file or a monitoring agent. If you want to understand what else might be going wrong at the host level alongside these kills, the guide on how to fix high memory usage on a Linux server covers identifying which process or container is responsible and whether swappiness is making things worse.

What to alert on: concrete thresholds for Docker memory

Not every memory spike deserves a page. Use these thresholds as a starting point and adjust for your workload:

  • MEM % above 85 % of the container limit for more than 5 minutes: warn. Above 95 % for more than 1 minute: page. At 100 % the kernel kills the process.
  • OOM kill count increments: alert immediately, every time. This is a production incident whether or not the container recovered.
  • Container restart count increases: alert on any new restart after the initial deploy period. Use `docker inspect --format '{{.RestartCount}}'` in a cron or monitoring script.
  • No memory limit set (`docker inspect` returns 0): flag at deploy time, not at 3 am. Add a CI check that rejects images deployed without a memory limit.
  • Host free memory below 10 %: even with per-container limits, the host can still be pressured if many containers are near their limits simultaneously. Check with `free -h` and watch `MemAvailable` in `/proc/meminfo`.
Rule: if a container has no memory limit, it can silently consume the entire host. Set `--memory` on every production container, even a generous one, so the kernel kills one service rather than the whole machine.

Keeping this visible without running commands every day

Running `docker stats` by hand is fine for a one-off check. It does not scale to a fleet where you need to know which container drifted past its memory budget overnight, or which one OOM-killed twice this week while the on-call was asleep.

Opservo auto-discovers every Docker container on your Linux servers and tracks memory usage, restart counts, and OOM events continuously. When a container approaches its limit or gets killed, you get a plain-English alert with the container name, the current memory figure, the limit, and a suggested fix, without writing a single PromQL query or Grafana dashboard. The agent is a single static Go binary under 10 MB, installs in one command, and is read-only by default.

Invite the reader to install Opservo's free 2-server plan to get continuous container memory tracking and OOM alerts without running docker stats by hand. — one command installs a read-only agent and your first findings arrive in five minutes: start free. Related: blog how to monitor a docker container s cpu and memory usage.