--- 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 #configMap: # name: qbittorrentvpn-manage-config # configMap: # 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 # Restart qbittorrentvpn reguarly. Sometimes VPN throughput slows down & a restart helps. #--- #apiVersion: batch/v1 #kind: CronJob #metadata: # name: qbittorrentvpn-restart # namespace: plex #spec: # 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