DiskGuard is a lightweight Docker sidecar for qBittorrent that prevents disk exhaustion by automatically pausing and safely resuming downloading torrents based on available free space.
When free space drops below defined levels:
- It pauses new torrents immediately (
POST /on-add). - It enforces
SOFTandHARDprotection modes via a polling loop. - It pauses additional torrents as required to prevent disk exhaustion.
When free space is restored:
- It resumes only torrents it previously paused per defined resume policy (
priority_fifo,smallest_first,largest_first). - It uses projected disk usage (
amount_left, active remaining, floor, buffer) to ensure resuming does not immediately re-trigger protection.
Additional guarantees:
- Respects manual force-start (
forcedDL). - All actions are idempotent and safe across restarts.
- State is derived entirely from qBittorrent tags (
diskguard_paused,soft_allowedwith no local state file).
DiskGuard is intentionally minimal. It does not:
- Delete torrents or files.
- Modify categories.
- Override
forcedDL. - Expose any public API.
- Maintain any local persistence database.
Important
DiskGuard never deletes data. It only pauses and resumes torrents.
- Limited disk environments - Ideal for NVMe or small SSD setups where space is constrained, especially when private tracker minimum seed times delay cleanup.
- Automated request systems (e.g., Seerr) - Prevent large bursts of requests from consuming all available disk space.
- System stability protection - Enforce a hard free-space floor to avoid disk exhaustion and service disruption.
- Docker and docker-compose or Python >=3.13.
- qBittorrent Web API reachable from DiskGuard container.
- qBittorrent
>= v4.2.0and Web API>= 2.3.0. - DiskGuard container must mount the same filesystem qBittorrent writes downloads to.
DiskGuard is designed to run as a Docker sidecar alongside qBittorrent.
-
Add the
diskguardservice to yourdocker-compose.yml(see example). -
Mount:
- Your qBittorrent downloads folder →
/downloads - A config folder →
/config
- Your qBittorrent downloads folder →
-
Create and edit the
config.tomlinside your mounted config directory (see example). -
Add the qBittorrent on-add hook script (see example):
/config/scripts/diskguard_on_add.sh "%I" -
Start or restart your stack:
docker compose up -d
If you are not using Compose:
-
Build the image (if not pulling from GHCR):
docker build -t diskguard:latest .Or pull the published image:
docker pull ghcr.io/alexkahler/qbittorrent-diskguard:latest
-
Run DiskGuard (see example):
docker run -d \ --name diskguard \ --network media \ --user 1000:1000 \ -v /path/to/downloads:/downloads:ro \ -v /path/to/diskguard:/config \ --restart unless-stopped \ ghcr.io/alexkahler/qbittorrent-diskguard:latest
-
Edit the created
config.toml(see example). -
Add the qBittorrent on-add hook (see example):
Create:
/path/to/qbittorrent/config/scripts/diskguard_on_add.shThen configure qBittorrent:
/config/scripts/diskguard_on_add.sh "%I" -
Restart DiskGuard after adding the hook.
Important
DiskGuard must mount the same underlying filesystem that qBittorrent writes downloads to.
Mounting a different path or an overlay filesystem will result in incorrect disk measurements and protection will not work correctly.
services:
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
networks: [media]
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=UTC
- WEBUI_PORT=8080
- DISKGUARD_SERVER_PORT=${DISKGUARD_SERVER_PORT:-7070}
volumes:
- /path/to/qbittorrent:/config
- /path/to/downloads:/downloads
ports:
- "8080:8080"
restart: unless-stopped
diskguard:
image: ghcr.io/alexkahler/qbittorrent-diskguard:latest
container_name: diskguard
networks: [media]
depends_on:
- qbittorrent
user: "${PUID:-1000}:${PGID:-1000}"
environment:
- DISKGUARD_SERVER_PORT=${DISKGUARD_SERVER_PORT:-7070} # Or use the config.toml
- DISKGUARD_ON_ADD_AUTH_TOKEN=${DISKGUARD_ON_ADD_AUTH_TOKEN:-} # Or use the config.toml
#- DISKGUARD_QBITTORRENT_URL=http://qbittorrent:8080 # Required if not using a persistent volume
#- DISKGUARD_QBITTORRENT_USERNAME=${QBITTORRENT_USERNAME:-admin} # Required if not using a persistent volume
#- DISKGUARD_QBITTORRENT_PASSWORD=${QBITTORRENT_PASSWORD:-} # Required if not using a persistent volume
volumes:
- /path/to/downloads:/downloads:ro # qBittorrent download folder
- /path/to/diskguard:/config
restart: unless-stopped
networks:
media:
driver: bridgeservices:
gluetun:
container_name: gluetun
image: ghcr.io/qdm12/gluetun:latest
cap_add:
- NET_ADMIN
volumes:
- ./gluetun:/config
environment:
- VPN_SERVICE_PROVIDER=
- VPN_TYPE=wireguard
- PORT_FORWARD_ONLY=on
- WIREGUARD_PRIVATE_KEY=
- VPN_PORT_FORWARDING=on
- VPN_PORT_FORWARDING_PROVIDER=protonvpn
- FIREWALL_OUTBOUND_SUBNETS=
- UPDATER_PERIOD=24h
- TZ=${TZ}
- VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c 'wget -O- --retry-connrefused --post-data "json={\"listen_port\":{{PORTS}}}" http://127.0.0.1:8080/api/v2/app/setPreferences 2>&1'
ports:
- ${LAN_IP}:8080:8080/tcp # qBittorrent
restart: always
networks:
- "vpn-net"
qbittorrent:
container_name: qbittorrent
image: lscr.io/linuxserver/qbittorrent:latest
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- WEBUI_PORT=${WEBUI_PORT}
volumes:
- /path/to/qbittorrent:/config
- /path/to/downloads:/downloads
restart: unless-stopped
network_mode: service:gluetun
stop_grace_period: 60s
healthcheck: # https://github.com/qdm12/gluetun/issues/641#issuecomment-933856220
test: "curl -sf ifconfig.me || exit 1"
interval: 1m
timeout: 10s
retries: 5
diskguard:
image: ghcr.io/alexkahler/qbittorrent-diskguard:latest
container_name: diskguard
user: "${PUID}:${PGID}"
depends_on:
qbittorrent:
condition: service_healthy
environment:
- DISKGUARD_SERVER_PORT=${DISKGUARD_SERVER_PORT:-7070}
volumes:
- /path/to/downloads:/downloads:ro
- /path/to/diskguard:/config
network_mode: service:gluetun
restart: unless-stopped
docker run -d \
--name diskguard \
--network media \
--user 1000:1000 \
-e DISKGUARD_SERVER_PORT=7070 \
-e DISKGUARD_ON_ADD_AUTH_TOKEN=your-static-token \
-v /path/to/downloads:/downloads:ro \
-v /path/to/diskguard:/config \
--restart unless-stopped \
ghcr.io/alexkahler/qbittorrent-diskguard:latest
Caution
Do not publish the DiskGuard API port externally. Keep it internal to the Docker network.
Optional hardening for the diskguard service: read_only: true,
cap_drop: ["ALL"], and security_opt: ["no-new-privileges:true"].
- Bind mounts keep host file ownership/permissions.
- If
/path/to/downloadsis not world-readable, DiskGuard may fail to read disk stats when container UID/GID do not match host ownership. user: "${PUID}:${PGID}"makes DiskGuard process run with host-equivalent IDs for reliable read access.
- Recommended: bind mount a folder (
/path/to/diskguard:/config) so first-run bootstrap writes/path/to/diskguard/config.toml. - Also supported: named volume (example:
diskguard_config:/config) for persistence across restarts.
Warning
If no /config volume is mounted, DiskGuard will start and create /config/config.toml,
but configuration will be lost when the container is removed.
It is recommended to always mount /config to persist settings.
[qbittorrent]
url = "http://qbittorrent:8080" # Required
username = "admin" # Required
password = "" # Required (must be non-empty)
[disk]
watch_path = "/downloads"
soft_pause_below_pct = 10
hard_pause_below_pct = 5
resume_floor_pct = 10
safety_buffer_gb = 10
downloading_states = ["downloading", "metaDL", "queuedDL", "stalledDL", "checkingDL", "allocating"]
[polling]
interval_seconds = 30
on_add_quick_poll_interval_seconds = 1.0
on_add_quick_poll_max_attempts = 10
on_add_quick_poll_max_queue_size = 64
[resume]
policy = "priority_fifo"
strict_fifo = true
[tagging]
paused_tag = "diskguard_paused"
soft_allowed_tag = "soft_allowed"
[logging]
level = "INFO"
[server]
host = "0.0.0.0"
port = 7070
on_add_auth_token = "" # Required (must be non-empty)
on_add_max_body_bytes = 8192Tip
Find a fully commented config.toml file in the examples folder.
Note
To enable quick stopping of torrents when they are added by your *arr applications, it is recommended to set up a shell script so that DiskGuard can be notified whenever a new torrent is added.
Generate a static shared secret once:
openssl rand -hex 32Set the same token value in both:
- DiskGuard config
server.on_add_auth_token(orDISKGUARD_ON_ADD_AUTH_TOKEN) - qBittorrent hook script variable
DISKGUARD_ON_ADD_AUTH_TOKEN
Create /path/to/qbittorrent/config/scripts/diskguard_on_add.sh (or another path which qBittorrent has access to):
#!/bin/sh
# Usage: diskguard_on_add.sh "<hash>"
HASH="$1"
DISKGUARD_URL=diskguard # Change this to match the service name, or localhost if using Gluetun
DISKGUARD_SERVER_PORT=7070 # Remember to update this if you have changed the default DiskGuard server port in the environment settings.
DISKGUARD_ON_ADD_AUTH_TOKEN=your-secret-token # Set this to the same value as [server].on_add_auth_token in DiskGuard config.
curl -fsS -m 2 \
-X POST "http://${DISKGUARD_URL}:${DISKGUARD_SERVER_PORT}/on-add" \
-H "X-DiskGuard-Token: ${DISKGUARD_ON_ADD_AUTH_TOKEN}" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "hash=${HASH}" \
>/dev/null 2>&1 &
exit 0Tip
Find a copy-paste ready diskguard_on_add.sh shell script in the examples folder.
Important
Hook URL requirements:
- Host must be Docker service name
diskguard(same Docker network as qBittorrent) orlocalhostif DiskGuard is innetwork_mode: service:<some_service>. - Port must match DiskGuard effective listen port:
server.portinconfig.toml, orDISKGUARD_SERVER_PORTenv override in DiskGuard container.
- Path must be
/on-add. - Header
X-DiskGuard-Tokenmust match DiskGuardserver.on_add_auth_token. - The first argument must be a "%I" postfix to the script.
Caution
If the host or port is incorrect, torrents will not be paused on add. SOFT mode polling will eventually correct this, but protection will be delayed.
Make it executable:
chmod +x ./qbittorrent/config/scripts/diskguard_on_add.shFinally, in qBittorrent, go to:
Options(Gear icon)- Click on
Downloadstab - Scroll down to
Run external programsection - Enable checkbox on
Run on torrent added: - Fill in the path to the script with:
/config/scripts/diskguard_on_add.sh "%I"
Tip
If you want a different webhook port, set one variable in environment section of your docker compose:
DISKGUARD_SERVER_PORT=7171DiskGuard reads /config/config.toml and supports flat environment variable overrides.
On startup it creates /config and /config/config.toml automatically when missing.
Important
Bootstrapped config initializes qbittorrent.password and server.on_add_auth_token
as empty values. DiskGuard exits until both are set to non-empty secrets.
qbittorrent.urlqbittorrent.usernameqbittorrent.passwordserver.on_add_auth_token
disk.watch_path = "/downloads"disk.soft_pause_below_pct = 10disk.hard_pause_below_pct = 5disk.resume_floor_pct = 10disk.safety_buffer_gb = 10polling.interval_seconds = 30polling.on_add_quick_poll_interval_seconds = 1.0polling.on_add_quick_poll_max_attempts = 10polling.on_add_quick_poll_max_queue_size = 64resume.policy = "priority_fifo"resume.strict_fifo = truetagging.paused_tag = "diskguard_paused"tagging.soft_allowed_tag = "soft_allowed"logging.level = "INFO"server.host = "0.0.0.0"server.port = 7070server.on_add_max_body_bytes = 8192
disk.hard_pause_below_pct < disk.soft_pause_below_pctdisk.resume_floor_pct >= disk.soft_pause_below_pct
DISKGUARD_QBITTORRENT_URL=http://qbittorrent:8080DISKGUARD_QBITTORRENT_USERNAME=adminDISKGUARD_QBITTORRENT_PASSWORD=your-qb-passwordDISKGUARD_QBITTORRENT_CONNECT_TIMEOUT_SECONDS=2.0DISKGUARD_QBITTORRENT_READ_TIMEOUT_SECONDS=8.0DISKGUARD_DISK_WATCH_PATH=/downloadsDISKGUARD_DISK_SOFT_PAUSE_BELOW_PCT=10DISKGUARD_DISK_HARD_PAUSE_BELOW_PCT=5DISKGUARD_DISK_RESUME_FLOOR_PCT=10DISKGUARD_DISK_SAFETY_BUFFER_GB=10DISKGUARD_DISK_DOWNLOADING_STATES=downloading,metaDL,queuedDL,stalledDL,checkingDL,allocatingDISKGUARD_POLLING_INTERVAL_SECONDS=30DISKGUARD_SERVER_PORT=7070DISKGUARD_ON_ADD_QUICK_POLL_INTERVAL_SECONDS=1.0DISKGUARD_ON_ADD_QUICK_POLL_MAX_ATTEMPTS=10DISKGUARD_ON_ADD_QUICK_POLL_MAX_QUEUE_SIZE=64DISKGUARD_RESUME_POLICY=priority_fifoDISKGUARD_RESUME_STRICT_FIFO=trueDISKGUARD_TAGGING_PAUSED_TAG=diskguard_pausedDISKGUARD_TAGGING_SOFT_ALLOWED_TAG=soft_allowedDISKGUARD_LOGGING_LEVEL=DEBUGDISKGUARD_SERVER_HOST=0.0.0.0DISKGUARD_ON_ADD_AUTH_TOKEN=your-secret-tokenDISKGUARD_SERVER_ON_ADD_MAX_BODY_BYTES=8192
Important
Environment variables always override values in config.toml.
If both are set, the environment variable takes precedence.
server.hostis the socket bind address inside the DiskGuard container.- In Docker, keep
server.host = "0.0.0.0"so other containers can reach DiskGuard. Only change this if your setup is unique. server.hostcannot be auto-derived from Docker service name; service names (diskguard) are DNS endpoints, not bind interfaces.server.portis the listen port and must match what the qBittorrent hook calls.server.on_add_auth_tokenis required and must be non-empty./on-addrejects requests missingX-DiskGuard-Tokenwith HTTP401.server.on_add_max_body_bytesbounds accepted payload size for/on-add(default8192).- Do not publish DiskGuard port externally (no
ports:mapping on the DiskGuard service).
When disk space becomes available again (NORMAL mode), DiskGuard resumes only torrents tagged diskguard_paused.
The order in which they are resumed is controlled by the resume.policy setting.
Resumes torrents by:
- Highest qBittorrent priority first
- Oldest first within the same priority
This respects manual priority settings and keeps queue behavior predictable.
If strict_fifo = true:
- Stops at the first torrent that does not fit the disk budget.
If strict_fifo = false:
- Skips torrents that do not fit and continues checking the next one.
Best for: predictable queue behavior that aligns with qBittorrent priorities.
Resumes torrents with the smallest amount_left first.
This maximizes the number of torrents that can resume within the available disk budget.
Best for: finishing many small downloads quickly.
Resumes torrents with the largest amount_left first.
This favors completing large downloads earlier.
Best for: prioritizing big releases or long-running downloads.
Tip
If unsure, keep the default priority_fifo. It aligns with qBittorrent’s built-in priority system and works well for most users.
Before resuming any torrent, DiskGuard calculates projected disk usage. A torrent is resumed only if doing so will not drop free space below:
resume_floor_pct- plus
safety_buffer_gb
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
PYTHONPATH=src python -m diskguardpip install -r requirements-dev.txt
PYTHONPATH=src pytest- Runtime container installs from
requirements.lockusing--require-hashes. - Generate the lockfile with the same Python minor version as the runtime image/CI (
3.13) to avoid hash-mode marker mismatches. - Regenerate lockfile after dependency updates:
pip install -r requirements-dev.txt
python -m piptools compile --generate-hashes --output-file requirements.lock requirements.in- Symptom: ERROR logs about disk probe failure, no pause/resume actions.
- Check that DiskGuard mounts the same downloads filesystem as qBittorrent.
- Symptom: startup fails with
/config is not writable. - Fix by mounting a writable config directory, for example
./diskguard:/config. - Avoid read-only
/configmounts, because DiskGuard creates/config/config.tomlon first run.
- Symptom: startup WARNING says
/configis not backed by a Docker volume. - DiskGuard is running without a mapped config volume.
- Mount
./diskguard:/config(recommended) ordiskguard_config:/configto persist config.
- Symptom: startup retries followed by ERROR preflight failure, or WARNING logs during runtime ticks.
- Verify
qbittorrent.url, username, password in/config/config.toml.
- Symptom: startup exits with
cannot be emptyforqbittorrent.passwordorserver.on_add_auth_token. - Set both
qbittorrent.passwordandserver.on_add_auth_tokento non-empty values in/config/config.toml.
- Symptom: qBittorrent hook runs, but DiskGuard returns HTTP
401. - Verify
X-DiskGuard-Tokenin the hook script matchesserver.on_add_auth_tokenexactly. - Ensure
server.on_add_auth_tokenis set to a non-empty secret value.
- Symptom: startup fails immediately with an incompatible version ERROR message.
- Required minimum: qBittorrent
>= v4.2.0and Web API>= 2.3.0. - Upgrade qBittorrent, then restart DiskGuard.
- Symptom: WARNING logs for unreachable qB API, delayed enforcement until recovery.
- Verify both services share the same Docker network and service name resolution works.
- Symptom: torrents not resuming or not protected as expected.
- Check tag names in
[tagging]config. - Verify qBittorrent account has permission to pause/resume and edit tags.
- Ensure hook script path in qBittorrent is correct and executable.
Note
This project used AI/LLM tools (e.g., ChatGPT, Claude, etc.) to assist with grammar, spelling, tone, clarity, and editorial review of documentation (e.g., README.md). AI was also used to help brainstorm and draft example code snippets and identify potential issues. All AI-assisted output was critically reviewed, edited as needed, and verified by a human maintainer who understands the content and code committed to this repository. No AI output was included verbatim without human approval.