SparkBox/Guides/qBittorrent unhealthy

qBittorrent keeps flipping to "unhealthy" and restarting on boot

You start your stack, run docker ps, and qBittorrent (and maybe Sonarr, Radarr, Prowlarr, Jellyfin) shows (unhealthy) — then it restarts on its own, over and over, before it ever finishes booting. On a slow board this can loop for minutes. The gist: the container's healthcheck grace window is too short for your hardware's first-boot time, so a watchdog restarts a perfectly fine app while it's still warming up.

qBittorrent web UI behind the SparkBox VPN
qBittorrent web UI behind the SparkBox VPN

Rather not hand-tune healthchecks? SparkBox ships start_period values that account for slow ARM first-boot and verifies container health on every up so the watchdog doesn't fire prematurely. See the walkthrough →

The 10-second version: The app isn't broken — its healthcheck start_period (the grace window before a failing check counts) is tuned for fast x86 and runs out before a slow ARM board finishes booting. Raise the start_period for the affected containers, restart the stack, and the unhealthy-restart loop stops.

What "unhealthy" actually means here

A healthcheck is a command Docker runs inside a container on a schedule to decide if the app is really working — usually a quick request to the app's web port. If that command keeps failing, Docker marks the container unhealthy.

Crucially, Docker has a start_period: a startup grace window during which a failing healthcheck is not counted against the container. The idea is to give slow-starting apps time to come up before anyone panics. When the start_period ends, the next failure flips the container to unhealthy.

Separately, a watchdog called deunhealth watches for the unhealthy label and restarts any container that gets it. That's its whole job, and on a healthy stack it's a useful safety net for an app that genuinely hangs later. The trouble is what happens when the label is wrong.

The real root cause: start_period tuned for x86, not slow ARM

The media healthchecks were tuned for typical x86 first-boot speed:

On a 2-core ARM board (think a small SBC or low-power NAS), first boot is much slower. The .NET apps (Sonarr/Radarr/Prowlarr/Lidarr/Bazarr) and Jellyfin spend their first launch building or migrating databases, which can take minutes, not seconds. So the start_period expires while the app is still initialising. The very next healthcheck fails, Docker stamps the container unhealthy, and deunhealth restarts it — mid-boot.

Why the loop self-reinforces: each restart throws away the warm-up progress and makes the app start over. So the same too-short window expires again, the watchdog fires again, and you can be stuck cycling for a long time on slow hardware. The app was never broken — it was just being interrupted.

How to confirm this is your problem

  1. Watch the container list as it boots:
    docker ps
    If you see (health: starting) turn into (unhealthy) and then the uptime resets to a few seconds, that's the restart loop.
  2. Check the recent events for one app — repeated restarts with short uptimes confirm a watchdog, not a crash:
    docker inspect --format '{{json .State.Health}}' qbittorrent
    A long list of failing ExitCode probe results during early boot, on running-but-restarting containers, points straight at start_period.
  3. Confirm the app is otherwise fine: once it's left alone long enough to finish, the web UI loads and the health flips to healthy. If that's true, this guide is your fix.

Fix 1 — Give the healthchecks a longer grace window

The clean fix is to raise start_period for the slow starters so it comfortably covers real first-boot time on your board. This doesn't make the app slower or weaker — it just stops the watchdog from judging it before it's ready.

  1. Open the Compose file that defines these services.
  2. Find each service's healthcheck block and raise start_period. A safe target on a 2-core ARM board is a few minutes for the .NET apps and Jellyfin, and at least a minute for qBittorrent. Use the time the app actually needs (from your docker ps observation) plus a margin. For example:
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS http://localhost:8080 || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 180s
    Keep the existing test line as-is — only change start_period. The port and path shown above are an illustration; use whatever your file already has.
  3. Recreate the affected containers so the new setting takes effect:
    docker compose up -d
  4. Watch docker ps through a full cold boot. The apps should sit in (health: starting) for as long as they genuinely need, then go healthy — with no restart in between.

Gotcha: changing the file isn't enough on its own — Docker reads healthcheck at container creation, so you must recreate the container (up -d or down then up), not just restart it. A plain docker restart keeps the old start_period.

Fix 2 — Don't let the watchdog punish a still-booting app

deunhealth only acts on containers Docker has already labelled unhealthy. So the moment Fix 1 stops premature unhealthy flags, deunhealth stops restarting things. You generally don't want to disable the watchdog — it's genuinely useful for an app that wedges itself after a good boot. Leave it running once the start_periods are right.

Jellyfin home screen on a SparkBox server
Jellyfin home screen on a SparkBox server

If you need a calm boot right now while you tune values, you can temporarily stop the watchdog so it can't interrupt the apps:

docker stop deunhealth

Let the stack finish booting fully (every app reaches healthy), then start it again:

docker start deunhealth

This is a stopgap, not a fix. If you leave the watchdog off and the start_periods unchanged, you simply lose the safety net while keeping the underlying mistuning. Do Fix 1 too.

Fix 3 — Rule out a real network/bridge fault

Most boot-time unhealthy flags on ARM are the start_period story above. But a healthcheck can also fail for real if the container can't reach the network it depends on — for example a stale or leaked Docker bridge after a crash, an interrupted update, or a host that rebooted unexpectedly. In that case the app never reaches healthy no matter how long you wait.

On SparkBox this class of fault is handled for you. ensure_sb_proxy_healthy() checks that the backing bridge network is live and recovers a stale or leaked one (it recreates the network, and restarts the Docker daemon only if that's needed — and it refuses to attempt the daemon restart from inside a container, which would be unsafe). That check runs as part of cmd_up, restore_start_stack, and repair-network, so the network is verified before the apps are asked to come up.

SparkBox's doctor also gained a [Container Health] section that flags sb- containers that are created, dead, or crashed and warns about a stale bridge — so a health check can no longer claim "all passed" while an app is quietly stuck. If you're not on SparkBox, the manual equivalent is: confirm the shared network exists (docker network ls), confirm the app is attached to it (docker inspect), and recreate the network if it looks orphaned, then bring the stack back up.

Which is it? If the app reaches healthy when left undisturbed (watchdog stopped), it's Fix 1 — too-short start_period. If it never goes healthy even after minutes, look at Fix 3 — a real connectivity or bridge problem.

Frequently asked

Why does qBittorrent show as unhealthy only on boot?

The healthcheck has a start_period — a grace window where a failing check is ignored. qBittorrent's is 20 seconds, which is fine on x86 but too short on slow ARM first-boot. If the app isn't answering yet when the window ends, Docker marks it unhealthy and the watchdog restarts it.

What is deunhealth and why is it restarting my containers?

deunhealth is a watchdog that restarts any container Docker has labelled unhealthy. It's doing its job correctly. The real fault is that the healthcheck flips unhealthy too early during boot, so the watchdog restarts the app mid-startup and makes boot slower.

How do I fix the qBittorrent unhealthy loop?

Raise the start_period on the affected containers so the grace window covers real first-boot time on your hardware, then recreate the containers with docker compose up -d and watch docker ps until everything settles to healthy.

Is an unhealthy status the same as a crashed container?

No. Unhealthy means the container is running but its healthcheck is failing. Crashed or dead means the process exited. During boot, an unhealthy flag is usually just an impatient healthcheck, not a real fault.

Skip the healthcheck tuning

SparkBox ships start_period values that account for slow ARM first-boot, verifies the network bridge before bringing apps up, and surfaces real container-health problems in doctor instead of hiding them — so qBittorrent and your *arr stack boot once and stay healthy.

Get SparkBox → Or read the media-server walkthrough →

About this guide: Written and tested by the SparkBox team on a UGREEN DXP4800 Plus and a $7/month Hostinger VPS, both running SparkBox 1.6.314. The causes above are the real ones we've diagnosed in d/sparkbox. If something doesn't match, tell us on YouTube.