A Kubernetes operator that manages qBittorrent Torrent instances through Custom Resource Definitions (CRDs). This operator allows you to declaratively manage Torrents in qBittorrent using native Kubernetes resources, so you are not forced to interact with the UI.
Check the Complete Setup Guide to build a full-functioning environment.
- How It Works
- Custom Resource Definition
- qBittorrent API Reference
- Installation
- Usage Examples
- Complete Setup Guide
- Configuration
- Monitoring
- Troubleshooting
The qBittorrent Operator introduces a new Custom Resource Definition (CRD) called Torrent that represents a torrent in your qBittorrent instance. The operator watches for changes to these Torrent resources and automatically:
- Creates torrents in qBittorrent when new
Torrentresources are created - Syncs status from qBittorrent back to the Kubernetes resource
- Removes torrents from qBittorrent when
Torrentresources are deleted - Maintains consistency between Kubernetes state and qBittorrent state
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ kubectl │ │ qBittorrent │ │ qBittorrent │
│ apply │───▶│ Operator │───▶│ Instance │
│ torrent.yaml │ │ (Controller) │ │ (Web API) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Kubernetes API │ │ Torrent │
│ (Torrent CRD) │ │ Downloads │
└──────────────────┘ └─────────────────┘
The operator follows the standard Kubernetes controller pattern:
- Watch: Monitors
Torrentresources for changes - Reconcile: Compares desired state (Kubernetes) with actual state (qBittorrent)
- Act: Makes API calls to qBittorrent to align states
- Update: Reports current status back to Kubernetes
The Torrent CRD defines the schema for managing torrents:
apiVersion: torrent.qbittorrent.io/v1alpha1
kind: Torrent
metadata:
name: my-torrent
namespace: media-server
spec:
magnet_uri: "magnet:?xt=urn:btih:example-hash"
status:
# Read-only fields populated by the operator
content_path: "/downloads/media/Example Torrent"
added_on: "1640995200"
state: "downloading"
total_size: 1073741824
name: "Example Torrent"
time_active: 3600
amount_left: 536870912
hash: "8c212779b4abde7c6bc608063a0d008b7e40ce32"
conditions:
- type: Available
status: "True"
reason: TorrentActive
message: "Torrent is active in qBittorrent"
lastTransitionTime: "2024-01-15T10:30:00Z"| Field | Type | Required | Description |
|---|---|---|---|
magnet_uri |
string | Yes | The magnet URI for the torrent to download |
| Field | Type | Description |
|---|---|---|
content_path |
string | Absolute path where torrent content is stored |
added_on |
string | Unix timestamp when torrent was added |
state |
string | Current torrent state (see Torrent States) |
total_size |
integer | Total size in bytes of all files in the torrent |
name |
string | Display name of the torrent |
time_active |
integer | Total active time in seconds |
amount_left |
integer | Bytes remaining to download |
hash |
string | Unique torrent hash identifier |
conditions |
array | Standard Kubernetes conditions array |
The state field can have the following values:
| State | Description |
|---|---|
downloading |
Torrent is actively downloading |
uploading |
Torrent is seeding (uploading to peers) |
pausedDL |
Download is paused |
pausedUP |
Upload/seeding is paused |
queuedDL |
Queued for download |
queuedUP |
Queued for upload |
stalledDL |
Download stalled (no peers) |
stalledUP |
Upload stalled (no peers) |
checkingDL |
Checking download integrity |
checkingUP |
Checking upload integrity |
error |
Error occurred |
missingFiles |
Torrent files are missing |
The operator uses the qBittorrent Web API v2. Key endpoints used:
POST /api/v2/auth/login- Authenticate and get session cookie
GET /api/v2/torrents/info- Get list of all torrentsPOST /api/v2/torrents/add- Add new torrent via magnet URIPOST /api/v2/torrents/delete- Remove torrent by hash
For complete API documentation, see: qBittorrent Web API
- Kubernetes cluster (v1.20+)
- kubectl configured
- Docker (for building images)
- Go 1.19+ (for development)
# Clone the repository
git clone https://github.com/guidonguido/qbittorrent-operator
cd qbittorrent-operator
# Deploy using kustomize
kubectl apply -k config/default/# Clone the repository
git clone https://github.com/guidonguido/qbittorrent-operator
cd qbittorrent-operator
# Install CRDs
make install
# Deploy the operator
make deploy IMG=controller:latest# Clone the repository
git clone https://github.com/guidonguido/qbittorrent-operator
cd qbittorrent-operator
# Generate single install file
make build-installer IMG=controller:latest
# Apply the generated manifest
kubectl apply -f dist/install.yamlVerify installation:
kubectl get pods -n qbittorrent-operator
kubectl get crd torrents.torrent.qbittorrent.iogit clone https://github.com/yourusername/qbittorrent-operator
cd qbittorrent-operator
# Build and deploy
make docker-build IMG=qbittorrent-operator:latest
make deploy IMG=qbittorrent-operator:latest# Create a torrent resource
apiVersion: torrent.qbittorrent.io/v1alpha1
kind: Torrent
metadata:
name: ubuntu-iso
namespace: media-server
spec:
magnet_uri: "magnet:?xt=urn:btih:ubuntu-22.04-desktop-amd64.iso"# Apply the torrent
kubectl apply -f torrent.yaml
# Check status
kubectl get torrents -n media-server
kubectl describe torrent ubuntu-iso -n media-server
# Delete torrent (removes from qBittorrent)
kubectl delete torrent ubuntu-iso -n media-serverapiVersion: torrent.qbittorrent.io/v1alpha1
kind: Torrent
metadata:
name: linux-distros
namespace: media-server
labels:
category: "operating-systems"
spec:
magnet_uri: "magnet:?xt=urn:btih:debian-12-amd64-netinst.iso"
---
apiVersion: torrent.qbittorrent.io/v1alpha1
kind: Torrent
metadata:
name: fedora-iso
namespace: media-server
labels:
category: "operating-systems"
spec:
magnet_uri: "magnet:?xt=urn:btih:fedora-39-x86_64-netinst.iso"# Watch torrent status in real-time
kubectl get torrents -n media-server -w
# Get detailed status
kubectl get torrent ubuntu-iso -n media-server -o yaml
# Check operator logs
kubectl logs -f deployment/qbittorrent-operator-controller-manager -n qbittorrent-operator-systemFirst, deploy qBittorrent with VPN protection:
# qbittorrent-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: media-server
---
# qbittorrent-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qbittorrent
namespace: media-server
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: media-pvc
namespace: media-server
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
# qbittorrent-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: qbittorrent
namespace: media-server
labels:
app: qbittorrent
part-of: qbittorrent
spec:
replicas: 1
selector:
matchLabels:
app: qbittorrent
part-of: qbittorrent
template:
metadata:
labels:
app: qbittorrent
part-of: qbittorrent
spec:
containers:
- name: qbittorrent
image: lscr.io/linuxserver/qbittorrent:5.1.1
ports:
- name: qbittorrent
containerPort: 8080
env:
- name: PUID
value: "0"
- name: PGID
value: "0"
- name: UMASK
value: "022"
volumeMounts:
- name: qbittorrent
mountPath: /config
- name: downloads-media
mountPath: /downloads/media
resources:
limits:
memory: 250Mi
requests:
memory: 200Mi
restartPolicy: Always
volumes:
- name: qbittorrent
persistentVolumeClaim:
claimName: qbittorrent
- name: downloads-media
persistentVolumeClaim:
claimName: media-pvc
---
# qbittorrent-service.yaml
apiVersion: v1
kind: Service
metadata:
name: qbittorrent
namespace: media-server
spec:
selector:
app: qbittorrent
part-of: qbittorrent
type: ClusterIP
ports:
- port: 8080
targetPort: qbittorrent- Port forward to access Web UI:
kubectl port-forward svc/qbittorrent 8080:8080 -n media-server-
Access Web UI: Open http://localhost:8080
- Default username:
admin - Get password from logs:
kubectl logs deployment/qbittorrent -n media-server
- Default username:
-
Configure qBittorrent:
- Go to Tools → Options → Web UI
- Set username/password for API access
- Note the credentials for operator configuration
- Create operator configuration:
# operator-config.yaml
apiVersion: v1
kind: Secret
metadata:
name: qbittorrent-secret
namespace: qbittorrent-operator
type: Opaque
data:
username: <base64-encoded-qbittorrent-username>
password: <base64-encoded-qbittorrent-password>
---
apiVersion: v1
kind: ConfigMap
metadata:
name: qbittorrent-config
namespace: qbittorrent-operator
data:
url: "http://qbittorrent.media-server.svc.cluster.local:8080"- Deploy the operator:
# Apply configuration
kubectl apply -f operator-config.yaml
# Deploy operator
make deploy IMG=qbittorrent-operator:latest# test-torrent.yaml
apiVersion: torrent.qbittorrent.io/v1alpha1
kind: Torrent
metadata:
name: test-torrent
namespace: media-server
spec:
magnet_uri: "magnet:?xt=urn:btih:ubuntu-22.04-desktop-amd64.iso"# Apply test torrent
kubectl apply -f test-torrent.yaml
# Monitor progress
kubectl get torrent test-torrent -n media-server -w
# Check in qBittorrent Web UI
# The torrent should appear automaticallyThe operator can be configured via environment variables:
| Variable | Description | Default |
|---|---|---|
QBITTORRENT_URL |
qBittorrent Web UI URL | Required |
QBITTORRENT_USERNAME |
qBittorrent username | Required |
QBITTORRENT_PASSWORD |
qBittorrent password | Required |
For optimal operation with the operator:
- Enable Web UI: Tools → Options → Web UI → Enable
- Set Authentication: Configure username/password
- Allow Cross-Origin: Set to allow API access
- Download Path: Configure default download location
The operator exposes Prometheus metrics on :8080/metrics:
controller_runtime_reconcile_total- Total reconciliationscontroller_runtime_reconcile_errors_total- Reconciliation errorscontroller_runtime_reconcile_time_seconds- Reconciliation duration
To enable Prometheus scraping of the operator metrics, follow these steps:
Uncomment the Prometheus resources in config/default/kustomization.yaml:
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
- ../prometheusEdit config/prometheus/monitor.yaml to match your Prometheus setup:
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
release: prometheus-stack # Match your Prometheus operator release
control-plane: controller-manager
app.kubernetes.io/name: qbittorrent-operator
app.kubernetes.io/managed-by: kustomize
name: controller-manager-metrics-monitor
namespace: system
spec:
endpoints:
- path: /metrics
interval: 30s # Scrape interval
scrapeTimeout: 30s # Scrape timeout
port: http # Port name from metrics service
scheme: http # HTTP (not HTTPS)
bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
honorLabels: true # Preserve metric labels
namespaceSelector:
matchNames:
- observability # Target namespace for monitoring
selector:
matchLabels:
control-plane: controller-manager
app.kubernetes.io/name: qbittorrent-operatorFor kube-prometheus-stack:
metadata:
labels:
release: prometheus-stack # or your Helm release nameFor different Prometheus namespace:
namespaceSelector:
matchNames:
- your-prometheus-namespace # e.g., monitoring, prometheus, observabilityFor Prometheus without namespace selector:
# Remove namespaceSelector entirely for cluster-wide discovery
namespaceSelector: {}# Deploy the operator with ServiceMonitor
kubectl apply -k config/default
# Verify ServiceMonitor creation
kubectl get servicemonitor -n qbittorrent-operator-system
# Check if Prometheus discovers the target
kubectl port-forward svc/prometheus-operated 9090:9090 -n observability
# Then visit http://localhost:9090/targetsOnce deployed, verify that Prometheus is collecting metrics:
-
Check Targets: In Prometheus UI → Status → Targets
- Look for
qbittorrent-operator-controller-manager-metrics-monitor - Status should be "UP"
- Look for
-
Query Metrics: Try these queries in Prometheus:
# Controller reconciliation rate rate(controller_runtime_reconcile_total[5m]) # Error rate rate(controller_runtime_reconcile_errors_total[5m]) # Reconciliation duration histogram_quantile(0.95, rate(controller_runtime_reconcile_time_seconds_bucket[5m]))
Operator logs include structured information:
# View operator logs
kubectl logs -f deployment/qbittorrent-operator-controller-manager -n qbittorrent-operator-system
# Increase log verbosity
kubectl patch deployment qbittorrent-operator-controller-manager -n qbittorrent-operator-system -p '{"spec":{"template":{"spec":{"containers":[{"name":"manager","args":["--leader-elect","--health-probe-bind-address=:8081","--v=2"]}]}}}}'The operator provides health endpoints:
/healthz- Liveness probe/readyz- Readiness probe (includes qBittorrent connectivity)
# Check operator logs
kubectl logs deployment/qbittorrent-operator-controller-manager -n qbittorrent-operator-system
# Common causes:
# - Wrong URL in configuration
# - qBittorrent not running
# - Network policies blocking access
# - Incorrect credentials# Check torrent resource status
kubectl describe torrent <torrent-name> -n <namespace>
# Look for conditions:
# - Available: True = Working correctly
# - Degraded: True = Error occurred (check message)# Check reconciliation frequency
kubectl get torrent <torrent-name> -n <namespace> -o yaml
# Status should update every 30 seconds
# If not updating, check operator logs for errors# List all torrents across namespaces
kubectl get torrents --all-namespaces
# Get detailed torrent information
kubectl get torrent <name> -n <namespace> -o yaml
# Check operator events
kubectl get events -n qbittorrent-operator-system
# Port forward to qBittorrent for direct access
kubectl port-forward svc/qbittorrent 8080:8080 -n media-serverLook for these log patterns:
# Successful reconciliation
"Successfully logged into qBittorrent"
"Torrent reconciliation completed"
# Connection issues
"Failed to login to qBittorrent"
"Failed to get torrents info list"
# API errors
"Unauthorized access to qbittorrent"
"Failed to add torrent to qBittorrent"
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.