Qdrant Operator
A Kubernetes operator for managing Qdrant vector database clusters
Documentation • Features • Installation • Usage • Configuration
Features
- Cluster Management - Deploy and manage Qdrant clusters via Helm
- Automated Backups - Per-node snapshots streamed to S3-compatible storage, with a manifest
- Scheduled Backups - CronJob-style schedules with concurrency and retention policies
- Disaster Recovery - Restore collections node-by-node from presigned URLs, with optional remapping
- Declarative Collections -
QdrantCollectionkeeps vectors, payload indexes and aliases in Git and re-creates what was deleted by hand - Least-Privilege Tokens -
QdrantAccessKeysigns Qdrant JWT tokens per collection and writes them into a Secret, renewing before expiry - Live Migrations -
QdrantMigrationcopies collections between clusters (or from Qdrant Cloud) without an S3 round trip - Admission Validation - CEL rules on every CRD reject impossible specs before the operator sees them
- Async-First - Built on kopf with fully async adapters for performance
Installation
Prerequisites
- Kubernetes cluster (v1.26+)
- Helm 3.x installed
- kubectl configured with cluster access
Install with Helm (Recommended)
# From the OCI registry (published on every release)
helm install qdrant-operator oci://ghcr.io/4thel00z/charts/qdrant-operator -n qdrant-system --create-namespace
# Or from the local chart
helm install qdrant-operator ./charts/qdrant-operator
# Or install in a specific namespace
helm install qdrant-operator ./charts/qdrant-operator -n qdrant-system --create-namespace
# Verify installation
kubectl get pods -l app.kubernetes.io/name=qdrant-operator
Upgrading
Helm installs the CRDs in crds/ once and never updates them. Before helm upgrade, apply the
CRDs of the new version (they are attached to every GitHub release):
kubectl apply -f manifests/crds/
helm upgrade qdrant-operator oci://ghcr.io/4thel00z/charts/qdrant-operator -n qdrant-system
Configuration
Override default values:
helm install qdrant-operator ./charts/qdrant-operator \
--set operator.logLevel=DEBUG \
--set resources.limits.memory=512Mi
Or use a values file:
helm install qdrant-operator ./charts/qdrant-operator -f my-values.yaml
Install from PyPI
The operator is also a Python package; the qdrant-operator command runs it against your kubeconfig,
forwarding any extra flags to kopf run (for example --namespace team-a).
uv tool install qdrant-operator # or: pipx install qdrant-operator
qdrant-operator --help
Container images: ghcr.io/4thel00z/qdrant-operator:<version> (linux/amd64, linux/arm64, signed provenance).
Build Docker Image (Optional)
# Replace with your registry
docker build -t ghcr.io/YOUR_ORG/qdrant-operator:0.1.0 .
docker push ghcr.io/YOUR_ORG/qdrant-operator:0.1.0
Local Development
# Install dependencies
uv sync
# Apply CRDs to cluster
kubectl apply -f manifests/crds/
# Run operator locally against your kubeconfig
make dev
Usage
Create a Qdrant Cluster
apiVersion: qdrant.io/v1alpha1
kind: QdrantCluster
metadata:
name: my-qdrant
namespace: default
spec:
replicas: 3
version: v1.16.3
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
persistence:
size: 10Gi
storageClassName: standard
cluster:
enabled: true
Create a Backup
apiVersion: qdrant.io/v1alpha1
kind: QdrantBackup
metadata:
name: my-backup
namespace: default
spec:
clusterRef:
name: my-qdrant
storage:
s3:
bucket: my-backups
prefix: qdrant
region: us-east-1
credentialsSecretRef:
name: s3-credentials
accessKeyIdKey: AWS_ACCESS_KEY_ID
Schedule Automated Backups
apiVersion: qdrant.io/v1alpha1
kind: QdrantBackupSchedule
metadata:
name: daily-backup
namespace: default
spec:
schedule: "0 2 * * *" # Daily at 2 AM
clusterRef:
name: my-qdrant
storage:
s3:
bucket: my-backups
prefix: scheduled
region: us-east-1
credentialsSecretRef:
name: s3-credentials
accessKeyIdKey: AWS_ACCESS_KEY_ID
retentionPolicy:
keepLast: 7
keepDaily: 30
concurrencyPolicy: Forbid
Restore from Backup
apiVersion: qdrant.io/v1alpha1
kind: QdrantRestore
metadata:
name: my-restore
namespace: default
spec:
backupRef:
name: my-backup
targetClusterRef:
name: my-qdrant-new
collectionMapping:
old_collection: new_collection
Declare a Collection
apiVersion: qdrant.io/v1alpha1
kind: QdrantCollection
metadata:
name: articles
namespace: default
spec:
clusterRef:
name: my-qdrant
collectionName: Articles # optional; Qdrant names may use characters Kubernetes names cannot
vectors:
- name: text
size: 768
distance: Cosine
memory: cold # Qdrant 1.19+; use onDisk: true on older releases
- name: image
size: 512
distance: Dot
sparseVectors:
- name: bm25
modifier: idf
replicationFactor: 2
optimizers: # Qdrant's own field names, passed through
indexing_threshold: 10000
payloadIndexes:
- field: tenant_id
type: keyword
params: {is_tenant: true}
- field: body
type: text
params: {tokenizer: word, lowercase: true}
aliases: [articles-live]
metadata: {owner: search-team} # merged into the collection's metadata
deletionPolicy: Retain # Delete drops the collection with the resource
Custom sharding declares its keys in the same resource; keys are created when missing and never dropped, because dropping one deletes its points:
spec:
shardingMethod: custom
shardNumber: 1 # shards per key
shardKeys:
- key: eu
- key: us
replicationFactor: 2
A single entry without name is Qdrant's unnamed vector. Vector size and distance, shardNumber
and shardingMethod cannot change on a live collection; a mismatch puts the resource into
phase: Degraded with a Ready=False condition instead of silently ignoring the spec. Everything
else is patched in place, and only fields you declared are compared. Indexes and aliases the
operator did not create are left alone.
Issue a Scoped Token
Requires spec.apiKey.jwtRbac: true on the cluster (tokens are signed with the cluster's API key).
apiVersion: qdrant.io/v1alpha1
kind: QdrantAccessKey
metadata:
name: billing-reader
namespace: default
spec:
clusterRef:
name: my-qdrant
collections: # or: access: r | m for cluster-wide tokens
- name: invoices
access: r # r, rw, or prw (points only, no snapshots or indexes)
subject: billing-service
ttl: 720h
renewBefore: 24h # default: a third of ttl
secretName: billing-qdrant # default: metadata.name
The Secret gets token and url keys and is garbage-collected with the resource. The token is
re-issued when the spec changes, the cluster's API key rotates, the Secret disappears, or
renewBefore is reached.
Migrate Between Clusters
apiVersion: qdrant.io/v1alpha1
kind: QdrantMigration
metadata:
name: cloud-to-k8s
namespace: default
spec:
source:
endpoint: # or: clusterRef: {name: old-qdrant}
url: https://xyz.eu-central.aws.cloud.qdrant.io:6333
apiKeySecretRef: {name: qdrant-cloud, key: api-key}
targetClusterRef:
name: my-qdrant
collections: [articles] # empty = all
collectionMapping:
articles: articles_v2
batchSize: 500
batchDelayMs: 0 # throttle
ensurePayloadIndexes: true # also on targets that already exist
target: # for collections the migration creates
replicationFactor: 2
# shardKey: tier-a # route every point to one shard key
# shardKeyField: tenant # or route by a payload field
Points are scrolled from the source and upserted on the target, so the run is idempotent and a
one-node source can become a three-node target (shard counts follow the target unless set under
target). Missing target collections are created from the source configuration, including
payload indexes; existing targets must have the same vector sizes and distances. On a
custom-sharded target, each point keeps its source shard key (keys are created as they are met)
unless routed with target.shardKey / target.shardKeyField; an auto-sharded target receives
the points without shard keys. Progress and the scroll offset live in status, so a
run interrupted by an operator restart resumes where it stopped.
Configuration
Environment Variables
| Variable | Description | Default |
|---|---|---|
KUBECONFIG |
Path to kubeconfig file | In-cluster config |
LOG_LEVEL |
Logging level | INFO |
LOG_FORMAT |
json or text |
json |
S3 Credentials Secret
apiVersion: v1
kind: Secret
metadata:
name: s3-credentials
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: "your-access-key"
AWS_SECRET_ACCESS_KEY: "your-secret-key"
Development
# Install dependencies
uv sync
# Run tests
uv run pytest
# Run integration tests (requires k8s cluster + helm)
make test-integration
# Lint and format
uv run ruff check src tests
uv run ruff format src tests
# Type checking
uv run pyright src tests
Releasing
CI (.github/workflows/ci.yml) runs lint, pyright, unit tests, chart lint, a wheel/image build and the
kind-based end-to-end suite on every push and pull request.
Releases are driven by release-please from the
conventional commit history, all inside .github/workflows/release.yml on pushes to main:
- A release PR is kept up to date that bumps
pyproject.tomlandChart.yaml, refreshesuv.lockand updatesCHANGELOG.md. - Merging that PR creates the
vX.Y.Ztag and the GitHub release, and the same run publishes the wheel and sdist to PyPI (trusted publishing, no token), pushes the multi-arch image and the Helm chart to ghcr.io, and attaches the artifacts and CRDs to the release.
No secrets are involved beyond the default GITHUB_TOKEN; the repository setting Allow GitHub
Actions to create and approve pull requests must be on for the release PR to appear.
License
MIT License - see LICENSE for details.
Metadata
Release files for qdrant-operator 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qdrant_operator-0.3.1.tar.gz | 40.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qdrant_operator-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 85.9 kB
Release files / qdrant_operator-0.3.1.tar.gz
| Download URL | qdrant_operator-0.3.1.tar.gz |
|---|---|
| Size | 40.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e9d59e61efae1806ecd1506b8f4355eeeb1d7a7d0120fe1a2ae674cd4cb52fc3
|
|
BLAKE2b-256 checksum How to use checksums |
9f01f563fab0f273a36e8a97778fb9000f25ca066701c9cb1c72b575162a99e2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / qdrant_operator-0.3.1-py3-none-any.whl
| Download URL | qdrant_operator-0.3.1-py3-none-any.whl |
|---|---|
| Size | 45.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c354a45c5be472dcc30aed0f0c94f15bd10f58b79a80bf1ce45ab7eb864f2615
|
|
BLAKE2b-256 checksum How to use checksums |
34e7506cac361446180007430c1f28bb81cacfc1350e9033aeef3995285ea39d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log