Guide

How to Get an Alert When a Docker Container Stops (4 Tested Methods)

A docker events watcher piped to Telegram or ntfy, restart policies, healthchecks, and a cron backstop. Every command was run on Docker 29.2.1.

PulseNode · updated July 2026

You want an alert when a Docker container stops, and Docker will not send you one. There is no built-in notification of any kind. What the daemon does have is an event stream, docker events, that reports every container death in real time with the exit code attached. This guide builds on that stream: an event watcher piped to Telegram or ntfy, restart policies, a healthcheck pattern, and a cron backstop. Every command was tested on Docker 29.2.1.

Why Docker doesn't tell you when a container dies

The daemon knows the moment a container dies. It emits die, stop, kill, and oom events (the docker events reference lists them all), and it can restart containers by policy. It just never tells a human, so the notification layer is yours to build. It takes about twenty lines of bash. The methods below cover different failures, and they stack.

MethodCatchesEffort
docker events watcherCrashes and kills, in real time, with exit codesOne script plus a systemd unit
Restart policyNothing (it repairs instead of reporting)One flag per container
HEALTHCHECK + autohealContainers that run but no longer workA few lines per image
Cron docker ps checkAnything not running, plus a dead Docker daemonOne script plus a cron line

Step 0: set a restart policy before you build alerts

Most container deaths should not page you; they should get fixed automatically. Docker has four restart policies, and the default, no, is the worst one for a server: a crashed container stays down until you notice.

The four restart policies compared

PolicyBehavior
noNever restart. The default.
on-failure[:max-retries]Restart only on a non-zero exit code, optionally capped at N attempts. Good for batch jobs.
alwaysRestart on any exit, even a clean one. After a daemon restart it comes back even if you had stopped it by hand.
unless-stoppedRestart on any exit unless the container was manually stopped. The sane default for services.

Apply it to containers that are already running

No recreate needed; docker update changes the policy on a live container:

# one container
docker update --restart unless-stopped myapp

# every currently running container at once
docker update --restart unless-stopped $(docker ps -q)

I verified the change sticks with docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' myapp. Two caveats from the docs: a policy only takes effect after the container has run successfully for at least 10 seconds, and a manual docker stop overrides it until the daemon restarts. Skip restart policies for containers already managed by systemd on the host; the two will fight over the same corpse.

Why a restart policy is not an alert

A restart policy hides problems as much as it fixes them. A crash-looping container restarts silently forever and even looks healthy in docker ps between deaths. The 10-second rule bites too: a container that crashes instantly on boot may never trigger the policy and just stays down. Restart policies cut the noise; the watcher below exists because you still want to know when they are doing work.

Get an alert when a Docker container stops with docker events

The one-liner: watch die events

The die event fires on every container termination, crash or not, and carries the exit code in its attributes. This prints one line per death:

docker events \
  --filter 'type=container' --filter 'event=die' \
  --format '{{.Actor.Attributes.name}} exited with code {{.Actor.Attributes.exitCode}}'

# output when a container crashes:
# myapp exited with code 7

One gotcha when copying older tutorials: many use {{.Status}} in the format string. That field is gone; on Docker 29.2.1 it errors with "can't evaluate field Status in type *events.Message". Use {{.Action}} for the event name, and --format 'json={{json .}}' to see the full payload.

Pipe it to Telegram or ntfy

The watcher becomes useful when each line goes somewhere that buzzes your phone. Telegram's sendMessage needs a bot token from @BotFather and your chat ID:

#!/usr/bin/env bash
# watch-docker-die.sh: notify on every unexpected container death.
HOST=$(hostname)
TG_TOKEN="123456:ABC-your-bot-token"   # from @BotFather
TG_CHAT="123456789"                    # your chat ID

docker events \
  --filter 'type=container' --filter 'event=die' \
  --format '{{.Actor.Attributes.name}} {{.Actor.Attributes.exitCode}}' \
| while read -r name code; do
    # skip clean exits (0) and graceful stops (143 = SIGTERM)
    case "$code" in 0|143) continue ;; esac
    curl -fsS -G "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
      --data-urlencode "chat_id=${TG_CHAT}" \
      --data-urlencode "text=container ${name} died with exit code ${code} on ${HOST}" \
      >/dev/null
  done

Tested end to end: crashing a test container delivered "container crashtest died with exit code 1" within a second. If you would rather skip the bot setup, ntfy.sh needs no signup. Swap the curl for:

curl -fsS -d "container ${name} died with exit code ${code} on ${HOST}" \
  ntfy.sh/your-unguessable-topic >/dev/null

and subscribe to the topic in the ntfy app. The topic name is effectively the password, so make it long and random.

Don't page yourself on every deploy

die fires on intentional terminations too. Deploy with compose and every replaced container dies with a clean code, so a naive watcher pings you on each release. The exit code sorts it out: 0 is a clean exit, 143 is SIGTERM (a graceful docker stop), 137 is SIGKILL. The script above skips 0 and 143. Careful with 137: it means SIGKILL, not out of memory. You get it from docker rm -f, from an expired stop grace period, or from the OOM killer, and real OOM kills also emit their own oom event.

Keep the watcher alive

Run from a shell, the watcher dies with your SSH session. Give it a systemd unit:

# /etc/systemd/system/docker-die-watch.service
[Unit]
Description=Alert when a Docker container dies
After=docker.service
Requires=docker.service

[Service]
ExecStart=/usr/local/bin/watch-docker-die.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now docker-die-watch.service

You can also replay missed events after downtime, since --since and --until accept durations. This prints every death from the last 30 minutes and exits:

docker events --since 30m --until 1s \
  --filter 'type=container' --filter 'event=die' \
  --format '{{.Actor.Attributes.name}} exited with code {{.Actor.Attributes.exitCode}}'

One gap remains: this watcher runs on the machine it watches. If the whole box goes down, nothing is left to send the message. That case needs a heartbeat checked from outside: how to get notified when a server goes offline.

Catch "running but dead": HEALTHCHECK plus autoheal

A container can sit in docker ps as Up for weeks while the process inside stopped answering. No die event will ever fire for that. Docker's answer is the healthcheck: a command run inside the container on a schedule, whose exit code marks it healthy or unhealthy.

Add a healthcheck

In a Dockerfile (full options in the HEALTHCHECK reference):

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Defaults if you omit the flags: interval 30s, timeout 30s, start period 0s, retries 3. The command must exit 0 for healthy or 1 for unhealthy (2 is reserved). Only the last HEALTHCHECK in a Dockerfile counts, and HEALTHCHECK NONE disables an inherited one. The compose equivalent:

services:
  app:
    image: myapp:latest
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

Version notes: the --start-interval flag needs Docker Engine 25.0 or newer, and the compose start_interval key needs Compose 2.20.2 or newer.

Alert on health_status events

When a check flips a container to unhealthy, the daemon emits a health_status event. Same watcher, different filter:

docker events --filter 'type=container' --filter 'event=health_status' \
  --format '{{.Action}} {{.Actor.Attributes.name}}'

# prints: health_status: unhealthy myapp

Restart unhealthy containers automatically with autoheal

Docker never restarts an unhealthy container on its own; unhealthy is a status, not an action. willfarrell/docker-autoheal (MIT licensed, image rebuilt daily) fills that gap:

docker run -d --name autoheal --restart unless-stopped \
  -e AUTOHEAL_CONTAINER_LABEL=all \
  -v /var/run/docker.sock:/var/run/docker.sock \
  willfarrell/autoheal

AUTOHEAL_CONTAINER_LABEL=all covers every container with a healthcheck; drop it and label containers with autoheal=true to opt in per container. One thing autoheal does not do is notify. It restarts and moves on, so pair it with the health_status watcher if you want to know it happened.

The cron safety net: expected versus running

The event watcher only reports what happens while it is running. If the watcher crashed, or the box rebooted and half the stack never came up, silence looks like good news. A cron poll closes that hole by comparing what you expect against what is running:

#!/usr/bin/env bash
# check-containers.sh: alert if any expected container is not running.
# Crontab: */5 * * * * /usr/local/bin/check-containers.sh
set -u
EXPECTED="nginx redis myapp"          # space-separated container names
NTFY_TOPIC="your-unguessable-topic"
HOST=$(hostname)

if ! running=$(docker ps --format '{{.Names}}' 2>/dev/null); then
  curl -fsS -d "ALERT on ${HOST}: docker ps failed, daemon may be down" \
    "ntfy.sh/${NTFY_TOPIC}" >/dev/null
  exit 1
fi

missing=""
for c in $EXPECTED; do
  printf '%s\n' "$running" | grep -qx -- "$c" || missing="$missing $c"
done

if [ -n "$missing" ]; then
  curl -fsS -d "ALERT on ${HOST}: containers not running:${missing}" \
    "ntfy.sh/${NTFY_TOPIC}" >/dev/null
  exit 1
fi

Tested with three expected names and only redis running: it sent "containers not running: nginx myapp" and exited 1. The first branch matters too, because if docker ps itself fails, the daemon is down and that deserves its own alert. (docker ps -a --filter status=exited --format '{{.Names}}: {{.Status}}' is the manual version; it shows what died and when.) This is the same cron-plus-notifier pattern as our disk-full alert script, with the same weakness: the box sending the alert is the box having the problem.

Where PulseNode fits (and where it doesn't)

PulseNode is my product, so weigh this section accordingly. The first thing to say is what it does not do: per-container stop alerts are not a PulseNode feature today. For a Telegram ping the second a specific container dies, the watcher above is the right tool.

What PulseNode does with Docker is visibility. The agent reports container status, image, and uptime from every server to one dashboard, so "is everything running?" becomes a glance instead of SSH-ing into five boxes to run docker ps.

Alerts are server-level. You connect a channel (email, or Telegram in settings, with no bot server to host), then write threshold rules like cpu > 85% or disk > 90%, with per-rule cooldowns and recovery notices. One alert needs no rule at all: if an agent stops pushing, you get an agent-offline alert. That built-in dead man's switch covers the case the local watcher structurally cannot, the box going dark with your watcher on it.

PulseNode is a young product run by one founder, hosted only, not open source. Hobby is €5/mo for up to 5 servers with 7 days of history; Pro is €15/mo for 20 servers and 30 days. If one VPS and the scripts above cover you, keep the scripts. PulseNode earns its fee when you have several servers and want the fleet view plus alerts that do not depend on the failing machine.

See every container on every server at a glance

One outbound-only agent per box. Container status across your fleet, plus CPU, RAM, and disk alerts by email and Telegram.

Start with Hobby for €5/mo → Pro: 20 servers, €15/mo

Which method should you actually run?

All four are small enough to run together, and each covers a failure the others miss. Put restart: unless-stopped on every long-running container. Run the die-event watcher under systemd with Telegram or ntfy; that is your page for real crashes. Add the cron check as a backstop, since it survives watcher crashes and catches a dead daemon. Give anything with an HTTP port a HEALTHCHECK, plus autoheal if you want unhealthy containers bounced automatically. PulseNode covers the remaining layer: the whole fleet's containers on one screen, and server-level alerts.

FAQ: alerting when Docker containers stop

How do I get an alert when a Docker container stops?

Run docker events --filter 'type=container' --filter 'event=die' and pipe the output to a notifier such as a Telegram bot or ntfy.sh. The stream prints one line per container death with the name and exit code attached. Wrap the watcher in a systemd service so it survives reboots, and skip exit codes 0 and 143 so deploys stay quiet.

Does Docker have built-in notifications when a container dies or is stopped?

No. The daemon records die, stop, and kill events and applies restart policies, but it never sends an email, a message, or a webhook. Any notification has to come from something that reads the event stream or polls docker ps, which is what the scripts in this guide do.

How do I send a Telegram notification when a Docker container stops?

Create a bot with @BotFather and copy your chat ID. Then have the die-event watcher call the Telegram sendMessage endpoint, which needs only two parameters: chat_id and text. One curl per event is enough; the full tested script is in the guide above.

What is the difference between the die, stop, and kill events in docker events?

die fires on every container termination, whatever the cause, and carries the exit code. stop fires when something calls docker stop, and kill fires when a signal is sent. For alerting, watch die: it fires on crashes, kills, and manual stops alike. Out-of-memory kills also emit a separate oom event.

If I use restart: unless-stopped, do I still need an alert?

Yes. A restart policy hides failures rather than reporting them: a crash-looping container restarts silently forever, and a container that dies within its first 10 seconds may never come back, because a policy only takes effect after 10 seconds of successful running. Use unless-stopped to fix the easy cases and a die-event watcher to know they are happening.

Can PulseNode notify me when a specific Docker container stops?

No. PulseNode shows container status, image, and uptime for every server on one dashboard, so you can see what is running at a glance, but per-container stop alerts are not a PulseNode feature today. Its alerts cover CPU, RAM, and disk thresholds plus an agent-offline check. For per-container notifications, use the docker events watcher from this guide.

See also