A self-hosted watchdog for qBittorrent that detects stalled torrents, monitors DHT health, and recovers your client automatically — with a clean dark-themed dashboard for real-time monitoring and zero-restart configuration.
qBittorrent occasionally gets itself wedged: torrents stall in stalledDL/stalledUP, DHT drops to zero, and downloads quietly grind to a halt. Watcharr watches for those exact conditions and takes graduated recovery action automatically — reannounce → pause/resume → container restart — so you stop noticing days later that nothing has been moving.
It's a single container, talks to qBittorrent over its Web API, and ships with an embedded dashboard so you don't need a separate Grafana panel just to know it's working.
- Stall Detection — Continuously monitors qBittorrent for torrents stuck in
stalledDL,stalledUP, ormetaDLstates - Automatic Recovery — Attempts reannounce + pause/resume cycles on stalled torrents before escalating
- DHT Health Monitoring — Tracks DHT node count to detect network connectivity issues; zero DHT nodes + stalled torrents triggers a container restart
- Container Restart — Automatically restarts your qBittorrent Docker container via the Docker socket when recovery is needed
- Stall Timeout & Removal — Optionally removes torrents that remain stalled beyond a configurable timeout (disabled by default)
- Web Dashboard — Real-time dark-themed UI with status cards, event timeline, torrent table, live log viewer, and settings editor
- Live Log Tailing — Byte-offset log streaming in the browser with automatic scroll-pinning
- Hot-Reloadable Settings — Change any setting from the web UI without restarting the container; the watchdog picks up changes on the next check cycle
- Layered Configuration — Settings priority: Web UI (JSON file) > environment variables > built-in defaults
- Optional Basic Auth — Protect the dashboard with username/password when exposed beyond your local network
- Health Check — Built-in
/api/healthendpoint with Docker HEALTHCHECK for monitoring - Responsive Design — Dashboard works on desktop and mobile
Live status, recent watchdog events, and the active torrent list — all in one view. The header pills surface DHT count and stalled count at a glance.
Byte-offset log tailing with rotation detection. New lines stream in as the watchdog runs; the view auto-pins to the bottom unless you scroll up.
Hot-reloadable configuration. Change qBittorrent credentials, check intervals, or stall handling — saves to a JSON file, picked up on the next watchdog cycle, no restart needed.
- Docker and Docker Compose
- A running qBittorrent instance with the Web UI enabled
git clone https://github.com/reedylab/watcharr.git
cd watcharrBoth .env and docker-compose.yml ship as .example files so you can keep your local edits without committing them. Copy them:
cp .env.example .env
cp docker-compose.yml.example docker-compose.ymlEdit .env with your actual values:
# qBittorrent connection
QB_URL=http://your-qbittorrent-ip:8080/
QB_USERNAME=admin
QB_PASSWORD=your-password
QB_CONTAINER=qbittorrent
# Watchdog settings
CHECK_INTERVAL=60
# Optional: protect the dashboard with basic auth
# AUTH_USERNAME=admin
# AUTH_PASSWORD=changemeFinding your qBittorrent details:
QB_URLis the same address you use to open the qBittorrent Web UI in your browser (the one with the login screen).QB_USERNAME/QB_PASSWORDare those Web UI credentials — not your system login.QB_CONTAINERmust match the container name shown bydocker psexactly, otherwise restarts won't work.
docker compose up -d
⚠️ Heads up — Docker socket access: Watcharr mounts/var/run/docker.sockso it can restart your qBittorrent container when recovery is needed. This is root-equivalent on the host — anything that can talk to the socket can run privileged containers. Only run images you trust, keep Watcharr on your trusted LAN, and consider tecnativa/docker-socket-proxy if you want to restrict it to justcontainers/restart.
Navigate to http://your-server-ip:5035 and click Start to begin monitoring.
Tested and confirmed working with:
- qBittorrent images:
linuxserver/qbittorrent,binhex/qbittorrentvpn,binhex/arch-qbittorrentvpn,lscr.io/linuxserver/qbittorrent, officialqbittorrentofficial/qbittorrent-nox - VPN routing:
qmcgaw/gluetun-routed qBittorrent containers (use Watcharr to restart the qBit container, not gluetun itself) - qBittorrent version: 4.5+ (anything with the v2 Web API)
- Hosts: Any Linux host with Docker + Compose; works fine on Synology/UnRAID/TrueNAS Scale where Docker is available
It will work with any qBittorrent setup whose Web API is reachable from the Watcharr container — the restart feature specifically requires qBit to be running as a Docker container on the same host, but stall detection / reannounce / pause-resume work over the network too.
Watcharr is a recovery layer, not a magic bullet. It will not fix:
- Slow downloads caused by VPN port-forwarding issues. If your VPN doesn't give you a forwarded port (notably Mullvad — they removed port forwarding in 2023), you're a passive peer and your usable peer pool shrinks dramatically. Watcharr can unstick wedged metadata, but it can't conjure peers that aren't connectable. Switch to ProtonVPN, AirVPN, or PIA if download speed and metadata reliability matter.
- Dead torrents. Reannounce + pause/resume can't recover a swarm that genuinely has zero working peers.
- qBittorrent on a different host. Restart needs Docker socket access on the same machine. Reannounce, pause/resume, and stall detection still work over the network — just not the container restart escalation step.
- VPN-level UDP throttling or DNS issues. If your VPN is dropping/blocking the UDP traffic DHT depends on, Watcharr will see DHT nodes drop and keep restarting qBit. Fix the VPN config, or raise
CHECK_INTERVALso you don't get into a restart loop. - Underlying qBittorrent bugs. If a specific qBit version corrupts state on every restart, Watcharr will faithfully reproduce the corruption. Pin to a known-good qBit image.
Watcharr uses a three-layer configuration system. Each layer overrides the one below it:
1. Web UI settings (saved to /app/data/settings.json) ← highest priority
2. Environment variables (.env file)
3. Built-in defaults ← lowest priority
This means you can set initial values via environment variables, then fine-tune from the web UI without restarting the container.
| Variable | Default | Description |
|---|---|---|
QB_URL |
http://localhost:8080/ |
qBittorrent Web UI URL |
QB_USERNAME |
admin |
qBittorrent username |
QB_PASSWORD |
(empty) | qBittorrent password |
QB_CONTAINER |
qbittorrent |
Docker container name to restart |
CHECK_INTERVAL |
60 |
Seconds between watchdog checks |
STALL_TIMEOUT_MIN |
5 |
Minutes before a stalled torrent is removed (when auto-removal is enabled) |
STALL_REMOVAL_ENABLED |
false |
Enable automatic removal of stalled torrents after timeout |
AUTH_USERNAME |
(empty) | Dashboard username (basic auth disabled when empty) |
AUTH_PASSWORD |
(empty) | Dashboard password (basic auth disabled when empty) |
LOG_FILE |
/app/logs/watcharr.log |
Path to the log file inside the container |
SETTINGS_FILE |
/app/data/settings.json |
Path to the persisted settings file |
services:
watcharr:
build:
context: .
container_name: watcharr
restart: unless-stopped
ports:
- "5035:5035"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- watcharr-data:/app/data
- watcharr-logs:/app/logs
env_file:
- .env
volumes:
watcharr-data:
watcharr-logs:| Mount | Purpose |
|---|---|
/var/run/docker.sock |
Required for Watcharr to restart the qBittorrent container |
watcharr-data |
Persists settings across container restarts |
watcharr-logs |
Persists log files |
Security note: Mounting the Docker socket grants root-equivalent access to the host. Watcharr only uses it to call
containers/<QB_CONTAINER>/restart, but the capability it has is unrestricted. If that's a concern, put a docker-socket-proxy in front and grant onlyCONTAINERS=1 POST=1.
Watcharr's dashboard has no authentication by default. If your instance is only accessible on a trusted local network, this is fine. If you're exposing it beyond your LAN:
- Set basic auth via
AUTH_USERNAMEandAUTH_PASSWORDenvironment variables, or - Use a reverse proxy (Nginx, Caddy, Traefik) with its own authentication layer
The /api/health endpoint is always accessible (bypasses auth) so Docker health checks continue to work.
Every CHECK_INTERVAL seconds, the watchdog runs through this decision loop:
1. Fetch torrent list from qBittorrent API
2. Fetch transfer info (DHT node count)
3. Identify stalled torrents (stalledDL, stalledUP states)
4. Identify metadata-stuck torrents (metaDL state + 0 DHT nodes)
5. For each stalled/stuck torrent:
a. Send reannounce request
b. Pause then resume the torrent
6. If (stalled OR metadata-stuck) AND DHT nodes == 0:
→ Restart qBittorrent container
→ Wait 30 seconds before next check
7. If auto-removal is enabled:
→ Track how long each torrent has been stalled
→ Remove torrents exceeding the stall timeout
All endpoints are under /api. Responses are JSON.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Returns {"status": "ok"} — always accessible, bypasses auth |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/status |
Watchdog status: running state, DHT nodes, stalled count, uptime, restart count, check count |
POST |
/api/start |
Start the watchdog |
POST |
/api/stop |
Stop the watchdog |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/torrents |
Current torrent list (proxied from qBittorrent) |
GET |
/api/events |
Last 200 watchdog events (stalls, restarts, removals, recoveries) |
GET |
/api/logs/tail?pos=0&inode= |
Incremental log tail using byte offsets; supports log rotation detection via inode tracking |
| Method | Endpoint | Body | Description |
|---|---|---|---|
POST |
/api/restart-qbit |
— | Manually restart the qBittorrent container |
POST |
/api/reannounce |
{"hash": "..."} |
Manually reannounce a specific torrent |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/settings |
Returns the settings schema (field types, labels, options) and current values |
POST |
/api/settings |
Save settings. Body is a JSON object of key-value pairs. Only known keys are accepted. |
Q: I'm using Mullvad + gluetun and metaDL still stalls even with Watcharr running. Why? A: Mullvad removed port forwarding from all servers in 2023, so your qBit is passive — it can only initiate outbound connections to peers who themselves have open ports. On lightly seeded torrents that pool can be effectively zero, and no amount of restarting will materialize peers. Watcharr fixes wedged DHT and stuck-after-reconnect states; it cannot fix "no connectable peers." Long-term answer: switch to a VPN that still does port forwarding (ProtonVPN, AirVPN, PIA), then point gluetun at it and forward the port to qBit's listen port.
Q: Will it work with linuxserver/qbittorrent / binhex/qbittorrentvpn / a gluetun-routed qBit?
A: Yes. Watcharr only needs the qBit Web API to be reachable from its container, plus the Docker socket on the host so it can issue a restart. Set QB_CONTAINER to whatever docker ps shows for your qBit container.
Q: My qBit is on a different host. Can I still use Watcharr?
A: Partially. Stall detection, reannounce, and pause/resume all work over the network — point QB_URL at the remote Web UI. The container restart step needs Docker socket access on the same machine as qBit, so that escalation won't fire. For most setups the network-only recovery is enough.
Q: Watcharr keeps restarting my qBit container in a loop. Help.
A: That means DHT is staying at zero between checks. Likely causes: (1) your VPN is dropping/blocking UDP, (2) the VPN tunnel is flapping, or (3) qBit isn't actually starting cleanly. Check the dashboard's log view and the events table. As a temporary mitigation, increase CHECK_INTERVAL so the restart cooldown gives DHT time to bootstrap, and verify qBit can hold DHT > 0 by itself with Watcharr stopped.
Q: Does this work with Docker on UnRAID / Synology / TrueNAS Scale?
A: Yes — anywhere docker compose works. The Docker socket lives at the same path on all of them.
Q: Does it auto-pull a published image?
A: Not yet — for now docker compose up -d builds locally from source. A pre-built image on GHCR is on the roadmap.
watcharr/
├── .github/workflows/ci.yml # GitHub Actions CI
├── docker-compose.yml # Container orchestration
├── Dockerfile # Python 3.11-slim image
├── entrypoint.sh # Creates data/log dirs, starts uvicorn
├── requirements.txt # Python dependencies
├── .env.example # Environment template (safe to commit)
├── VERSION # Semantic version
├── LICENSE # MIT
│
├── core/
│ ├── config.py # Layered settings: JSON > env > defaults
│ ├── logging_setup.py # Dual-output logging (file + console)
│ └── watchdog.py # WatchdogThread: monitoring loop + recovery logic
│
├── web/
│ ├── __init__.py # Package marker
│ ├── app.py # FastAPI app, lifespan, all routes
│ ├── auth.py # Basic Auth dependency (optional)
│ ├── shared_state.py # Watchdog global + settings schema
│ ├── templates/
│ │ └── ui.html # Dashboard HTML shell
│ └── static/
│ ├── ui.js # Client-side logic: polling, events, settings editor
│ └── style.css # Dark theme, responsive layout
│
└── tests/
├── test_watchdog.py # Core watchdog logic tests
└── test_api.py # API + auth tests
# Create a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Set required environment variables
export QB_URL="http://your-qbittorrent-ip:8080/"
export QB_USERNAME="admin"
export QB_PASSWORD="your-password"
export QB_CONTAINER="qbittorrent"
# Start the development server
uvicorn web.app:app --host 0.0.0.0 --port 5035 --workers 1The dashboard will be available at http://localhost:5035.
Note: Container restart functionality requires access to the Docker socket. When running outside Docker, make sure the Docker socket is accessible or expect restart operations to fail gracefully.
pip install pytest
python -m pytest tests/ -vdocker compose build --no-cache
docker compose up -dContributions are welcome. To get started:
- Fork the repository
- Create a feature branch:
git checkout -b my-feature - Make your changes
- Run tests:
python -m pytest tests/ -v - Test locally with Docker:
docker compose up --build - Open a pull request
Please keep changes focused and include a clear description of what changed and why.


