July 12, 2026 · Docker HQ Team

How to Monitor Docker Container CPU and Memory Usage

The fastest way to monitor Docker CPU memory usage is docker stats. Run it bare for a live view of every running container, or docker stats <container> for one. It reads CPU percentage, memory usage against the limit, network, and block I/O straight from the kernel's cgroups, so the numbers are real and cost you almost nothing. For history and alerting instead of a one-time glance, pipe those cgroup metrics into cAdvisor plus Prometheus, or watch them from your phone.

Monitor Docker CPU memory with docker stats

Every Docker install ships with this. No agent, no config.

docker stats

You get a table that refreshes every second:

CONTAINER ID   NAME       CPU %   MEM USAGE / LIMIT     MEM %   NET I/O       BLOCK I/O    PIDS
3f2a1b9c8d7e   api        14.2%   256MiB / 512MiB       50.0%   1.2GB/840MB   0B/12MB      23
9a8b7c6d5e4f   postgres    2.1%   180MiB / 1GiB         17.6%   45MB/120MB    8MB/2.4GB    18

Read it like this:

  • CPU % is relative to the host's total capacity. On a 4-core box, a single-threaded container maxing one core shows ~100%, and a container using all four shows ~400%. That surprises people. It is not capped at 100.
  • MEM USAGE / LIMIT shows real usage against the container's memory limit. If you never set a limit, LIMIT is the host's total RAM, which makes MEM % nearly useless. Set limits.
  • PIDS is the process/thread count. A number that climbs forever usually means a thread or fork leak.

For scripts and dashboards, drop the interactive stream and format the output:

docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"

--no-stream grabs a single snapshot and exits, which is what you want in a cron job or an SSH one-liner. The --format flag takes a Go template, so you can emit CSV or JSON and pipe it anywhere.

docker stats --no-stream --format '{{json .}}' | jq

What docker stats won't tell you

It is a live gauge, not a recorder. Close the terminal and the data is gone. There's no history, no percentiles, no alert when a container crosses 90%. If a container got OOM-killed at 3am, docker stats shows you nothing about the moment it died.

The memory number also needs a caveat. By default docker stats reports usage minus the page cache on cgroup v1, but the exact accounting differs between cgroup v1 and v2. If your reported memory looks higher than expected, cache is often the reason. Check which version you're on:

stat -fc %T /sys/fs/cgroup/

cgroup2fs means v2 (the default on modern kernels), tmpfs means v1.

Reading the raw cgroup files

When docker stats disagrees with your intuition, go to the source. The kernel exposes per-container accounting under /sys/fs/cgroup. On cgroup v2:

# find the container's cgroup path, then:
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.current   # bytes in use
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max       # the limit
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.stat      # cache, rss, etc.

The file that saves you at 3am is memory.events:

cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.events
low 0
high 0
max 42
oom 3
oom_kill 3

A non-zero oom_kill means the kernel has already killed processes in this container for exceeding its memory limit. That's the fingerprint behind Docker exit code 137. If you're chasing a container that keeps dying, this counter tells you whether memory is the cause before you waste an hour on anything else.

Set memory and CPU limits, then watch them

Monitoring without limits is half the job. A container with no memory limit can eat the whole host and take everything else down with it. Set the ceiling at run time:

docker run -d \
  --name api \
  --memory="512m" \
  --memory-swap="512m" \
  --cpus="1.5" \
  myapp:latest
  • --memory="512m" is the hard limit. Cross it and the container gets OOM-killed.
  • --memory-swap set equal to --memory disables swap for the container, so a leak fails fast instead of thrashing the disk.
  • --cpus="1.5" caps the container at one and a half cores' worth of time.

In Compose, the same thing lives under deploy.resources:

services:
  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 512M

Once limits are in place, MEM % in docker stats becomes meaningful, and you have a real threshold to alert on.

Getting history and alerts: cAdvisor + Prometheus

For anything beyond a live glance you need a time series. The common stack is Google's cAdvisor scraping the same cgroup metrics and exposing them to Prometheus, with Grafana on top.

cAdvisor runs as one container:

docker run -d \
  --name cadvisor \
  --volume=/:/rootfs:ro \
  --volume=/var/run:/var/run:ro \
  --volume=/sys:/sys:ro \
  --volume=/var/lib/docker/:/var/lib/docker:ro \
  --publish=8080:8080 \
  gcr.io/cadvisor/cadvisor:latest

Hit http://<host>:8080/metrics and you'll see per-container series like container_memory_usage_bytes and container_cpu_usage_seconds_total. Point Prometheus at that endpoint, then write alert rules. A memory alert usually looks like usage over limit for a sustained window:

- alert: ContainerHighMemory
  expr: container_memory_usage_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""} > 0.9
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "{{ $labels.name }} over 90% memory for 5m"

CPU is a rate, not a gauge, so always wrap the counter in rate():

rate(container_cpu_usage_seconds_total{name!=""}[5m])

Raw container_cpu_usage_seconds_total only ever climbs. rate() over a window turns it into cores-per-second, which is the number you actually care about. This is the same pipeline we cover in the complete guide to Docker container monitoring if you want the full setup end to end.

Monitoring from your phone

Setting up Prometheus and Grafana is worth it for a fleet. It's overkill when you have three servers and you just want to know if the API container is pinning CPU while you're away from your desk. That's the gap Docker HQ fills: it connects to your hosts over SSH, streams live CPU and memory per container the same way docker stats does, and sends a push notification when a host or container goes down or crosses a threshold. No agent to install on the server, since it reads the same cgroup data through your existing SSH access. See pricing for the alerting and multi-host tiers.

A quick triage routine

When a host feels slow, this is the order that finds the culprit fastest:

# 1. Which container is hot right now?
docker stats --no-stream

# 2. Did anything get OOM-killed recently?
docker inspect <container> --format '{{.State.OOMKilled}} {{.State.ExitCode}}'

# 3. Is it restarting in a loop?
docker inspect <container> --format '{{.RestartCount}}'

# 4. What is it actually doing inside?
docker top <container>

Step 2 returning true 137 confirms an out-of-memory kill. A high restart count with no OOM usually points at a crash-restart cycle, which is its own rabbit hole covered in how to fix a container stuck in a restart loop.

Frequently asked questions

Why does docker stats show CPU over 100%?

Because the percentage is measured against a single core, not the whole machine. A container using two full cores on an 8-core host reads 200%. To see it as a fraction of the host, divide by the core count, or set --cpus and watch usage against that limit instead.

Does docker stats slow down my containers?

No meaningfully. It reads pre-computed counters the kernel already maintains in cgroups, so the overhead is a few file reads per interval. Running it continuously in a terminal is fine. The cost only shows up if you scrape thousands of containers at a high frequency.

How do I monitor a container that already exited?

docker stats can't help once the process is gone. Use docker inspect <container> to read State.OOMKilled, State.ExitCode, and State.FinishedAt, and check docker logs <container> for the last output before it died. For a real timeline you need a scraper like cAdvisor or Docker HQ that was already recording before the crash.

What's the difference between docker stats memory and RSS?

docker stats reports total memory charged to the cgroup, which can include page cache depending on cgroup version. RSS (resident set size) is only the anonymous memory a process actually holds. Read memory.stat inside the cgroup to split them apart when a number looks too high.

Start by adding --memory and --cpus limits to your containers if you haven't. Without them, every memory percentage you read is measured against the whole host, and the first sign of a leak will be the OOM killer instead of an alert.