Sync torrents between qBittorrent instances. Stream pieces from a source server to a destination server in real-time as they download.
go install github.com/arsac/qb-sync/cmd/qbsync@latestOr with Docker:
docker pull ghcr.io/arsac/qb-sync:latestStreams pieces from source qBittorrent to destination server:
qbsync source \
--data /downloads \
--qb-url http://localhost:8080 \
--destination-addr 192.168.1.100:50051Receives pieces and adds verified torrents to destination qBittorrent:
qbsync destination \
--data /downloads \
--qb-url http://localhost:8080 \
--listen :50051All flags can be set via environment variables with the prefix QBSYNC_SOURCE_ or QBSYNC_DESTINATION_.
| Variable | Flag | Description | Default |
|---|---|---|---|
QBSYNC_SOURCE_DATA |
--data |
Data directory path | (required) |
QBSYNC_SOURCE_QB_URL |
--qb-url |
qBittorrent WebUI URL | (required) |
QBSYNC_SOURCE_QB_USERNAME |
--qb-username |
qBittorrent username | |
QBSYNC_SOURCE_QB_PASSWORD |
--qb-password |
qBittorrent password | |
QBSYNC_SOURCE_DESTINATION_ADDR |
--destination-addr |
Destination server gRPC address | (required) |
QBSYNC_SOURCE_MIN_SPACE |
--min-space |
Min free space (GB) before syncing | 50 |
QBSYNC_SOURCE_MIN_SEEDING_TIME |
--min-seeding-time |
Min seeding time (seconds) | 3600 |
QBSYNC_SOURCE_SLEEP |
--sleep |
Sleep interval between checks (seconds) | 30 |
QBSYNC_SOURCE_RATE_LIMIT |
--rate-limit |
Max bytes/sec (0 = unlimited) | 0 |
QBSYNC_SOURCE_PIECE_TIMEOUT |
--piece-timeout |
Timeout for stale in-flight pieces (seconds) | 60 |
QBSYNC_SOURCE_RECONNECT_MAX_DELAY |
--reconnect-max-delay |
Max reconnect backoff delay (seconds) | 30 |
QBSYNC_SOURCE_NUM_SENDERS |
--num-senders |
Concurrent sender workers | 4 |
QBSYNC_SOURCE_MIN_CONNECTIONS |
--min-connections |
Minimum TCP connections to destination server | 2 |
QBSYNC_SOURCE_MAX_CONNECTIONS |
--max-connections |
Maximum TCP connections to destination server | 8 |
QBSYNC_SOURCE_SYNCED_TAG |
--synced-tag |
Tag for synced torrents (empty to disable) | synced |
QBSYNC_SOURCE_SOURCE_REMOVED_TAG |
--source-removed-tag |
Tag on destination when source removed (empty to disable) | source-removed |
QBSYNC_SOURCE_EXCLUDE_CLEANUP_TAG |
--exclude-cleanup-tag |
Tag that prevents cleanup from source (empty to disable) | |
QBSYNC_SOURCE_DRAIN_ANNOTATION |
--drain-annotation |
Pod annotation key to gate shutdown drain (empty to drain unconditionally) | qbsync/drain |
QBSYNC_SOURCE_DRAIN_TIMEOUT |
--drain-timeout |
Shutdown drain timeout (seconds) | 300 |
QBSYNC_SOURCE_HEALTH_ADDR |
--health-addr |
Health/metrics endpoint | :8080 |
QBSYNC_SOURCE_LOG_LEVEL |
--log-level |
Log level: debug, info, warn, error | info |
QBSYNC_SOURCE_DRY_RUN |
--dry-run |
Run without making changes | false |
| Variable | Flag | Description | Default |
|---|---|---|---|
QBSYNC_DESTINATION_DATA |
--data |
Data directory path | (required) |
QBSYNC_DESTINATION_LISTEN |
--listen |
gRPC listen address | :50051 |
QBSYNC_DESTINATION_SAVE_PATH |
--save-path |
Save path as qBittorrent sees it | (defaults to --data) |
QBSYNC_DESTINATION_QB_URL |
--qb-url |
qBittorrent WebUI URL | |
QBSYNC_DESTINATION_QB_USERNAME |
--qb-username |
qBittorrent username | |
QBSYNC_DESTINATION_QB_PASSWORD |
--qb-password |
qBittorrent password | |
QBSYNC_DESTINATION_POLL_INTERVAL |
--poll-interval |
Verification poll interval (seconds) | 2 |
QBSYNC_DESTINATION_POLL_TIMEOUT |
--poll-timeout |
Verification timeout (seconds) | 300 |
QBSYNC_DESTINATION_STREAM_WORKERS |
--stream-workers |
Concurrent piece writers (0 = auto: 8) | 0 |
QBSYNC_DESTINATION_MAX_STREAM_BUFFER |
--max-stream-buffer |
Global memory budget for buffered pieces (MB) | 512 |
QBSYNC_DESTINATION_SYNCED_TAG |
--synced-tag |
Tag for synced torrents (empty to disable) | synced |
QBSYNC_DESTINATION_HEALTH_ADDR |
--health-addr |
Health/metrics endpoint | :8080 |
QBSYNC_DESTINATION_LOG_LEVEL |
--log-level |
Log level: debug, info, warn, error | info |
QBSYNC_DESTINATION_DRY_RUN |
--dry-run |
Run without making changes | false |
These standard variables are also supported as fallbacks:
| Variable | Used For |
|---|---|
HTTP_PORT / HEALTH_PORT |
Health/metrics endpoint |
GRPC_PORT / PORT |
Destination server listen address |
Both servers expose HTTP endpoints on the health address:
| Endpoint | Description |
|---|---|
/healthz |
Basic liveness check |
/livez |
Liveness with dependency checks |
/readyz |
Readiness for traffic |
/metrics |
Prometheus metrics |
When the source server receives SIGTERM, it can drain fully-synced torrents from the source qBittorrent before exiting. This is useful during node maintenance to migrate workloads off a node gracefully.
Drain is gated by a pod annotation. On SIGTERM, the source server checks the annotation via the Kubernetes API:
- Annotation set to
"true": drain proceeds, removing fully-synced torrents from source - Annotation missing,
"false", or any other value: drain is skipped - Annotation check fails (K8s API unreachable): drain is skipped (fail-closed)
- No
--drain-annotationconfigured (empty string): drain runs unconditionally on SIGTERM
The drain annotation check reads the pod's own metadata via the Kubernetes API. The service account needs get on pods in its namespace:
apiVersion: v1
kind: ServiceAccount
metadata:
name: qbsync-source
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: qbsync-source
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: qbsync-source
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: qbsync-source
subjects:
- kind: ServiceAccount
name: qbsync-sourcemetadata:
annotations:
qbsync/drain: "false" # Set to "true" before node maintenance
spec:
serviceAccountName: qbsync-source
terminationGracePeriodSeconds: 600
containers:
- name: qbsync-source
args: ["source", "--drain-annotation=qbsync/drain", "--drain-timeout=480"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace- Set the annotation:
kubectl annotate pod <pod> qbsync/drain=true --overwrite - Evict or delete the pod (SIGTERM is sent)
- Source server checks the annotation, drains synced torrents, then exits
Ensure terminationGracePeriodSeconds exceeds --drain-timeout so the kubelet doesn't SIGKILL before drain completes.
services:
qbsync-source:
image: ghcr.io/arsac/qb-sync:latest
command: source
environment:
QBSYNC_SOURCE_DATA: /downloads
QBSYNC_SOURCE_QB_URL: http://qbittorrent-source:8080
QBSYNC_SOURCE_DESTINATION_ADDR: qbsync-destination:50051
volumes:
- /path/to/source/downloads:/downloads:ro
qbsync-destination:
image: ghcr.io/arsac/qb-sync:latest
command: destination
environment:
QBSYNC_DESTINATION_DATA: /downloads
QBSYNC_DESTINATION_QB_URL: http://qbittorrent-destination:8080
volumes:
- /path/to/destination/downloads:/downloadsMIT