Decision. Downloads on local SSD, then import
Alternative. Downloading straight into the served library
Why. Seeding and library hygiene stay decoupled; incomplete files never appear in the library.
The download workhorse: pulls torrents to the media NAS and seeds them back
qBittorrent lands downloads on the local SSD. Library imports move finished files onward, keeping the torrent client and the served library separate.
templates/qbittorrent/qbittorrent-rollout.yaml qBittorrent Rollout
{{- $projectName := .Values.global.projectName -}}
{{- $imagePullSecretsName := .Values.global.imagePullSecretsName -}}
{{- $hostMediaMountPath := .Values.global.hostMediaMountPath -}}
{{- $timeZone := .Values.global.timeZone -}}
{{- $userID := .Values.global.userID -}}
{{- with .Values.applications.qbittorrent }}
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: {{ .name }}
namespace: {{ $projectName }}
spec:
replicas: 1
strategy:
canary:
maxSurge: 0
maxUnavailable: 1
steps:
- setWeight: 100
selector:
matchLabels:
app: {{ .name }}
template:
metadata:
labels:
app: {{ .name }}
spec:
{{- include "media-systems.nodePlacement" $ | nindent 6 }}
serviceAccount: {{ $projectName }}-service-account
serviceAccountName: {{ $projectName }}-service-account
automountServiceAccountToken: true
containers:
- name: {{ .name }}
image: {{ .image }}
imagePullPolicy: IfNotPresent
ports:
- name: {{ .name | substr 0 9 }}-http
containerPort: {{ .ports.http }}
protocol: TCP
- name: {{ .name }}-tcp
containerPort: {{ .ports.torrent }}
protocol: TCP
- name: {{ .name }}-udp
containerPort: {{ .ports.torrent }}
protocol: UDP
env:
- name: WEBUI_PORT
value: {{ .ports.http | quote }}
- name: PGID
value: {{ $userID | quote }}
- name: PUID
value: {{ $userID | quote }}
- name: TZ
value: {{ $timeZone | quote }}
volumeMounts:
- name: {{ .name }}-config
mountPath: /config
- name: {{ .name }}-downloads
mountPath: {{ .downloadMount }}
resources: {}
initContainers:
- name: {{ .name }}-configuration
image: busybox
command:
- "/bin/sh"
- "-c"
- |
echo "######## Copy certificates to be used by qBittorrent #######";
mkdir -p {{ .configMount }}/.certs/;
chmod 644 {{ .configMount }}/.certs/;
cp /.config/tls.crt {{ .configMount }}/.certs/tls.crt;
cp /.config/tls.key {{ .configMount }}/.certs/tls.key;
chown {{ $userID }}:{{ $userID }} {{ .configMount }}/.certs/tls.crt;
chown {{ $userID }}:{{ $userID }} {{ .configMount }}/.certs/tls.key;
chmod 600 {{ .configMount }}/.certs/tls.crt;
chmod 400 {{ .configMount }}/.certs/tls.key;
echo "Certs Done";
echo "######## Copy config file #######";
cp /tmp/qBittorrent.conf {{ .configMount }}/qBittorrent.conf;
chown {{ $userID }}:users {{ .configMount }}/qBittorrent.conf;
chmod 644 {{ .configMount }}/qBittorrent.conf;
echo "Config Done";
echo "######## Mount and copy Category file #######";
cp /tmp/categories.json {{ .configMount }}/categories.json;
chown {{ $userID }}:users {{ .configMount }}/categories.json;
chmod 644 {{ .configMount }}/categories.json;
echo "Category File Done";
echo "######## Fix ownership of /config for the app user #######";
chown -R {{ $userID }}:{{ $userID }} /config;
echo "Ownership Done";
volumeMounts:
- name: {{ .name }}-config
mountPath: /config
- name: {{ .name }}-tls-secret
mountPath: /.config/tls.crt
subPath: tls.crt
readOnly: false
- name: {{ .name }}-tls-secret
mountPath: /.config/tls.key
subPath: tls.key
readOnly: false
- name: {{ .name }}-config-conf
mountPath: /tmp/qBittorrent.conf
subPath: qBittorrent.conf
- name: {{ .name }}-config-configmap
mountPath: /tmp/categories.json
subPath: categories.json
resources:
requests:
memory: "364Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2"
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
volumes:
- name: {{ .name }}-config
emptyDir: {}
- name: {{ .name }}-config-conf
secret:
secretName: apps-configs-secret
items:
- key: qbittorrent_config
path: qBittorrent.conf
- name: {{ .name }}-config-configmap
configMap:
name: {{ .name }}-config-configmap
- name: {{ .name }}-tls-secret
secret:
secretName: pinilla-io-domain-secret
- name: {{ .name }}-downloads
hostPath:
path: {{ $hostMediaMountPath }}/downloads
type: Directory
{{- end -}} templates/qbittorrent/qbittorrent-config-configmap.yaml qBittorrent settings ConfigMap
{{- $projectName := .Values.global.projectName }}
{{- with .Values.applications.qbittorrent }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .name }}-config-configmap
namespace: {{ $projectName }}
data:
categories.json: |
{
"movies": {
"save_path": "/downloads/movies"
},
"series": {
"save_path": "/downloads/series"
},
"anime": {
"save_path": "/downloads/anime"
},
"others": {
"save_path": "/downloads/others"
}
}
{{- end -}} templates/qbittorrent/tcproute.yaml TCPRoute publishing the torrent port through the Istio gateway.
{{- $projectName := .Values.global.projectName }}
{{- with .Values.applications.qbittorrent }}
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: {{ .name }}-tcproute
namespace: {{ $projectName }}
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: istio-gateway
namespace: istio
sectionName: torrent
rules:
- backendRefs:
- group: ''
kind: Service
name: {{ .name }}-torrent-service
port: {{ .ports.torrent }}
weight: 1
{{- end }} templates/qbittorrent/torrent-service.yaml Dedicated LoadBalancer Service for torrent traffic, on its own MetalLB address.
{{- $projectName := .Values.global.projectName }}
{{- with .Values.applications.qbittorrent }}
apiVersion: v1
kind: Service
metadata:
name: {{ .name }}-torrent-service
namespace: {{ $projectName }}
spec:
type: ClusterIP
selector:
app: {{ .name }}
ipFamilyPolicy: SingleStack
ports:
- name: {{ .name }}-tcp
protocol: TCP
port: {{ .ports.torrent }}
targetPort: {{ .ports.torrent }}
{{- end }} templates/_shared-templates/config-tpl/qbittorrent-configs-config-tpl.yaml Config volume template: renders the qBittorrent config directory
apiVersion: v1
kind: ConfigMap
metadata:
name: qbittorrent-configs-config-tpl
namespace: {{ .Release.Namespace }}
data:
qbittorrent_config: |
[Application]
FileLogger\Age=1
FileLogger\AgeType=1
FileLogger\Backup=true
FileLogger\DeleteOld=true
FileLogger\Enabled=true
FileLogger\MaxSizeBytes=66560
FileLogger\Path=/config/qBittorrent/logs
[AutoRun]
enabled=false
program=
[BitTorrent]
Session\AnonymousModeEnabled=true
Session\DefaultSavePath=/downloads/
Session\DisableAutoTMMByDefault=false
Session\DisableAutoTMMTriggers\CategorySavePathChanged=false
Session\DisableAutoTMMTriggers\DefaultSavePathChanged=false
Session\ExcludedFileNames=
Session\Interface=eth0
Session\InterfaceAddress=0.0.0.0
Session\InterfaceName=eth0
Session\MaxActiveDownloads=6
Session\MaxActiveUploads=2
Session\PerformanceWarning=true
Session\Port=6881
Session\QueueingSystemEnabled=true
Session\SubcategoriesEnabled=false
Session\TempPath=/downloads/incomplete/
[Core]
AutoDeleteAddedTorrentFile=Never
[LegalNotice]
Accepted=true
[Meta]
MigrationVersion=6
[Network]
Cookies=@Invalid()
PortForwardingEnabled=false
Proxy\HostnameLookupEnabled=false
Proxy\Profiles\BitTorrent=true
Proxy\Profiles\Misc=true
Proxy\Profiles\RSS=true
[Preferences]
Connection\PortRangeMin=6881
Connection\UPnP=false
Downloads\SavePath=/downloads/
Downloads\TempPath=/downloads/incomplete/
General\Locale=en
MailNotification\req_auth=true
WebUI\Address=*
WebUI\AuthSubnetWhitelist=192.168.1.0/24, 10.6.0.2/32
WebUI\AuthSubnetWhitelistEnabled=true
WebUI\CustomHTTPHeaders=Access-Control-Allow-Origin: https://service-catalog.opi5cluster.co.uk
WebUI\CustomHTTPHeadersEnabled=true
WebUI\HTTPS\CertificatePath=/config/qBittorrent/.certs/tls.crt
WebUI\HTTPS\Enabled=true
WebUI\HTTPS\KeyPath=/config/qBittorrent/.certs/tls.key
WebUI\LocalHostAuth=false
WebUI\Password_PBKDF2="{{ `{{ .qbittorrent_password }}` }}"
WebUI\Port=8090
WebUI\ReverseProxySupportEnabled=false
WebUI\SecureCookie=false
WebUI\ServerDomains=*
WebUI\TrustedReverseProxiesList=
[RSS]
AutoDownloader\DownloadRepacks=true
AutoDownloader\SmartEpisodeFilter=s(\\d+)e(\\d+), (\\d+)x(\\d+), "(\\d{4}[.\\-]\\d{1,2}[.\\-]\\d{1,2})", "(\\d{1,2}[.\\-]\\d{1,2}[.\\-]\\d{4})" Decision. Downloads on local SSD, then import
Alternative. Downloading straight into the served library
Why. Seeding and library hygiene stay decoupled; incomplete files never appear in the library.