Docker Container Monitoring: How to Track Health, Metrics, and Performance
Learn Docker container monitoring, from CPU, memory, and health checks to logs and alerts that catch failing containers early.
GrowthX8 min read

Docker container monitoring keeps a record of each container's central processing unit (CPU) use, memory, health, and logs after the container itself is gone. With that record, you can reconstruct an overnight restart loop down to the exit code and the last health check output. Docker's built-in commands cover one host in real time, and production history needs a pipeline that ships the data off the host.
This guide covers the metrics worth a dashboard and the built-in Docker commands for checking container health. It then shows how to collect production data with cAdvisor, the OpenTelemetry Collector, and log forwarders, and how to turn those signals into early-firing alerts.
What Should Docker Container Monitoring Cover?
Docker container monitoring should cover resource metrics, lifecycle and health events, and logs from containers and the Docker daemon. Linux control groups expose CPU, memory, and block I/O usage for each container as Docker runtime metrics, and the Engine API adds state, health, and events on top of those counters. The configured logging driver captures each container's stdout and stderr.
Container monitoring spans three layers:
- Host and daemon signals: Docker daemon telemetry, container-state counts, and the host disk that containers share.
- Per-container signals: CPU, memory, network, and block I/O against the limits set at docker run, plus restart count, exit codes, out-of-memory (OOM) kills, and HEALTHCHECK results.
- Application signals: The stdout and stderr logs inside each container and the service errors they record.
Little of that history stays on the host. docker stats shows only a live stream, docker events returns only the last 256 events, and the default json-file logging driver doesn't rotate logs, so unmanaged logs compete with containers for disk. Coralogix, a cross-stack observability platform, stores logs and traces as open Apache Parquet in your own cloud bucket, so they stay queryable after the container is removed.
Key Docker Container Metrics to Track
The metrics most likely to explain a failing container are memory working set against its limit, CPU throttling, OOM kills, and restart count. Each metric has a warning sign worth investigating:
| Metric | What it means | Warning sign to investigate |
|---|---|---|
| CPU use and Completely Fair Scheduler (CFS) throttling | Throttled periods divided by total CFS periods gives the share of time the kernel held the container back | Throttled share above 25 percent alongside slower responses |
| Working memory versus limit | container_memory_working_set_bytes compared with container_spec_memory_limit_bytes | Working set holding above 80 to 90 percent of the limit |
| OOM events and OOMKilled | container_oom_events_total counts OOM events, and State.OOMKilled confirms an OOM kill | Any OOM kill, especially with exit code 137 |
| Restart count and exit code | RestartCount and State.ExitCode from docker inspect | Restart count climbing within minutes |
| Health | State.Health.Status reports starting, healthy, or unhealthy | unhealthy or a growing FailingStreak |
Treat the thresholds in these Docker container alert rules as starting points to tune per workload, because acceptable CPU volatility differs between bursty batch jobs and steady services. A short spike alone shouldn't trip an alert, so give each rule a duration window.
Built-In Ways to Check Container Health
Docker has five built-in ways to check container health on one host, with nothing to install, from live usage and health status to lifecycle events, log retention, and the daemon's own metrics.
Live Usage with docker stats
docker stats is the first check to run on one host during an incident. It streams CPU, memory against the limit, network and block I/O, and PIDs for running containers. The --no-stream flag prints one snapshot instead of a live stream.
docker stats --no-stream
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}"
On Linux, the docker stats CLI subtracts cache from total memory usage, using total_inactive_file on cgroup v1 and inactive_file on cgroup v2.
Health Status with HEALTHCHECK
A Dockerfile HEALTHCHECK runs a command inside the container every 30 seconds by default and marks the container unhealthy after three consecutive failures. docker inspect exposes the status and recent check output under State.Health, next to the OOMKilled flag and exit code.
docker inspect --format '{{json .State.Health.Log}}' web
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' web
State.Health.Log keeps the first 4096 bytes of each check's output, which often names the failing dependency. Exit codes above 128 follow the shell exit status convention of 128 plus the signal number, so 137 means SIGKILL, 139 means SIGSEGV, and 143 means SIGTERM. With OOMKilled: true, exit code 137 confirms an out-of-memory kill, and with OOMKilled: false something else sent SIGKILL, such as docker stop forcing out a container that ignored SIGTERM for the 10-second grace period.
Lifecycle Events with docker events
docker events streams container lifecycle actions, including die, oom, kill, restart, and health_status. Repeated filters with the same key combine as OR, and filters with different keys combine as AND.
docker events --filter type=container \
--filter event=die --filter event=oom --filter event=restart \
--since 30m --format '{{json .}}'
The event stream catches a crash loop that docker stats misses, because the container is down between restarts. Piping the JSON Lines output into a log forwarder keeps that history past the daemon's 256-event limit.
Log Drivers and Rotation
The default json-file logging driver doesn't rotate logs until you set its rotation options, since max-size defaults to unlimited and max-file defaults to 1. Values in daemon.json must be quoted strings.
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
The local driver rotates by default, keeping five compressed 20 MB files per container. New settings apply only to containers created after the daemon restarts.
Daemon Metrics Endpoint
The Docker daemon can serve Prometheus metrics about the Docker engine itself through the metrics-addr key in daemon.json, which lives at /etc/docker/daemon.json on Linux. Per-container CPU and memory still come from cAdvisor or the OTel Docker Stats receiver.
{
"metrics-addr": "127.0.0.1:9323"
}
Binding to 127.0.0.1 keeps port 9323 on the loopback interface, and binding to 0.0.0.0 exposes it to the wider network.
How to Collect Docker Metrics for Production
Production history requires a collector or log forwarder on each host that ships metrics, events, and logs to a backend before containers exit, events age out, or logs rotate.
Container Metrics with cAdvisor and Prometheus
cAdvisor collects per-container CPU, memory, network, and filesystem metrics and serves them on port 8080 at /metrics. The documented docker run command runs privileged with read-only host mounts, and the Coralogix Prometheus and Docker guide walks through the full setup. The Prometheus cAdvisor guide collects those metrics with a five-second scrape job:
scrape_configs:
- job_name: cadvisor
scrape_interval: 5s
static_configs:
- targets: [cadvisor:8080]
Container labels multiply cardinality, the number of unique time series a metric creates. Setting --store_container_labels=false with --whitelisted_container_labels keeps only chosen labels, and --docker_only=true drops raw cgroup series other than the root. Coralogix ingests the result through its Prometheus integration, and its metrics engine supports unlimited labels, though Coralogix pricing bills metrics by active series, so each label you keep still adds cost.
Container Metrics with the OTel Docker Stats Receiver
The OpenTelemetry (OTel) Collector contrib distribution includes the Docker Stats receiver, which reads container stats from the Docker socket every 10 seconds by default. The receiver is at alpha stability for metrics, needs Docker API 1.25 or later, and doesn't support macOS hosts.
receivers:
docker_stats:
endpoint: unix:///var/run/docker.sock
metrics:
container.state.health.status:
enabled: true
container.state.health.status is disabled by default, so the metrics block enables it for health alerts. The Collector also needs read access to the socket, which the rootless contrib image lacks by default.
Container Logs with Filelog and Fluent Bit
The OTel filelog receiver can tail json-file output under /var/lib/docker/containers and parse it with the container operator. On a plain Docker host, the file path holds only the container ID, so the receiver can't attach container names from the files alone, a limit tracked in the filelog metadata issue.
receivers:
filelog:
include: [/var/lib/docker/containers/*/*-json.log]
operators:
- type: container
format: docker
Fluent Bit's tail input also reads Docker logs, and its OpenTelemetry output sends them over OpenTelemetry Protocol (OTLP) to a backend. Either path moves logs off the host before rotation removes them.
Alerts That Catch Docker Problems Early
Docker alerts catch problems early when they page on user-facing symptoms and send root causes to tickets or dashboards, the split Prometheus alerting practices recommend. A container at 90 percent of its memory limit is a cause, and rising errors on its service are the symptom. These Prometheus Query Language (PromQL) expressions and event sources are starting points to adapt to each workload:
| Signal | Starting expression or source | Route |
|---|---|---|
| Memory near limit | container_memory_working_set_bytes{name!=""} / (container_spec_memory_limit_bytes{name!=""} > 0) > 0.9 for 5m | Ticket, page if errors rise |
| CPU throttling | sum by (name) (rate(container_cpu_cfs_throttled_periods_total[5m])) / sum by (name) (rate(container_cpu_cfs_periods_total[5m])) > 0.25 | Dashboard |
| Restart loop | Forwarded restart events, counted per 10 minutes | Page if user-facing |
| Unhealthy | container.state.health.status from the OTel receiver, or health_status: unhealthy events | Ticket, page if user-facing errors rise |
| OOM kill | increase(container_oom_events_total[10m]) > 0, confirmed with OOMKilled | Ticket |
| Disk filling | Host filesystem usage on /var/lib/docker from a host metrics exporter | Ticket |
Both memory and throttling rules depend on configured limits, since the > 0 guard skips containers with no memory limit and CFS throttling counters only exist for containers with a CPU limit. container_oom_events_total can stay at zero in some setups, which makes OOMKilled the safer confirmation. None of the cAdvisor expressions count restarts, so forwarded restart events are the dependable source for a restart-loop alert.
Without a shared pipeline, memory arrives from cAdvisor, restarts and health from the event stream, and disk from a host exporter. Coralogix runs in-stream alerting across logs, metrics, traces, and spans, so alerts can fire without waiting for indexing. Notification Center then routes them to destinations such as PagerDuty, Slack, and ServiceNow.
How to Choose a Docker Monitoring Tool
The right Docker monitoring tool depends on how long you need history and whether your workloads will move to Kubernetes. Five criteria separate the options:
- Signal coverage: Container metrics, daemon metrics, events, and logs together.
- Open ingestion: OTLP and Prometheus remote_write, so collectors stay vendor-neutral.
- Pricing unit: Per host, per GB ingested, or per active series.
- Retention after exit: How long logs and metrics stay queryable once a container is gone.
- Kubernetes path: Whether the same collectors, queries, and alerts carry over if workloads move to a cluster.
Four common setups fit different teams:
| Option | Best fit | Trade-off |
|---|---|---|
| Docker CLI (stats, inspect, events) | Single-host triage | No history and only the last 256 events |
| cAdvisor with Prometheus | Self-managed teams | The team runs storage, retention, and cardinality tuning |
| OTel Collector to a backend | Mixed or changing backends | Receiver is alpha, and file logs lack container names |
| Managed observability platform | One place for all signals | Pricing unit and retention vary by vendor |
The Trade-Off Between Host Tools and a Central Pipeline
Built-in Docker commands answer what is wrong on one host right now at no extra cost. They lose history as containers restart, events age out, and logs rotate. A central pipeline keeps that history, but it adds collectors, label decisions, and a bill.
Coralogix keeps that history in storage you own, where container logs stay queryable long after the container is gone. If you want to keep your Docker container history and alerts after each container exits, explore the Coralogix platform. You get queryable container logs and alerts on Docker symptoms in one pipeline.
Frequently Asked Questions About Docker Container Monitoring
How do I check the health of a Docker container?
A Dockerfile HEALTHCHECK gives the container a starting, healthy, or unhealthy status, which docker ps and docker inspect --format '{{.State.Health.Status}}' report. For slow-starting services, --start-period gives the container time to initialize, and probe failures during that window don't count toward --retries. The --timeout option (default 30 seconds) marks any check that runs longer as failed.
Does exit code 137 always mean a Docker container ran out of memory?
No, exit code 137 means the process received SIGKILL. An out-of-memory kill is one cause, and docker inspect --format '{{.State.OOMKilled}}' confirms it. If OOMKilled is false, the application logs and host dmesg output help identify what sent the kill.
How does Docker container monitoring change in Kubernetes?
In Kubernetes, you read container failures from pod status and Kubernetes events such as BackOff and OOMKilling instead of docker events. The kubelet restarts the container within the same pod when the restart policy allows. A rising restart count, rather than a vanished container, is the signal you watch. The Kubernetes monitoring guide covers pod and node signals in more depth.