The *arr stack is a supply chain. Jellyseerr takes a request, Sonarr or Radarr looks for it through Prowlarr's indexers, hands the result to a download client that lives behind a VPN, imports the file into a root folder on a NAS, and Plex or Jellyfin picks it up. Six services, five hand-offs, and every one of them can break while every container shows "running" and every web UI loads. The symptom is nothing: nothing new arrives for a week, and the first report is someone asking why the show they requested hasn't shown up.
The useful thing about the *arr apps is that they know when they're broken. Each one runs its own health checks and shows the results under System → Status, and each exposes those results through an API. This guide puts a monitor on each link using those endpoints, adds a heartbeat for the two things no API covers (the VPN around the download client and the root-folder mount), and shows how to read the chain from the pattern of alerts.
Each *arr app: /ping, then /health
Every *arr application (Sonarr, Radarr, Lidarr, Readarr, Prowlarr) exposes two endpoints worth monitoring. The first is /ping, which answers {"status":"OK"} with no authentication, even when the UI itself requires a login. An HTTP monitor on https://sonarr.example.com/ping (include your URL base, such as /sonarr/ping, if you use one) expecting 200 proves the app is up and reachable through your reverse proxy. Free plan, three-minute checks, certificate tracked.
The second is the health endpoint, which returns the app's own list of problems: /api/v3/health on Sonarr and Radarr, /api/v1/health on Prowlarr, Lidarr, and Readarr. It requires the app's API key (Settings → General) in an X-Api-Key header, which you add as a custom header on the monitor; headers are on every plan. When everything is fine it returns an empty array, []. When something isn't, it returns entries like "source":"IndexerStatusCheck", "source":"DownloadClientCheck", "source":"RootFolderCheck", or "source":"ImportMechanismCheck", each with a type of warning or error and a message. The endpoint still returns 200 either way, so the status code alone can't tell you; on Pro, add a keyword check in does not contain mode for "type":"error", and the monitor goes down exactly when the app would show a red banner. If you also want warnings (a single indexer flaking), use a second monitor on "type":"warning", or accept that warnings are what System → Status is for.
Create both monitors for each app you run. The /ping one tells you the app is reachable; the health one tells you it can do its job. Their disagreement is diagnostic, as the table below shows.
Prowlarr: the indexers everyone depends on
Prowlarr is the link that breaks most often and matters most, because every search in every other app goes through it. Its health endpoint reports indexers that have failed too many times and been temporarily disabled (IndexerStatusCheck), an indexer whose API key or captcha broke, and applications it can no longer sync to. The error-keyword monitor on /api/v1/health catches all three. Prowlarr also exposes /api/v1/indexerstatus (same header) listing disabled indexers with the time they'll be retried; a script that fails a heartbeat when more than N indexers are disabled is a finer-grained alternative if you run many.
The download client behind the VPN
The download client (qBittorrent, Deluge, SABnzbd) usually runs inside or alongside a VPN container such as Gluetun, with a kill switch so that when the tunnel drops, nothing leaks. That's correct behavior, and it also means a dropped tunnel looks like a client that stopped downloading, indefinitely, with no error anywhere the *arr apps can see beyond "download client unreachable" or stalled downloads. No external check can reach it either, which is the point. So this link gets a heartbeat from the host:
#!/usr/bin/env bash
# /usr/local/bin/arr-health.sh — run from cron every minute on the Docker host
set -uo pipefail
HEARTBEAT="https://cronalert.com/api/heartbeat/<token>"
VPN_CONTAINER=gluetun # the container the download client's network goes through
GLUETUN_CONTROL="http://127.0.0.1:8000" # Gluetun control server, if published
MEDIA_ROOT=/mnt/media # root folder shared with Sonarr/Radarr
fail=0
# 1. VPN container healthy and the tunnel's public IP is not your real one
[ "$(docker inspect -f '{{.State.Health.Status}}' "$VPN_CONTAINER" 2>/dev/null)" = healthy ] || { echo "$VPN_CONTAINER not healthy"; fail=1; }
real_ip=$(curl -fsS -m 5 https://ifconfig.me 2>/dev/null || true)
vpn_ip=$(curl -fsS -m 5 "$GLUETUN_CONTROL/v1/publicip/ip" 2>/dev/null | sed -E 's/.*"public_ip":"([^"]+)".*/\1/' || true)
[ -z "$vpn_ip" ] && vpn_ip=$(docker exec "$VPN_CONTAINER" wget -qO- -T 5 https://ifconfig.me 2>/dev/null || true)
[ -n "$vpn_ip" ] && [ "$vpn_ip" != "$real_ip" ] || { echo "vpn public ip '$vpn_ip' (real: $real_ip)"; fail=1; }
# 2. The download client's web API answers through the VPN container's network
docker exec "$VPN_CONTAINER" wget -qO- -T 5 http://127.0.0.1:8080/api/v2/app/version >/dev/null 2>&1 || { echo "download client api not answering"; fail=1; }
# 3. The root folder is a real mount with room
mountpoint -q "$MEDIA_ROOT" || { echo "$MEDIA_ROOT not mounted"; fail=1; }
use=$(df --output=pcent "$MEDIA_ROOT" 2>/dev/null | tail -1 | tr -dc '0-9')
[ -n "${use:-}" ] && [ "$use" -ge 95 ] && { echo "$MEDIA_ROOT at ${use}%"; fail=1; }
[ "$fail" -eq 0 ] && curl -fsS -m 10 -X POST "$HEARTBEAT" >/dev/null
exit 0 The IP comparison is the heart of it: if the public address seen from inside the VPN container equals the address seen from the host, the tunnel is down and the kill switch is (hopefully) the only thing between the client and your ISP. The download-client line assumes qBittorrent on 8080 with localhost auth bypass enabled; adjust for your client. Create the heartbeat with a 1-minute expected interval (alerts after two minutes with the default grace), and journalctl -t arr-health names the failing line. Heartbeats are on the Pro plan.
Jellyseerr: the part people actually touch
Jellyseerr (or Overseerr) is the one service in the stack with users, and the one most often published through a reverse proxy or Cloudflare Tunnel. It exposes /api/v1/status without authentication, returning the version and whether an update is available; an HTTP monitor on https://requests.example.com/api/v1/status expecting 200 covers reachability, the proxy, and the certificate, on the free plan. The failure Jellyseerr can't report is the one downstream of it: it accepted a request and forwarded it to a Sonarr or Radarr it can no longer reach, so the request sits at "requested" forever. That's why the health monitors on Sonarr and Radarr matter even if nobody opens their UIs; name all three so the pattern reads at a glance.
Reading the chain
| Jellyseerr status | *arr /ping | *arr /health keyword | Host heartbeat | Most likely |
|---|---|---|---|---|
| Down | Down | Down | Silent | The host or the network. Everything behind it is gone. |
| Up | Up | Failing | Fine | The app can't do its job: check its System → Status. Usually indexers (Prowlarr) or the download client. |
| Up | Up | Fine | Silent | The VPN dropped or the root folder unmounted, before the apps have noticed. |
| Up | One down | — | Fine | That container. Jellyseerr requests to it are stuck at "requested". |
| Down | Up | Fine | Fine | Jellyseerr or its public path: the proxy, tunnel, or certificate. Automation is fine; people can't request. |
Alerts and quiet hours
This is a household stack, so send alerts where you'll see them and the household won't: Discord is free and most request communities already have one, or ntfy if you run it. Put a maintenance window over your update slot, and if you let Watchtower or a scheduled pull recreate containers nightly, over that too, because a recreate is thirty seconds of 502 that doesn't need to wake anyone. A status page with the Jellyseerr monitor on it answers "is requests down?" without a message.
Frequently asked questions
Which URL do I monitor on Sonarr or Radarr?
/ping for reachability (no auth), and /api/v3/health with an X-Api-Key header for the app's own problem list.
How do I catch the VPN dropping?
A heartbeat script comparing the public IP inside the VPN container to the host's real one, and withholding the ping when they match.
Does Jellyseerr being up mean requests work?
No. It forwards to Sonarr and Radarr; monitor those too, or requests sit at "requested" silently.
Which plan?
The HTTP checks, including the header-authenticated health endpoint, are free. The error keyword and heartbeats are Pro ($5/mo).
What about Bazarr, Lidarr, Readarr?
Same pattern: /ping plus the health endpoint (/api/v1/health on Lidarr and Readarr; Bazarr's API is under /api/system/status with the same header style).
Monitor the chain, not the containers
"Running" is the least informative word in Docker. The *arr apps already know when their indexers are dead, their download client is unreachable, and their root folder is gone; two monitors per app read that knowledge from outside, and one script on the host covers the VPN and the mount they can't see. The next broken link gets fixed before the request queue notices. Create a free account, add the ping and health checks, and upgrade to Pro for the keyword and the heartbeat. Related reading: monitoring a Plex or Jellyfin server at the end of the chain, monitoring the NAS the root folder lives on, monitoring a VPN, monitoring Docker and self-hosted apps, and choosing between HTTP, TCP, and heartbeat monitors.