Docker Healthcheck Failing? Here's How to Get Alerted

Docker's HEALTHCHECK instruction can mark a container unhealthy and, paired with a restart policy, restart it. It does not notify a human. To get alerted when a healthcheck fails, something outside the container has to watch for that status change and push it to a notification channel: either a heartbeat monitor that expects a periodic ping, or a direct alert call fired from the failing check itself. Neither one ships with Docker. Both take a small shell script to wire up.

What Happens When a Docker Healthcheck Fails?

A healthcheck is a command Docker runs on a schedule inside the container, defined by the HEALTHCHECK instruction in the Dockerfile or the healthcheck: block in Compose. If the command exits non-zero for more tries than the configured retries count, Docker flips the container's status to unhealthy. That status shows up in docker ps and docker inspect, and an orchestrator watching it (Swarm, or a restart policy) can act on it.

What Docker does not do is tell anyone. The status change sits in docker inspect output until something reads it. If nobody is running that query, an unhealthy container can sit that way for hours, quietly failing requests or being restarted in a loop, before a human notices.

Why Doesn't Docker Alert You When a Container Goes Unhealthy?

Docker's healthcheck mechanism was built as a signal for orchestrators, not for people. Kubernetes, Swarm, and ECS all consume container health to decide whether to route traffic or replace a task; none of them assume you also want a text or a push notification, because that decision is specific to your team, your hours, and which failures actually deserve a wake-up. Docker leaves that layer to you on purpose, the same way it leaves you to pick a logging backend instead of shipping one.

That gap is exactly where Docker's own HEALTHCHECK documentation stops: it defines the instruction and the exit-code contract, and says nothing about notification, because notification was never in scope.

How Do You Send a Push Alert From a Failing Healthcheck?

The simplest reliable pattern is a heartbeat monitor: a dead man's switch that expects a ping on a schedule and alerts you the moment a ping doesn't arrive. You wire it in by wrapping your existing healthcheck command in a script that pings the monitor only when the check passes.

#!/bin/sh
set -e

# Run the real check first. If it fails, exit 1 and skip the ping.
curl -fsS http://localhost:8080/health

# Only reached on success.
curl -fsS "$PINGWIRE_HEARTBEAT_URL" >/dev/null 2>&1 || true

Point Docker's HEALTHCHECK at the wrapper instead of the raw check:

healthcheck:
  test: ["CMD", "/usr/local/bin/healthcheck-and-ping.sh"]
  interval: 30s
  timeout: 5s
  retries: 3

When the real check fails, the script exits before reaching the ping line, the monitor's expected-ping window lapses, and it fires an alert to whatever channel you attached. When the container is killed, OOM-killed, or the host goes down, the same thing happens for the same reason: no ping arrives.

Setting this up end to end:

  1. Create a heartbeat monitor and copy its ping URL.
  2. Wrap your container's health check command in a script that pings that URL only after a successful check.
  3. Point Docker's HEALTHCHECK at the wrapper script instead of the raw check.
  4. Set the monitor's expected interval a little above your healthcheck interval, so one slow check doesn't false-alarm.
  5. Attach a channel as the monitor's alert target.
  6. Break the health endpoint on purpose and confirm the alert arrives before you rely on it.

Firing an Alert the Instant a Check Fails, Instead of Waiting for Silence

A heartbeat monitor tells you something stopped reporting. If you want the alert to carry the actual failure — which endpoint, which status code — call an alerting API directly on the failure branch of the script instead of, or alongside, the heartbeat ping. Pingwire's REST API accepts a message with a bearer key from any script that can run curl; the developer docs cover the request shape and authentication.

Watching Every Container on a Host From One Script

Wrapping each container's healthcheck works well when you own a handful of Dockerfiles. On a host running many containers you don't want to edit, a single watcher process reading Docker's event stream covers all of them at once:

#!/bin/sh
# Watches every container on this host and alerts on each unhealthy transition.
docker events --filter event=health_status --format '{{json .}}' | while read -r event; do
  status=$(echo "$event" | grep -o 'unhealthy')
  if [ "$status" = "unhealthy" ]; then
    name=$(echo "$event" | grep -o '\"name\":\"[^\"]*\"' | cut -d'\"' -f4)
    curl -fsS -H "Authorization: Bearer $PINGWIRE_API_KEY" \
      -d "text=Container $name went unhealthy" \
      https://pingwire.dev/api/v1/messages
  fi
done

Run this once per host, outside any container it's watching (a container that dies can't alert on its own death). It never touches the containers themselves, so it survives a bad deploy of any one of them, and it keeps working even for containers you add later without touching their Dockerfiles.

Should You Use a Heartbeat Monitor or a Webhook Call?

These solve two different failure shapes, and most setups end up using both.

ApproachFires whenSetup effortBest for
Heartbeat monitorThe container or host goes silent entirely — crash, OOM-kill, host downOne dashboard step, one curl call in the scriptCatching total silence, including failures that never run your alert code at all
Direct alert on failureThe script explicitly detects an unhealthy resultAn extra API call on the failure branchGetting the failure detail (endpoint, status code) attached to the alert immediately

A heartbeat monitor is the one that still works when everything else has stopped running, since it depends on the absence of a signal, not on your own alerting code executing successfully. A direct call on failure is the one that tells you what actually broke. Running the heartbeat ping as the baseline and adding a direct alert for detail is the combination that covers the most ground for the least code.

How Do You Test the Alert Before You Need It?

An alert you have never seen fire is not a working alert, it's a guess. Before trusting this in production:

  • Stop the health endpoint on purpose (kill the process behind it, or return a 500 deliberately) and time how long it takes the alert to arrive.
  • docker kill the container outright and confirm the heartbeat monitor still catches it, since the wrapper script never gets a chance to run.
  • Check the alert lands somewhere you'll actually see it outside working hours, not only a channel you check once a day.

Run all three at least once. The failure mode where the alert itself is silently broken is the one that costs the most, because you find out during the next real incident instead of during a test.

What About Restart Policies — Isn't That Already Enough?

The most common reason to skip this setup: "my restart policy already recovers the container, so why alert on it?" A restart policy fixes the container; it does not tell you the restart happened, how many times, or whether it's now looping every thirty seconds while still serving errors on every third request. A crash loop that keeps restarting still degrades the service the entire time it runs, and a restart policy has no concept of a human who should know about that.

The alert and the restart policy answer different questions: one keeps the container up, the other keeps you informed. You want both, and neither one substitutes for the other.

Set Up the Alert Now, Not During the Next Incident

Create a heartbeat monitor, paste the ping curl into your existing healthcheck script, and break the health endpoint once to watch the alert arrive. That's the whole setup, and it takes about the same amount of time as reading this article. Create a free Pingwire account to get a monitor running before the next unhealthy container is the one you find out about from a customer.

Related reading: cron-job dead man's switches use the same missed-ping pattern for scheduled jobs instead of long-running containers, and systemd service failure alerts cover the equivalent setup for services that aren't containerized at all. If your healthcheck failures come from a CI pipeline rather than a running container, see how to get notified when a GitHub Actions workflow fails.

Frequently asked questions

Does Docker have a built-in way to alert on healthcheck failures?

No. Docker's HEALTHCHECK instruction only changes the container's reported status to unhealthy and, combined with a restart policy, can trigger a restart. It does not send an email, a push notification, or a webhook by itself. You have to watch the status change and forward it to something that alerts a human.

What's the difference between an unhealthy container and a stopped one, for alerting purposes?

An unhealthy container is still running and can still report its own status. A stopped, killed, or OOM-killed container may vanish along with the process that would have reported it. A heartbeat monitor catches both cases, because it alerts on the absence of an expected ping rather than on a specific failure message from the container itself.

Can I alert straight from docker events instead of wrapping the healthcheck command?

Yes. Running docker events --filter event=health_status in a small watcher process gives you every health transition on the host, and you can forward each unhealthy event to a notification API. It takes more code than a wrapper script, but it covers every container on that host from one place instead of one script per container.

Will a heartbeat monitor tell me if the whole server goes down, not just one container?

A heartbeat monitor pinged from inside that same host goes silent right along with the host, so the missed-ping alert still fires. What it cannot tell you is whether the cause was one container or the entire box. Pair it with a monitor that checks the host from outside if you need to tell those two apart.

Do I need to change my container's restart policy to start using this?

No, your restart policy and this kind of alerting are independent and run side by side. The restart policy is what fixes the container. The alert is what tells you the fix happened, or didn't, so you find out from your own monitoring instead of from a customer report.

Try Pingwire

Send your first alert in under 30 seconds — one HTTP call, straight to a chat and your phone.

Create a free account Read the API docs

More from the blog