--- apiVersion: apps/v1 kind: Deployment metadata: name: qbittorrentvpn namespace: plex spec: strategy: type: Recreate selector: matchLabels: app: qbittorrentvpn replicas: 1 template: metadata: labels: app: qbittorrentvpn annotations: backup.velero.io/backup-volumes-excludes: seedbox,media,media2,data-ec,scratch spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: seedbox operator: In values: - "true" containers: - name: qbittorrentvpn image: binhex/arch-qbittorrentvpn:5.1.4-1-01 ports: - containerPort: 8080 name: http-web-svc securityContext: privileged: true env: - name: DEBUG value: "true" - name: ENABLE_PRIVOXY value: "no" - name: LAN_NETWORK value: "172.16.69.0/24,10.42.0.0/16" - name: NAME_SERVERS value: "209.244.0.3,209.244.0.4" - name: PGID value: "1000" - name: PUID value: "1000" - name: STRICT_PORT_FORWARD value: "yes" - name: VPN_CLIENT value: "wireguard" - name: VPN_ENABLED value: "yes" - name: VPN_PROV value: "airvpn" - name: VPN_USER valueFrom: secretKeyRef: name: qbittorrentvpn key: VPN_USER - name: VPN_PASS valueFrom: secretKeyRef: name: qbittorrentvpn key: VPN_PASS livenessProbe: exec: command: ["curl", "--fail", "localhost:8080"] volumeMounts: - mountPath: "/media" name: media - mountPath: "/media2" name: media2 - mountPath: "/dataec" name: data-ec - mountPath: "/config" name: config - mountPath: "/scratch" name: seedbox volumes: - name: media persistentVolumeClaim: claimName: plex-pvc - name: media2 persistentVolumeClaim: claimName: media2-pvc - name: data-ec persistentVolumeClaim: claimName: data-ec-pvc - name: config persistentVolumeClaim: claimName: qbittorrentvpn-pvc - name: seedbox hostPath: path: /seedbox/torrents type: Directory --- apiVersion: v1 kind: Service metadata: name: qbittorrentvpn-service namespace: plex spec: selector: app: qbittorrentvpn type: ClusterIP ports: - name: qbittorrentvpn-web-port protocol: TCP port: 8080 targetPort: http-web-svc --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: qbittorrentvpn namespace: plex annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.middlewares: kube-system-lanonly@kubernetescrd spec: rules: - host: qbittorrentvpn.lan.jibby.org http: paths: - path: / pathType: Prefix backend: service: name: qbittorrentvpn-service port: number: 8080 --- apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: qbittorrentvpn namespace: plex spec: target: name: qbittorrentvpn deletionPolicy: Delete template: type: Opaque data: VPN_USER: |- {{ .username }} VPN_PASS: |- {{ .password }} data: - secretKey: username sourceRef: storeRef: name: bitwarden-login kind: ClusterSecretStore remoteRef: key: 19b0020e-51d3-42eb-b78b-b1d7012d1a8a property: username - secretKey: password sourceRef: storeRef: name: bitwarden-login kind: ClusterSecretStore remoteRef: key: 19b0020e-51d3-42eb-b78b-b1d7012d1a8a property: password --- apiVersion: apps/v1 kind: Deployment metadata: name: qbittorrentvpn-exporter namespace: plex spec: strategy: type: Recreate selector: matchLabels: app: qbittorrentvpn-exporter replicas: 1 template: metadata: labels: app: qbittorrentvpn-exporter spec: containers: - name: qbittorrentvpn-exporter image: ghcr.io/esanchezm/prometheus-qbittorrent-exporter:latest ports: - containerPort: 8000 name: metrics env: - name: QBITTORRENT_HOST value: qbittorrentvpn.lan.jibby.org - name: QBITTORRENT_PORT value: "443" - name: QBITTORRENT_SSL value: "True" - name: QBITTORRENT_USER valueFrom: secretKeyRef: name: qbittorrentvpn-exporter key: QBITTORRENT_USER - name: QBITTORRENT_PASS valueFrom: secretKeyRef: name: qbittorrentvpn-exporter key: QBITTORRENT_PASS livenessProbe: exec: command: - "/bin/sh" - "-c" - 'wget -O - 0.0.0.0:8000 | grep -E "qbittorrent_up\{.* 1.0"' initialDelaySeconds: 3 timeoutSeconds: 5 periodSeconds: 3 failureThreshold: 15 resources: requests: memory: "0" limits: memory: "256Mi" --- apiVersion: v1 kind: Service metadata: name: qbittorrentvpn-exporter-service namespace: plex labels: app: qbittorrentvpn-exporter spec: selector: app: qbittorrentvpn-exporter type: ClusterIP ports: - name: metrics protocol: TCP port: 8000 targetPort: metrics --- apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: labels: prometheus: qbittorrent role: alert-rules name: prometheus-qbittorrent-rules namespace: plex spec: groups: - name: ./qbittorrent.rules rules: - alert: QbittorrentErroredTorrents expr: sum(qbittorrent_torrents_count{status="error"}) > 0 --- apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: qbittorrentvpn-exporter namespace: plex spec: target: name: qbittorrentvpn-exporter deletionPolicy: Delete template: type: Opaque data: QBITTORRENT_USER: |- {{ .username }} QBITTORRENT_PASS: |- {{ .password }} data: - secretKey: username sourceRef: storeRef: name: bitwarden-login kind: ClusterSecretStore remoteRef: key: 8dd7dfc3-800d-4af5-8a45-b23f0132806c property: username - secretKey: password sourceRef: storeRef: name: bitwarden-login kind: ClusterSecretStore remoteRef: key: 8dd7dfc3-800d-4af5-8a45-b23f0132806c property: password # qbit_manage to auto-tag by tracker URL --- apiVersion: batch/v1 kind: CronJob metadata: name: qbittorrentvpn-manage namespace: plex spec: schedule: "*/10 * * * *" successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 concurrencyPolicy: Forbid jobTemplate: spec: activeDeadlineSeconds: 60 template: metadata: labels: app: qbittorrentvpn-manage spec: restartPolicy: OnFailure containers: - name: qbittorrentvpn-manage image: ghcr.io/stuffanthings/qbit_manage:v4.6.5@sha256:4f36632a138b4e5aeab3b765b7f389087bfb140c80dbbec1343eca74dc351245 command: - python3 - qbit_manage.py - "--run" volumeMounts: - name: config mountPath: /config/config.yml subPath: config.yml volumes: - name: config secret: secretName: qbittorrentvpn-manage items: - key: config.yml path: config.yml --- apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: qbittorrentvpn-manage namespace: plex spec: target: name: qbittorrentvpn-manage deletionPolicy: Delete template: type: Opaque data: config.yml: |- {{ .trackertags }} qbt: host: https://qbittorrentvpn.lan.jibby.org user: {{ .username }} pass: {{ .password }} commands: recheck: false cat_update: false tag_update: true rem_unregistered: false rem_orphaned: false tag_tracker_error: false tag_nohardlinks: false share_limits: false skip_cleanup: false dry_run: false skip_qb_version_check: false # Not using any of these fields, but they're required for qbit_manage cat: tv-sonarr: Uncategorized completed: /not/using/cat recyclebin: enabled: false save_torrents: false split_by_category: false empty_after_x_days: directory: root_dir: /not/using/rootdir torrents_dir: orphaned: max_orphaned_files_to_delete: 50 min_file_age_minutes: 0 empty_after_x_days: exclude_patterns: settings: force_auto_tmm: false tracker_error_tag: issue nohardlinks_tag: noHL stalled_tag: stalledDL share_limits_tag: ~share_limit share_limits_min_seeding_time_tag: MinSeedTimeNotReached share_limits_min_num_seeds_tag: MinSeedsNotMet share_limits_last_active_tag: LastActiveLimitNotReached cat_filter_completed: true share_limits_filter_completed: true tag_nohardlinks_filter_completed: true rem_unregistered_filter_completed: false cat_update_all: true disable_qbt_default_share_limits: true tag_stalled_torrents: true rem_unregistered_grace_minutes: 10 rem_unregistered_max_torrents: 10 private_tag: force_auto_tmm_ignore_tags: [] rem_unregistered_ignore_list: [] webhooks: error: run_start: run_end: function: tag_tracker_error: share_limits: data: - secretKey: username sourceRef: storeRef: name: bitwarden-login kind: ClusterSecretStore remoteRef: key: 8dd7dfc3-800d-4af5-8a45-b23f0132806c property: username - secretKey: password sourceRef: storeRef: name: bitwarden-login kind: ClusterSecretStore remoteRef: key: 8dd7dfc3-800d-4af5-8a45-b23f0132806c property: password - secretKey: trackertags sourceRef: storeRef: name: bitwarden-notes kind: ClusterSecretStore remoteRef: key: 54c175aa-aa4f-4a28-a8f6-b3f80146e440 # Disabled for now (see suspend: true) # Sometimes VPN throughput slows down & a restart helps. Other times a restart # slows things down. --- apiVersion: batch/v1 kind: CronJob metadata: name: qbittorrentvpn-restart namespace: plex spec: suspend: true schedule: "*/30 * * * *" successfulJobsHistoryLimit: 1 failedJobsHistoryLimit: 1 concurrencyPolicy: Forbid jobTemplate: spec: template: metadata: labels: app: qbittorrentvpn-restart spec: serviceAccountName: qbittorrentvpn-restart-serviceaccount securityContext: runAsUser: 1000 runAsGroup: 1000 restartPolicy: OnFailure containers: - name: qbittorrentvpn-restart image: python:3.14 command: - python3 - -c - | import subprocess import json import pprint import urllib.parse import sys import datetime # Vars to configure namespace = 'plex' qparams = {'labelSelector': 'app=qbittorrentvpn'} max_runtime = datetime.timedelta(days=3) # serviceaccount/k8s specific vars. Likely don't need to edit these. serviceaccount_dir = '/var/run/secrets/kubernetes.io/serviceaccount' apiserver = 'https://kubernetes.default.svc' token = open(f'{serviceaccount_dir}/token').read() result = subprocess.run([ 'curl', '--cacert', f'{serviceaccount_dir}/ca.crt', '--header', f'Authorization: Bearer {token}', '-X', 'GET', f'{apiserver}/api/v1/namespaces/{namespace}/pods?{urllib.parse.urlencode(qparams)}' ], capture_output=True, check=True, ) pod_list = json.loads(result.stdout) items = pod_list.get('items') if items is None or len(items) < 1: print(f'No pod found? Exiting. {pod_list=}') sys.exit(1) if len(items) > 1: print(f'>1 pod? Exiting. {items=}, {len(items)=}') sys.exit(1) pod = items[0] container_statuses = pod['status']['containerStatuses'] if len(container_statuses) != 1: print(f'len(containerStatuses) != 1? Exiting. {container_statuses=}') sys.exit(1) running = container_statuses[0]['state'].get('running') if not running: print(f'Pod not running? Exiting. {container_statuses["state"]=}') started_at = datetime.datetime.fromisoformat(running["startedAt"]) runtime = datetime.datetime.now(tz=datetime.UTC) - started_at print(f'{runtime=} > {max_runtime=} ? {runtime > max_runtime}') if runtime > max_runtime: pod_name = pod['metadata']['name'] print(f'Deleting pod {pod_name}') result = subprocess.run([ 'curl', '--cacert', f'{serviceaccount_dir}/ca.crt', '--header', f'Authorization: Bearer {token}', '-X', 'DELETE', f'{apiserver}/api/v1/namespaces/{namespace}/pods/{pod_name}' ], capture_output=True, check=True, ) --- apiVersion: v1 kind: ServiceAccount metadata: name: qbittorrentvpn-restart-serviceaccount namespace: plex --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: qbittorrentvpn-restart-serviceaccount-edit namespace: plex roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: edit subjects: - kind: ServiceAccount name: qbittorrentvpn-restart-serviceaccount namespace: plex