Docker Container Monitoring: A Complete Guide
Docker container monitoring means tracking the health, resource usage, and logs of your running containers so you catch failures, memory leaks, and slowdowns before your users do. At minimum you watch CPU, memory, disk, restarts, and health check status per container, plus the logs each one emits. You can start with the built-in docker stats and docker events commands, then add health checks and an alerting layer once you know what normal looks like.
Most teams don't monitor anything until a container falls over at 3am. Then they bolt on a full observability stack, get overwhelmed, and turn most of it off. This guide walks the middle path: the signals that actually predict incidents, the commands to read them, and where to draw the line.
What docker container monitoring actually covers
Four categories cover the vast majority of container incidents.
Resource usage. CPU percentage, memory usage against the container's limit, network I/O, and block I/O. A container creeping toward its memory limit is the single most common cause of a mysterious OOMKilled at midnight.
Container state and restarts. Is the container running, exited, restarting, or paused? A container flapping between restarting and exited is failing its command and looping. That pattern deserves its own attention, and we cover it in how to fix a Docker container stuck in a restart loop.
Application health. A process can be running and still be broken: deadlocked, unable to reach its database, or returning 500s. Container state won't tell you that. A health check will.
Logs. When something breaks, logs are usually where the real cause hides. Monitoring means being able to read them fast, ideally filtered and searchable, not SSHing in and scrolling.
Reading metrics with docker stats
The fastest way to see live resource usage is built in:
docker stats
This streams a live table of CPU %, memory usage and limit, memory %, network I/O, and block I/O for every running container. For scripting or a one-shot snapshot, disable the stream and pick your columns:
docker stats --no-stream \
--format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
That prints once and exits, which is what you want in a cron job or a quick SSH check. If memory usage sits near the limit and refuses to drop after traffic dies down, you probably have a leak. The deep dive on reading these numbers correctly, including why MemUsage includes cache, is in how to monitor Docker container CPU and memory usage.
One caveat: docker stats shows a point in time. It won't tell you a container spiked to 100% CPU an hour ago. For history you need something that records over time, which is where a monitoring tool or a scraped metrics endpoint comes in.
Watching container lifecycle events
To see state changes as they happen, listen to the event stream:
docker events --filter 'type=container'
You'll see start, die, kill, oom, and health_status events in real time. The die event includes the exit code, which tells you a lot. Exit code 137 means the process was killed with SIGKILL, almost always an out-of-memory kill by the kernel. Exit code 143 is SIGTERM, a normal shutdown. To pull just the failures since this morning:
docker events --since '08:00' \
--filter 'event=die' \
--filter 'type=container'
For a point-in-time view of what's up and what died, docker ps -a with a format string is enough:
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.State}}"
Health checks turn "running" into "actually working"
A health check is a command Docker runs inside the container on an interval to decide if it's healthy. Without one, Docker only knows whether the main process is alive, not whether your app can serve requests.
Define it in the Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Or in docker-compose.yml:
services:
api:
image: myapp:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
start_period: 10s
retries: 3
Once a health check exists, the container status shows (healthy) or (unhealthy), and you can inspect the last few probe results:
docker inspect --format '{{json .State.Health}}' my-container | jq
Health checks are worth getting right, because a badly written one either passes when the app is broken or hammers your service every few seconds. We break down the tradeoffs in Docker health checks explained, with examples.
Centralizing logs
By default Docker uses the json-file log driver, and you read logs per container:
docker logs -f --tail 100 --since 15m my-container
--tail limits how far back you start, --since filters by time, and -f follows new output. This is fine for one container on one host. It falls apart across a fleet.
Two things you should do regardless of scale. First, cap log size so a chatty container doesn't fill the disk, which is its own outage:
services:
api:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Second, if you run more than a couple of hosts, ship logs somewhere central. A log driver like journald, fluentd, or a hosted collector means you search one place instead of SSHing into each box.
A monitoring stack, and when you don't need one
The standard open-source answer is cAdvisor to export per-container metrics, Prometheus to scrape and store them, and Grafana to chart and alert. It works well and it's the right call for a large fleet. It's also a real amount of infrastructure to run and keep patched.
If you have a handful of hosts, that stack is often more than the problem needs. A cron job running docker stats --no-stream piped into a check, plus health checks and disk-space alerts, covers most of what actually pages you. Add Prometheus when you genuinely need historical dashboards and multi-dimensional queries, not before.
The piece a metrics stack tends to miss is the human one: you're asleep, or away from your laptop, and a container just died. That's the gap Docker HQ fills. It connects to your hosts over SSH, shows live container state and host CPU, RAM, and disk from your phone, and sends a push notification when a host or a container goes down. You get the alert, open the app, read the logs, and restart the container without opening a laptop. If you're covering on-call for a small team, that's most of a monitoring setup without standing up Prometheus.
Set alert thresholds that mean something
An alert that fires constantly gets muted, and a muted alert is worse than none. Start conservative:
- Container down or unhealthy: alert immediately. This is almost always real.
- Memory over ~90% of limit for several minutes: alert. A brief spike is noise; a sustained climb predicts an OOM kill.
- Restart count climbing: alert. Check it with
docker inspect --format '{{.RestartCount}}' my-container. - Host disk over ~85%: alert. A full disk takes down every container on the host and corrupts things on the way.
CPU is trickier. High CPU is often fine, that's the container doing its job. Alert on sustained high CPU combined with rising latency, not CPU alone.
Frequently asked questions
What is the difference between docker stats and docker inspect?
docker stats gives live resource usage: CPU, memory, network, and disk I/O, updated continuously. docker inspect gives static configuration and current state: the container's config, mounts, network settings, restart count, and health status. Use stats for "how hard is it working right now" and inspect for "how is it configured and is it healthy."
How do I monitor Docker containers without installing extra tools?
Combine the built-ins. docker stats --no-stream for a metrics snapshot, docker ps -a for state, docker events for live lifecycle changes, and docker logs for output. Add a HEALTHCHECK to each image so container status reflects real application health. Wrap those in a small script on a cron schedule and you have a basic monitor with nothing installed beyond Docker itself.
Why does my container get OOMKilled when there's free RAM on the host?
Because the kill is against the container's memory limit, not the host's total memory. If you set --memory=512m and the process exceeds it, the kernel kills it (exit code 137) even with gigabytes free on the host. Check the limit with docker inspect and either raise it or fix the memory growth in the app.
How often should health checks run?
An interval of 30 seconds suits most web services. Shorter intervals catch failures faster but add load and can cause flapping if the check is heavy. Always set a start_period long enough for the app to boot, otherwise the container is marked unhealthy during normal startup and may get restarted before it's ready.
Start with health checks on every service and a disk-space alert on every host. Those two catch more real incidents than any dashboard, and you can add the rest once you know which metric actually wakes you up.