Articles

How to Monitor a Docker Container's CPU and Memory Usage

A step‑by‑step guide to using Docker’s built‑in stats, setting memory limits, logging historical data, and scaling up with cAdvisor and Prometheus for reliable CPU and memory monitoring.

Written by:
APin

Senior Technology Analyst • Verified Expert

More from this author
How to Monitor a Docker Container's CPU and Memory Usage

A step‑by‑step guide to using Docker’s built‑in stats, setting memory limits, logging historical data, and scaling up with cAdvisor and Prometheus for reliable CPU and memory monitoring.

Why Monitoring Docker Containers Matters

Unmonitored Docker containers represent a significant operational risk, particularly regarding silent resource exhaustion. Without visibility into container behavior, transient anomalies—such as CPU spikes or memory leaks—often manifest as intermittent production instability that vanishes before an engineer can investigate. If a container lacks defined resource constraints, a single runaway process can consume all available host RAM, triggering the kernel's Out-Of-Memory (OOM) killer to terminate critical services, often resulting in systemic failure.

Understanding resource consumption requires distinguishing between host capacity and container limits. Docker metrics, such as those provided by docker stats, report CPU percentages relative to a single core. Consequently, values exceeding 100% indicate that a container is utilizing multiple physical cores rather than exceeding a mathematical boundary. Memory metrics display current Resident Set Size (RSS) against assigned limits; failure to set these limits allows containers to compete for host memory, risking host-wide resource starvation.

To mitigate these risks, engineers should adopt a structured monitoring approach:

  • Implement Strict Resource Limits: Use --memory and --memory-reservation flags during container runtime to establish hard ceilings and soft thresholds, preventing any single container from monopolizing host resources.
  • Establish Audit Trails: For environments without comprehensive observability stacks, utilize simple shell scripts to pipe docker stats output into CSV files, enabling historical analysis of performance trends.
  • Utilize Specialized Exporters: Deploy cAdvisor to scrape cgroup data and expose Prometheus-compatible metrics. This enables the tracking of nuanced telemetry, such as the container_memory_working_set_bytes, which provides a more accurate representation of memory pressure than raw usage figures.

Effective monitoring should focus on actionable thresholds: sustained CPU utilization above 80% for over five minutes, memory usage approaching 90% of configured limits, and increasing restart counts. By identifying these patterns proactively, engineering teams can resolve performance degradations before they escalate into production outages.

Quick Real‑Time Insights with docker stats

The docker stats command provides immediate, real-time visibility into resource consumption across active containers. By default, it streams live updates for all containers, but engineers can target specific services or capture discrete data points for automated analysis.

To capture a point-in-time snapshot, use the --no-stream flag. This is essential for integrating container metrics into shell scripts, cron jobs, or lightweight audit logs. You can refine the output by using the --format flag with Go templates to extract specific fields like container name, CPU percentage, and memory utilization.

Interpreting Resource Metrics

  • CPU Percentage: The reported value is relative to a single core. If a container reports 250%, it is consuming the equivalent of 2.5 full cores. On a multi-core host, values exceeding 100% indicate high utilization across multiple threads or processes, not an error.
  • Memory Usage: Displays data in the format current_usage / limit. The "current" value represents Resident Set Size (RSS) usage. The limit is the explicit constraint defined at runtime; if no limit is configured, this figure defaults to the host’s total available memory.

For persistent monitoring, you can pipe formatted snapshots into a CSV file. For instance, the following command records a log entry every 30 seconds:

while true; do docker stats --no-stream --format '{{.Name}},{{.CPUPerc}},{{.MemUsage}}' >> /var/log/container-stats.csv; sleep 30; done

Because containers without explicit limits can consume host RAM until the kernel Out-of-Memory (OOM) killer intervenes, engineers should verify settings via docker inspect or by checking raw cgroup files at /sys/fs/cgroup/memory/docker/<container_id>/memory.usage_in_bytes. Defining hard limits with --memory and soft thresholds with --memory-reservation during deployment is a critical practice for maintaining host stability and preventing cross-container resource starvation.

Setting and Verifying Memory Limits

Docker enforces memory isolation through two complementary controls: a hard limit (--memory) that the kernel will never allow a container to exceed, and a soft reservation (--memory-reservation) that acts as a warning threshold. The hard limit caps the cgroup memory.limit_in_bytes value; once reached, the kernel triggers an OOM‑kill for processes inside the container. The reservation sets memory.soft_limit_in_bytes; when the host’s free memory falls below this value, the kernel preferentially reclaims memory from containers that have only met their reservation, helping to avoid host‑wide starvation.

Applying the limits

docker run \
  --memory="512m" \
  --memory-reservation="400m" \
  my-image
  • --memory="512m" establishes a hard ceiling of 512 MiB.
  • --memory-reservation="400m" signals that the container should stay below 400 MiB under memory pressure.

Verifying the configuration

  • Inspect the container’s JSON definition:
    docker inspect  | grep -i memory
    Look for Memory and MemoryReservation fields, which reflect the values passed at run time.
  • Read the raw cgroup files on the host:
    cat /sys/fs/cgroup/memory/docker//memory.limit_in_bytes
    cat /sys/fs/cgroup/memory/docker//memory.soft_limit_in_bytes
    cat /sys/fs/cgroup/memory/docker//memory.usage_in_bytes
    The first two commands confirm the hard and soft limits; the third shows current RSS‑style usage.

Why limits matter

Without a hard limit, a runaway process can allocate all available RAM, causing the kernel’s OOM‑killer to terminate unrelated services—a scenario that “starves the host.” By enforcing a ceiling, the container is forced to respect the host’s memory pool, and the soft reservation provides an early‑warning mechanism that allows the kernel to reclaim memory before the hard limit is hit. This isolation is essential for compliance frameworks such as SOC 2 or ISO 27001, where resource exhaustion is considered a security risk, and for NIST guidelines that recommend limiting the blast radius of compromised workloads.

In practice, combine the docker stats --no-stream output with the cgroup checks above to confirm that the container stays within its allocated envelope during load testing, ensuring that no single container can jeopardize the stability of the entire host.

Collecting Historical Data with Simple Scripts

Docker ships with the docker stats command, which can emit a single snapshot of container metrics when used with --no‑stream. The output can be formatted with Go templates, making it suitable for ingestion by scripts that need a stable, parsable representation such as CSV.

Below is a minimal Bash loop that records the container name, CPU percentage, and memory usage every 30 seconds. The redirection appends each line to /var/log/container-stats.csv, creating a time‑ordered audit trail that can be grepped later.

#!/usr/bin/env bash
# Ensure the CSV has a header row on first run
if [ ! -f /var/log/container-stats.csv ]; then
    echo "timestamp,container,cpu_perc,mem_usage" > /var/log/container-stats.csv
fi

while true; do
    ts=$(date --iso-8601=seconds)
    docker stats --no-stream --format "{{.Name}},{{.CPUPerc}},{{.MemUsage}}" \
        | awk -v t="$ts" '{print t "," $0}' >> /var/log/container-stats.csv
    sleep 30
done

Running the loop interactively is fragile because an SSH disconnect will terminate the process. Two common approaches keep the collector alive:

  • tmux or screen – start a new session, launch the script, and detach. The session persists on the host and can be re‑attached for troubleshooting.
  • systemd service – define a unit file (e.g., /etc/systemd/system/docker‑stats‑collector.service) with Type=simple and Restart=always. Enabling the unit ensures the script starts at boot and is automatically restarted after failures.

Both methods avoid the need for a full‑blown monitoring stack while still providing a reliable data source for post‑mortem analysis. When an incident occurs, engineers can:

  • Filter the CSV for the time window of interest using grep or awk.
  • Plot the columns with tools like gnuplot or import into a spreadsheet to visualize CPU and memory trends.
  • Correlate spikes with deployment timestamps, configuration changes, or external load patterns.

Because the collector writes plain text, it complies with audit‑logging requirements found in standards such as SOC 2 or ISO 27001, which mandate immutable records of system behavior. The approach is intentionally lightweight; it does not replace dedicated time‑series databases but offers a quick, reproducible way to capture the state of containers for later forensic review.

Deep Dive: cAdvisor and Prometheus Integration

cAdvisor (Container Advisor) is an open‑source exporter that runs as a Docker container, reads cgroup metrics from the host, and presents them in a format compatible with Prometheus. Deploying it requires only read‑only mounts of the Docker runtime and system files, which limits the attack surface while still granting access to the necessary kernel interfaces.

docker run \
    --volume=/var/run:/var/run:ro \
    --volume=/sys:/sys:ro \
    --volume=/var/lib/docker:/var/lib/docker:ro \
    --publish=8080:8080 \
    --detach=true \
    --name=cadvisor \
    gcr.io/cadvisor/cadvisor

After the container starts, two endpoints become available:

  • UI: http://<host>:8080/containers/ – a built‑in web interface that renders per‑container CPU and memory graphs.
  • Metrics: http://<host>:8080/metrics – a Prometheus‑compatible endpoint exposing series such as container_cpu_usage_seconds_total, container_memory_usage_bytes, and container_memory_working_set_bytes.

To ingest these metrics, add a scrape job to the Prometheus configuration:

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

Prometheus stores the time‑series data on local or remote storage back‑ends, enabling alerting rules that reference the same metrics used by the UI. A typical alert for sustained high CPU might look like:

ALERT ContainerHighCPU
  IF sum(rate(container_cpu_usage_seconds_total[1m])) BY (container) > 0.8
  FOR 5m
  LABELS {severity="critical"}
  ANNOTATIONS {
    summary = "CPU usage > 80% for 5 minutes",
    description = "Container {{ $labels.container }} is consuming excessive CPU."
  }

Grafana connects to Prometheus as a data source and provides long‑term visualizations. After adding the Prometheus data source, import or create dashboards that query the same series, for example:

  • CPU usage: rate(container_cpu_usage_seconds_total[5m])
  • Memory working set: container_memory_working_set_bytes
  • Restart count (via container_restart_total if exported)

By combining cAdvisor’s low‑overhead collection, Prometheus’s durable storage, and Grafana’s flexible panels, engineering teams obtain a complete observability stack that satisfies compliance frameworks such as SOC 2 or ISO 27001, which require systematic monitoring and retention of operational metrics.

Practical Alerting Thresholds

Before defining alert rules, understand the metrics that Docker exposes natively. The docker stats command streams per‑container CPU percentage (relative to a single core), current memory usage, and the configured memory limit. For example, docker stats --no‑stream --format '{{.Name}},{{.CPUPerc}},{{.MemUsage}}' produces a CSV line that can be logged or fed to a time‑series database. When a container runs without a --memory limit, its RSS can grow until the host kernel OOM‑killer intervenes, which is why hard limits are a prerequisite for reliable alerting.

Alerting should focus on conditions that indicate a sustained problem rather than a transient spike. The following concrete thresholds have proven effective in production environments:

  • CPU utilization > 80 % for > 5 minutes – a brief burst is normal; continuous high usage usually means a stuck thread or unexpected traffic load.
  • Memory usage > 90 % of the container’s hard limit – crossing this boundary puts the process within a few megabytes of an OOM kill.
  • Monotonic memory growth over several hours – a steadily rising container_memory_working_set_bytes without plateau is a classic memory‑leak signature.
  • Restart count increase – a rising value from docker inspect … | grep RestartCount signals repeated crashes that may not generate visible errors.

Implementing these thresholds can be done with a simple script that writes docker stats snapshots to a log and evaluates the last N entries. Below is a minimal Bash example that triggers a warning when CPU exceeds 80 % for three consecutive 30‑second samples:

#!/usr/bin/env bash
while true; do
  line=$(docker stats --no-stream --format '{{.Name}} {{.CPUPerc}}')
  cpu=$(echo $line | awk '{print $2}' | tr -d '%')
  if (( ${cpu%.*} > 80 )); then
    ((high++))
  else
    high=0
  fi
  ((high >= 3)) && echo "ALERT: $line sustained high CPU" && high=0
  sleep 30
done

For teams that require historical analysis and automated notification, exporting cAdvisor metrics to Prometheus and defining alert rules in Prometheus’ alertmanager provides a scalable solution. Tools such as Opservo can consume these Prometheus alerts, enrich them with context (e.g., recent deploys), and present them in plain language, reducing the cognitive load on on‑call engineers.

By establishing the four thresholds above, enforcing memory limits, and wiring the metrics into a time‑series store, engineers gain a deterministic baseline from which to detect performance degradation before it escalates to an incident.

Editorial Policy & Research Methodology

Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.

Have an Idea?

Let's Build Something Amazing Together.