Files
nixos/services/qbittorrent.nix
Simon Gardling e8e3174420
All checks were successful
Build and Deploy / mreow (push) Successful in 2m22s
Build and Deploy / yarn (push) Successful in 2m19s
Build and Deploy / muffin (push) Successful in 1m59s
remove timeout stop sec for qbt
2026-04-20 22:19:24 -04:00

212 lines
8.5 KiB
Nix
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
pkgs,
config,
service_configs,
lib,
inputs,
...
}:
let
categoriesFile = pkgs.writeText "categories.json" (
builtins.toJSON (lib.mapAttrs (_: path: { save_path = path; }) service_configs.torrent.categories)
);
in
{
imports = [
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_hdds [
service_configs.torrents_path
])
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_ssds [
"${config.services.qbittorrent.profileDir}/qBittorrent"
])
(lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "qbittorrent")
(lib.serviceFilePerms "qbittorrent" [
# 0770: group (media) needs write to delete files during upgrades —
# Radarr/Sonarr must unlink the old file before placing the new one.
# Non-recursive (z not Z): UMask=0007 ensures new files get correct perms.
# A recursive Z rule would walk millions of files on the HDD pool at every boot.
"z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.SavePath} 0770 ${config.services.qbittorrent.user} ${service_configs.media_group}"
"z ${config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}"
"Z ${config.services.qbittorrent.profileDir} 0700 ${config.services.qbittorrent.user} ${config.services.qbittorrent.group}"
])
(lib.mkCaddyReverseProxy {
subdomain = "torrent";
port = service_configs.ports.private.torrent.port;
auth = true;
vpn = true;
})
];
services.qbittorrent = {
enable = true;
webuiPort = service_configs.ports.private.torrent.port;
profileDir = "/var/lib/qBittorrent";
# Set the service group to 'media' so the systemd unit runs with media as
# the primary GID. Linux assigns new file ownership from the process's GID
# (set by systemd's Group= directive), not from /etc/passwd. Without this,
# downloads land as qbittorrent:qbittorrent (0700), blocking Radarr/Sonarr.
group = service_configs.media_group;
serverConfig.LegalNotice.Accepted = true;
serverConfig.Preferences = {
WebUI = {
AlternativeUIEnabled = true;
RootFolder = "${pkgs.vuetorrent}/share/vuetorrent";
# disable auth because we use caddy for auth
AuthSubnetWhitelist = "0.0.0.0/0";
AuthSubnetWhitelistEnabled = true;
};
Downloads = {
inherit (service_configs.torrent) SavePath TempPath;
};
};
serverConfig.BitTorrent = {
Session = {
MaxConnectionsPerTorrent = 100;
MaxUploadsPerTorrent = 50;
MaxConnections = -1;
MaxUploads = -1;
MaxActiveCheckingTorrents = 2;
# queueing
QueueingSystemEnabled = true;
MaxActiveDownloads = 15;
MaxActiveUploads = -1;
MaxActiveTorrents = -1;
IgnoreSlowTorrentsForQueueing = true;
GlobalUPSpeedLimit = 0;
GlobalDLSpeedLimit = 0;
# Alternate speed limits for when Jellyfin is streaming
AlternativeGlobalUPSpeedLimit = 500; # 500 KB/s when throttled
AlternativeGlobalDLSpeedLimit = 800; # 800 KB/s when throttled
IncludeOverheadInLimits = true;
GlobalMaxRatio = 7.0;
AddTrackersEnabled = true;
AdditionalTrackers = lib.concatStringsSep "\\n" (
lib.lists.filter (x: x != "") (
lib.strings.splitString "\n" (builtins.readFile "${inputs.trackerlist}/trackers_all.txt")
)
);
AnnounceToAllTrackers = true;
# idk why it also has to be specified here too?
inherit (config.services.qbittorrent.serverConfig.Preferences.Downloads) TempPath;
TempPathEnabled = true;
ConnectionSpeed = 200; # half-open connections/s; faster peer discovery
SaveResumeDataInterval = 300; # save resume data every 5 min (default 60s)
ResumeDataStorageType = "SQLite"; # SQLite is more efficient than legacy per-file .fastresume storage
# Automatic Torrent Management: use category save paths for new torrents
DisableAutoTMMByDefault = false;
DisableAutoTMMTriggers.CategorySavePathChanged = false;
DisableAutoTMMTriggers.DefaultSavePathChanged = false;
ChokingAlgorithm = "RateBased";
SeedChokingAlgorithm = "FastestUpload"; # unchoke peers we upload to fastest
PieceExtentAffinity = true;
SuggestMode = true;
# POSIX-compliant disk I/O: uses pread/pwrite instead of mmap.
# On ZFS, mmap forces data into BOTH ARC and Linux page cache (double-caching),
# wasting RAM. pread/pwrite goes only through ARC, maximizing its effectiveness.
DiskIOType = "Posix";
FilePoolSize = 500; # keep more files open to reduce open/close overhead
AioThreads = 24; # 6 cores * 4; better disk I/O parallelism
# Send buffer watermarks control how much upload data qBittorrent pre-reads
# from disk per peer. Pushed well above libtorrent defaults (and above
# high_performance_seed's 3 MiB) to let the disk I/O thread issue larger,
# less-frequent preadv() calls. Larger watermarks give ZFS's vdev aggregator
# (4 MiB limit, 128 KiB gap) more contiguous requests to merge per sweep.
# Memory cost is roughly watermark × active-peer-count; 6 MiB × a few hundred
# peers is well within muffin's RAM budget.
SendBufferLowWatermark = 512; # 512 KiB -- trigger reads sooner to prevent upload stalls
SendBufferWatermark = 6144; # 6 MiB -- bigger pre-reads per peer than high_performance_seed's 3 MiB
SendBufferWatermarkFactor = 200; # percent -- scale watermark more aggressively with upload rate
# Maximum outstanding block requests per peer (libtorrent max_out_request_queue).
# Default 500. Tripled so libtorrent's disk I/O thread sees deeper per-peer
# request queues, giving it more contiguous blocks to coalesce into single
# preadv() calls before issuing them to the kernel.
RequestQueueSize = 1500;
};
Network = {
# traffic is routed through a vpn, we don't need
# port forwarding
PortForwardingEnabled = false;
};
Session.UseUPnP = false;
};
};
systemd.services.qbittorrent.serviceConfig = {
# Default UMask=0022 creates files as 0644 (group read-only). With 0007,
# new files get 0660/0770 so the media group has read+write immediately
# instead of relying on the tmpfiles Z rule to fix permissions at restart.
UMask = lib.mkForce "0007";
};
# Pre-define qBittorrent categories with explicit save paths so every
# torrent routes to its category directory instead of the SavePath root.
systemd.tmpfiles.settings.qbittorrent-categories = {
"${config.services.qbittorrent.profileDir}/qBittorrent/config/categories.json"."L+" = {
argument = "${categoriesFile}";
user = config.services.qbittorrent.user;
group = config.services.qbittorrent.group;
mode = "1400";
};
};
# Ensure category directories exist with correct ownership before first use.
systemd.tmpfiles.rules = lib.mapAttrsToList (
_: path: "d ${path} 0770 ${config.services.qbittorrent.user} ${service_configs.media_group} -"
) service_configs.torrent.categories;
# Periodically checkpoint qBittorrent's SQLite WAL (Write-Ahead Log).
# qBittorrent holds a read transaction open for its entire lifetime,
# preventing SQLite's auto-checkpoint from running. The WAL grows
# unbounded (observed: 405 MB) and must be replayed on next startup,
# causing 10+ minute "internal preparations" hangs.
# A second sqlite3 connection can checkpoint concurrently and safely.
# See: https://github.com/qbittorrent/qBittorrent/issues/20433
systemd.services.qbittorrent-wal-checkpoint = {
description = "Checkpoint qBittorrent SQLite WAL";
after = [ "qbittorrent.service" ];
requires = [ "qbittorrent.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.sqlite}/bin/sqlite3 ${config.services.qbittorrent.profileDir}/qBittorrent/data/torrents.db 'PRAGMA wal_checkpoint(TRUNCATE);'";
User = config.services.qbittorrent.user;
Group = config.services.qbittorrent.group;
};
};
systemd.timers.qbittorrent-wal-checkpoint = {
description = "Periodically checkpoint qBittorrent SQLite WAL";
wantedBy = [ "timers.target" ];
timerConfig = {
OnUnitActiveSec = "4h";
OnBootSec = "30min";
RandomizedDelaySec = "10min";
};
};
users.users.${config.services.qbittorrent.user}.extraGroups = [
service_configs.media_group
];
}