qBittorrent

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.

Manifests & templates

templates/qbittorrent/qbittorrent-rollout.yaml

qBittorrent Rollout

Show manifest
{{- $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

Show manifest
{{- $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.

Show manifest
{{- $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.

Show manifest
{{- $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

Show manifest
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})"
  • media-systems is one umbrella Application: the whole pipeline deploys as a unit, pinned to a single node with local SSD storage. Per-app settings live in the media chart, not here.
  • This app is a section of the shared media-systems values file above; the media stack deploys as one ArgoCD Application.

Trade-offs

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.

← Back to Media · All service groups