How to monitor Docker containers from outside your host
Updated Aug 19, 2026
Docker is very good at restarting things and very bad at telling you it had to. A container that crash loops all night looks much like one that has been up for a month, and nothing anywhere sends you a message.
The gap between running and working
Container monitoring in most homelabs is a dashboard of green squares, checked when someone remembers. That catches nothing, because the interesting failures do not change the colour of the square.
- A container is running and its process is wedged. The restart policy never fires because nothing exited.
- A container exits, restarts, exits again, for hours. Uptime resets constantly and no alert exists for it.
- A bind mount points at a path that vanished after a NAS reboot, so the app runs against an empty directory and appears healthy.
- The whole host is down, which also silences any monitoring you were running in a container on it.
That last point is the important one. Watchtower, a dashboard, and a self hosted uptime tool all share a fate with the daemon they inspect. Something outside the host has to be keeping time.
Do not expose the Docker API
The obvious idea is to let a monitoring service query Docker directly. Docker documents that binding the daemon to a TCP socket gives unencrypted and unauthenticated direct access by default, and that doing so risks non root users gaining root on the host. Anyone who reaches that socket can give any instruction to the daemon.
There is no version of publishing /var/run/docker.sock or the remote API to the internet that is worth a green dot. Everything below leaves the socket where it is.
Read health locally, report outward
Docker already computes container health when the image or compose file defines a HEALTHCHECK. In the Compose specification the healthcheck block takes test, interval, timeout, retries, and start_period, plus start_interval for faster polling during startup.
services:
app:
image: example/app
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
The daemon then tracks a status you can read. Values are starting, healthy, and unhealthy, and State.Health only exists when a healthcheck is defined at all:
docker inspect --format '{{.State.Health.Status}}' app
# healthy
docker inspect --format '{{.State.Running}}' app
# true
Now turn that local truth into an external signal. A cron job pings a heartbeat URL only when the container reports healthy:
*/2 * * * * [ "$(docker inspect -f '{{.State.Health.Status}}' app)" = healthy ] \
&& curl -fsS -m 10 https://undownable.com/ping/your-monitor-id > /dev/null
With no healthcheck defined, fall back to the running state, which still catches the crash loop because a container mid restart is not running when cron looks:
*/2 * * * * [ "$(docker inspect -f '{{.State.Running}}' app)" = true ] \
&& curl -fsS -m 10 https://undownable.com/ping/your-monitor-id > /dev/null
Or probe the service the way a user would, through the published port, which is the most honest test because it exercises the port mapping too:
*/2 * * * * curl -fsS -m 10 http://127.0.0.1:8080/health > /dev/null \
&& curl -fsS -m 10 https://undownable.com/ping/your-monitor-id > /dev/null
One heartbeat per container, or one per stack
This is a question of how specific you want the alert to be.
| Approach | Monitors used | Alert says |
|---|---|---|
| One per critical container | One each | Exactly which service is down |
| One per compose stack | One per stack | Something in the media stack is down |
| One for the host | One | The whole box is gone |
A pragmatic middle ground for a busy host is a single loop that names its containers and only reports in when all of them are up, from one cron line and one monitor:
*/2 * * * * for c in jellyfin sonarr radarr qbittorrent; do \
[ "$(docker inspect -f '{{.State.Running}}' $c 2>/dev/null)" = true ] || exit 1; \
done; curl -fsS -m 10 https://undownable.com/ping/your-monitor-id > /dev/null
Name the containers rather than enumerating everything running, otherwise a container you deliberately stopped pages you at midnight. Keep a separate ungated heartbeat for the host, so you can tell one dead container from a dead machine.
For containers that are published anyway
If a container serves a public hostname through a reverse proxy or tunnel, monitor it as a website rather than as a container. One keyword check against the real hostname tests the container, the port mapping, the proxy, the certificate, and the DNS record at once.
Use a keyword assertion so a proxy error page cannot pass as success, and add an SSL check on the hostname. Custom request headers are stored encrypted, which covers containers behind Cloudflare Access service tokens or an API key.
Alerting that is not a container
Anything you self host for notifications is a container on a host that can fail. Route alerts from outside instead:
- ntfy, hosted or self hosted with a tk_ access token sent as a bearer token, stored encrypted. Put it somewhere other than the Docker host you are watching.
- Telegram, the least circular option available, needing nothing but a bot token.
- Email, plus a webhook if you want to feed something downstream.
Flap suppression matters more here than anywhere else, because containers legitimately restart. Combine it with a confirmation threshold and a maintenance window over your update schedule, and a nightly image pull stops generating incidents.
Can Undownable check my containers directly?
Not by talking to Docker, deliberately. Docker documents that a TCP bound daemon is unauthenticated by default and that exposing it risks root access to the host. Read health locally with docker inspect and push a heartbeat outward instead.
What if a container has no HEALTHCHECK?
Use docker inspect -f {{.State.Running}} as the gate, or better, curl the published port the way a user would. State.Health only exists when a healthcheck is defined, so a script assuming it gets an empty string and a confusing failure.
How do I catch a container that keeps restarting?
A gated heartbeat catches it naturally: during a restart the container is not running, so the check fails and the ping is skipped. Set the expected interval to about two and a half times your cron period.
Should I have one monitor per container?
For the handful you care about, yes, because the alert then names the service. For the long tail, one monitor gated on a list of container names is enough, plus one ungated heartbeat for the host.
Related reading
Monitoring that watches from the outside
Free plan with 10 monitors, plus a 14-day Pro trial. No credit card required.
Start free