How to Obtain Usage Statistics of a Running Container: Docker Stats and Resource Monitoring

How to Obtain Usage Statistics of a Running Container

Somewhere between “the container is running” and “the container is healthy” there’s a whole category of problems that only docker stats catches — a memory leak that’s slowly climbing toward the limit, a container pegging CPU because of a runaway loop, or a network-chatty process eating bandwidth nobody accounted for. I check docker stats reflexively any time something feels sluggish, before I even open logs. This guide covers the command itself, how to make it scriptable, and what’s actually happening under the hood via cgroups — because understanding that is what lets you interpret the numbers correctly instead of guessing.

Basic Usage

docker stats

This launches a live, auto-refreshing table for all running containers:

CONTAINER ID   NAME         CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
a1b2c3d4e5f6   web          2.34%     45.2MiB / 512MiB       8.83%    1.2kB / 850B      0B / 4.1kB        4
f6e5d4c3b2a1   worker       78.91%    390MiB / 512MiB        76.17%   3.4kB / 1.2kB     12MB / 0B         9

Press Ctrl+C to exit — this doesn’t stop any containers, only detaches the view.

Scoping to Specific Containers

docker stats web worker

Or by pattern using shell expansion with docker ps:

docker stats $(docker ps --filter "name=web" --format "{{.Names}}")

Getting a One-Time Snapshot (Not Live-Streaming)

By default docker stats streams continuously, which is awkward in scripts. Use --no-stream for a single sample:

docker stats --no-stream

Expected output: the same table format, printed once, then the command exits immediately — ideal for cron jobs, health-check scripts, or logging snapshots at intervals.

Machine-Readable Output With --format

For scripting or feeding into a monitoring pipeline, use Go template formatting:

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

Expected output:

NAME      CPU %     MEM USAGE / LIMIT     MEM %
web       2.34%     45.2MiB / 512MiB      8.83%
worker    78.91%    390MiB / 512MiB       76.17%

For JSON (much easier to parse programmatically):

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

Expected output — one JSON object per container:

{"BlockIO":"0B / 4.1kB","CPUPerc":"2.34%","Container":"a1b2c3d4e5f6","ID":"a1b2c3d4e5f6","MemPerc":"8.83%","MemUsage":"45.2MiB / 512MiB","Name":"web","NetIO":"1.2kB / 850B","PIDs":"4"}

Pipe through jq to alert on thresholds:

docker stats --no-stream --format '{{json .}}' | jq -r 'select(.MemPerc | rtrimstr("%") | tonumber > 75) | "\(.Name) is at \(.MemPerc) memory usage"'

Expected output when a container crosses 75% memory:

worker is at 76.17% memory usage

Scripted Polling for Monitoring Pipelines

A simple script that logs stats every 10 seconds to a CSV for later analysis:

#!/usr/bin/env bash
# poll-stats.sh
OUTFILE="container-stats.csv"
echo "timestamp,name,cpu_perc,mem_usage,mem_perc" > "$OUTFILE"

while true; do
  ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  docker stats --no-stream --format '{{.Name}},{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}' \
    | while IFS=, read -r name cpu mem_usage mem_perc; do
        echo "${ts},${name},${cpu},${mem_usage},${mem_perc}" >> "$OUTFILE"
      done
  sleep 10
done
chmod +x poll-stats.sh
nohup ./poll-stats.sh &

This is a lightweight starting point — for anything beyond ad-hoc analysis, feed the same data into Prometheus instead (see below), which handles retention, querying, and alerting far better than a flat CSV file.

Inspecting a Single Container’s Raw cgroup Stats via the API

docker stats is actually a formatted view over the Docker Engine API’s /containers/{id}/stats endpoint, which returns raw cgroup accounting data. You can query it directly:

curl --unix-socket /var/run/docker.sock \
  "http:/containers/web/stats?stream=false" | jq .

Expected output (trimmed): a large JSON object including cpu_stats, precpu_stats, memory_stats, networks, and blkio_stats — this is the raw data docker stats computes its percentages from. Useful when you need finer-grained fields than the CLI table exposes, like per-CPU usage or detailed memory breakdown (cache, rss, swap).

{
  "memory_stats": {
    "usage": 47431680,
    "limit": 536870912,
    "stats": {
      "cache": 2048000,
      "rss": 45383680
    }
  }
}

How Docker Calculates These Numbers (cgroups Under the Hood)

Docker containers get their resource accounting from Linux control groups (cgroups) — the same kernel mechanism that enforces resource limits. Every container is assigned a cgroup, and the kernel tracks CPU time, memory usage, and I/O directly against it. Docker doesn’t measure anything itself; it reads these kernel-maintained counters and does the math.

CPU % is calculated as:

cpu_delta = current_cpu_usage - previous_cpu_usage
system_delta = current_system_cpu_usage - previous_system_cpu_usage
CPU % = (cpu_delta / system_delta) * number_of_cpus * 100

This is why CPU percentages can exceed 100% for multi-threaded containers on multi-core hosts — a container using 2 full cores shows as ~200%.

MEM USAGE is the cgroup’s tracked memory, which by default includes page cache on cgroup v1 — meaning a container reading a lot of files from disk can show inflated memory usage that isn’t actually “at risk” memory the OOM killer would reclaim under pressure. On cgroup v2 (default on modern Linux distros and Docker 20.10+), this accounting is more accurate and generally excludes reclaimable cache from the pressure calculation, though docker stats display still includes cache in the raw usage number.

Check which cgroup version your host uses:

docker info --format '{{.CgroupVersion}}'

Expected output:

2

Setting Resource Limits (What “LIMIT” in the Output Means)

The LIMIT column reflects whatever you constrained the container to at run time — if unset, it defaults to the host’s total memory. Set explicit limits:

docker run -d --name limited-app \
  --memory=256m \
  --memory-swap=256m \
  --cpus="0.5" \
  nginx:alpine
docker stats limited-app --no-stream

Expected output:

NAME           CPU %     MEM USAGE / LIMIT     MEM %
limited-app    0.05%     3.2MiB / 256MiB        1.25%

--cpus="0.5" caps the container to half a CPU core — under sustained load, docker stats CPU% for this container will plateau near 50% even though more capacity exists on the host, because the cgroup CPU quota is throttling it.

Docker Compose Resource Limits and Stats

# docker-compose.yml
services:
  worker:
    image: myapp/worker:latest
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          memory: 256M

Note: deploy.resources is honored by docker compose up for local development in recent Compose versions, and always honored in Swarm mode. Check applied limits:

docker inspect worker --format '{{.HostConfig.Memory}}'

Feeding Stats Into Prometheus for Real Monitoring

For anything beyond ad-hoc checks, run cAdvisor, which exposes per-container resource metrics in Prometheus format — this is the standard approach in production rather than polling docker stats yourself:

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 \
  --volume=/dev/disk/:/dev/disk:ro \
  --publish=8080:8080 \
  --privileged \
  --device=/dev/kmsg \
  gcr.io/cadvisor/cadvisor:v0.49.1

Verify metrics are exposed:

curl -s http://localhost:8080/metrics | grep container_memory_usage_bytes | head -3

Expected output:

container_memory_usage_bytes{container_label_...,name="web"} 4.7431680e+07

Point a Prometheus scrape config at it:

scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['localhost:8080']

From there, Grafana dashboards and alerting rules (e.g., container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.9) give you the sustained, historical view that a live docker stats terminal session can’t.

Security Considerations

  • cAdvisor requires broad host mounts and --privileged to read cgroup/proc data — restrict it to trusted internal networks and don’t expose port 8080 publicly without authentication in front of it (e.g., a reverse proxy with auth).
  • Access to docker stats requires Docker socket access, same root-equivalent caveat as every other command in this series — don’t grant docker group membership casually on shared/multi-tenant hosts.
  • Resource limits (--memory, --cpus) are a security boundary as much as a performance one — an unconstrained container can starve co-located workloads (noisy-neighbor problem) or exhaust host memory, triggering the OOM killer against unrelated processes.

Troubleshooting

SymptomCauseFix
docker stats shows -- for all fieldsContainer just started, first sample not yet availableWait one refresh cycle (~1s)
MEM % seems too high for what the app should usecgroup v1 including page cache in usageCheck docker info cgroup version; consider upgrading host to cgroup v2
CPU % stuck near a fixed ceiling--cpus limit throttling the containerCheck docker inspect --format '{{.HostConfig.NanoCpus}}'
docker stats hangs / very slow to updateLarge number of containers on the hostScope to specific containers instead of the full fleet
cAdvisor container fails to startMissing --privileged or required host mountsRe-check the exact volume/device flags in the run command above

Real-World Deployment Notes

docker stats is the tool I reach for first because it needs zero setup — no exporter, no scrape config, just a running Docker daemon. That makes it ideal for the “something feels off, let me check right now” moment. But I’ve learned not to trust a single snapshot too much: a container can look fine at the instant you check and still have been thrashing memory five minutes earlier. That gap is exactly why cAdvisor-plus-Prometheus earns its place once a service matters enough to have an on-call rotation — it turns “what does this look like right now” into “what did this look like during the incident window,” which is almost always the more useful question during a postmortem.

Summary

docker stats gives you a real-time window into what a container is actually doing on the host — CPU, memory, network, and block I/O — sourced directly from the kernel’s cgroup accounting rather than anything Docker computes independently. It’s the right tool for live debugging and quick sanity checks; for anything you need historical trends or alerting on, graduate to cAdvisor plus Prometheus and Grafana, which read the same underlying cgroup data but retain and query it properly over time.

References

Total
2
Shares

Leave a Reply

Previous Post
How to Get Detailed Information About a Container with docker inspect

How to Get Detailed Information About a Container with Docker Inspect: Complete Guide

Next Post
How to Listen to Docker Events on Your Docker Hosts

How to Listen to Docker Events on Your Docker Hosts: Real-Time Event Monitoring Guide

Related Posts