How to Install Qdrant on Ubuntu 24.04 VPS: Self-Hosted Vector Database for AI & RAG
Vector databases have become the backbone of modern AI applications. Whether you are building a retrieval-augmented generation (RAG) pipeline, a semantic search engine, a recommendation system, or an agent that recalls past conversations, you need a fast, scalable store for high-dimensional embeddings. Qdrant is one of the most performant open-source vector databases available, and running it on your own VPS eliminates the eye-watering per-vector pricing of managed services like Pinecone.
This guide walks you through installing Qdrant on Ubuntu 24.04 LTS, from the initial SSH connection to a hardened production deployment with TLS, API-key authentication, payload indexing, hybrid dense/sparse search, and automated snapshot backups.
Recommended plan: CloudCore Professional — 6 vCPU, 12 GB RAM, 100 GB NVMe. Enough headroom to index tens of millions of embeddings with HNSW and scalar quantization enabled.
Table of Contents
config.yaml/dashboard)What is Qdrant?
Qdrant is an open-source, Rust-written vector similarity search engine. It indexes high-dimensional vectors (typically embeddings produced by models like text-embedding-3-small, nomic-embed-text, or bge-large) and lets you find the nearest neighbours to a query vector in milliseconds, even across hundreds of millions of items.
Under the hood, Qdrant uses HNSW (Hierarchical Navigable Small World) graphs for approximate nearest-neighbour search, supports scalar, product, and binary quantization to reduce memory footprint by up to 32x, and offers dense vectors, sparse vectors (for BM25-style lexical matching), multi-vector representations (ColBERT-style late interaction), and hybrid search combining all of them through Query API fusion.
Qdrant is written in Rust, runs as a single static binary, exposes both a REST API (port 6333) and a gRPC API (port 6334), and ships with a built-in web dashboard for visual collection management. It supports horizontal scaling through sharding and replication, and handles payload filtering with full-text search, geo-radius queries, numeric ranges, and boolean logic — all co-located with the vector index for sub-millisecond filtered search.
Typical use cases include RAG pipelines feeding local LLMs like Ollama, semantic code search, image and multimodal retrieval, recommendation systems, deduplication, anomaly detection, and long-term memory for AI agents.
Why Self-Host Qdrant Instead of Using Pinecone?
Managed vector databases are convenient — until the invoice arrives. Pinecone's pricing scales with vector count, query volume, and pod replicas. For a modest RAG workload of 5 million vectors at 768 dimensions with moderate query traffic, you can easily spend $300–$1,000+ per month on a managed tier. Self-hosting Qdrant on a single CloudCore Professional VPS handles the same workload for a flat EUR 19.99/month, with no per-query or per-vector fees.
| Scenario | Pinecone Managed | Qdrant Cloud (Managed) | Self-Hosted Qdrant on VPS |
|---|---|---|---|
| 1M vectors (768 dim) | ~$70/mo (Standard) | ~$25–$70/mo | EUR 19.99/mo |
| 10M vectors (768 dim) | ~$300–$500/mo | ~$150–$300/mo | EUR 19.99/mo |
| 50M vectors (768 dim) | ~$1,500+/mo | ~$700+/mo | EUR 39.99/mo (larger plan) |
| Data sovereignty / GDPR | Vendor-controlled | Vendor-controlled | Full control, EU-hosted |
| Egress fees | Yes | Yes | None |
| Rate limits | Yes (QPS cap) | Yes | Hardware-limited only |
| Custom quantization & HNSW tuning | Limited | Limited | Full control |
| Offline / air-gapped deployment | No | No | Yes |
hnsw_config.ef_construct, m, custom quantization profiles, memory-mapped storage thresholds, and per-collection replication. Your embeddings (which often encode sensitive internal documents, customer data, or proprietary code) never leave your server.Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- SSH access to your server.
- At least 4 GB of RAM (8 GB+ recommended for production; 12 GB+ for multi-million-vector workloads).
- At least 20 GB of free disk space (vector storage grows with collection size — budget roughly 4 GB per 1 M × 768-dim vectors with scalar quantization).
- Ports 6333 (REST) and 6334 (gRPC) available on the server.
Recommended Plan: CloudCore Professional>
For production RAG and semantic search workloads, the CloudCore Professional plan is the sweet spot:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This handles 10–20 M vectors at 768 dimensions with HNSW + scalar quantization while leaving room for an embedding model running alongside.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl wget ca-certificates gnupg lsb-release ufwIf your kernel was updated, reboot:
sudo rebootStep 2: Install Qdrant (Binary + systemd)
Qdrant ships a single statically-linked Rust binary. This is the lightest, fastest way to run Qdrant on a VPS — no container overhead, no extra runtime.
2.1 Create a dedicated system user and directories
sudo useradd --system --home /var/lib/qdrant --shell /usr/sbin/nologin qdrant
sudo mkdir -p /var/lib/qdrant/storage /var/lib/qdrant/snapshots /etc/qdrant
sudo chown -R qdrant:qdrant /var/lib/qdrant2.2 Download the latest release
Fetch the latest release tag from GitHub and download the Linux x86_64-unknown-linux-gnu tarball:
QDRANT_VERSION=$(curl -s https://api.github.com/repos/qdrant/qdrant/releases/latest | grep '"tag_name"' | cut -d'"' -f4) echo "Installing Qdrant ${QDRANT_VERSION}"
cd /tmp wget "https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSION}/qdrant-x86_64-unknown-linux-gnu.tar.gz" tar -xzf qdrant-x86_64-unknown-linux-gnu.tar.gz sudo mv qdrant /usr/local/bin/qdrant sudo chmod +x /usr/local/bin/qdrant
Verify:
/usr/local/bin/qdrant --versionExpected output:
qdrant 1.12.42.3 Fetch the static web UI assets
Qdrant's /dashboard UI is distributed separately as a dist bundle. Download it into /var/lib/qdrant/static:
cd /tmp
wget "https://github.com/qdrant/qdrant-web-ui/releases/latest/download/dist-qdrant.zip"
sudo apt install -y unzip
sudo unzip -o dist-qdrant.zip -d /var/lib/qdrant/
sudo mv /var/lib/qdrant/dist /var/lib/qdrant/static
sudo chown -R qdrant:qdrant /var/lib/qdrant/static2.4 Create the systemd unit
sudo tee /etc/systemd/system/qdrant.service > /dev/null <<'EOF' [Unit] Description=Qdrant Vector Database After=network.target[Service] Type=simple User=qdrant Group=qdrant WorkingDirectory=/var/lib/qdrant ExecStart=/usr/local/bin/qdrant --config-path /etc/qdrant/config.yaml Restart=on-failure RestartSec=5 LimitNOFILE=1048576 LimitNPROC=infinity
Hardening
NoNewPrivileges=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/qdrant PrivateTmp=true
[Install] WantedBy=multi-user.target EOF
Do not start the service yet — we still need to write config.yaml in Step 3.
Step 2 (Alternative): Install Qdrant with Docker
If you prefer containers (or already run a Docker-based stack), you can skip the binary install and run Qdrant via Docker instead.
Install Docker
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USERLog out and back in so your user picks up the docker group.
Run Qdrant
Create host directories for persistent state:
sudo mkdir -p /opt/qdrant/storage /opt/qdrant/snapshots /opt/qdrant/configWrite a minimal config.yaml (we will flesh this out in Step 3):
sudo tee /opt/qdrant/config/config.yaml > /dev/null <<'EOF'
service:
host: 0.0.0.0
http_port: 6333
grpc_port: 6334
storage:
storage_path: /qdrant/storage
snapshots_path: /qdrant/snapshots
EOFRun the container:
docker run -d \
--name qdrant \
--restart unless-stopped \
-p 127.0.0.1:6333:6333 \
-p 127.0.0.1:6334:6334 \
-v /opt/qdrant/storage:/qdrant/storage \
-v /opt/qdrant/snapshots:/qdrant/snapshots \
-v /opt/qdrant/config/config.yaml:/qdrant/config/production.yaml \
qdrant/qdrant:latestBinding to 127.0.0.1 keeps Qdrant off the public internet — we will front it with Nginx + TLS in Step 9.
The rest of this guide assumes the binary + systemd path, but every REST call and config snippet works identically against the Docker container.
Step 3: Write a Production config.yaml
Qdrant's defaults are sensible for development but not for production. Write a dedicated config file at /etc/qdrant/config.yaml:
sudo tee /etc/qdrant/config.yaml > /dev/null <<'EOF' log_level: INFOservice: host: 127.0.0.1 http_port: 6333 grpc_port: 6334 max_request_size_mb: 64 max_workers: 0 # 0 = auto (one per CPU core) enable_cors: true # API key protection — generate with: openssl rand -hex 32 api_key: CHANGE_ME_TO_A_LONG_RANDOM_STRING # Read-only key for dashboards / analytics clients read_only_api_key: CHANGE_ME_READONLY_KEY static_content_dir: /var/lib/qdrant/static
storage: storage_path: /var/lib/qdrant/storage snapshots_path: /var/lib/qdrant/snapshots temp_path: /var/lib/qdrant/temp on_disk_payload: true # Keep large payloads on disk, not RAM performance: max_search_threads: 0 # 0 = auto max_optimization_threads: 2 optimizers: deleted_threshold: 0.2 vacuum_min_vector_number: 1000 default_segment_number: 0 # 0 = auto based on CPU count flush_interval_sec: 5 max_segment_size_kb: null memmap_threshold_kb: 200000 # Memory-map segments above ~200 MB indexing_threshold_kb: 20000 # Build HNSW for segments above ~20 MB hnsw_index: m: 16 # Graph connectivity — higher = better recall, more RAM ef_construct: 100 # Build-time search depth full_scan_threshold_kb: 10000 # Fall back to flat scan below this size on_disk: false # Keep HNSW graph in RAM for speed
cluster: enabled: false # Single-node for most VPS deployments # To enable clustering across multiple nodes: # enabled: true # p2p: # port: 6335 # consensus: # tick_period_ms: 100
telemetry_disabled: true # Disable anonymous usage telemetry EOF
Generate strong API keys and replace the placeholders:
echo "Full-access key: $(openssl rand -hex 32)"
echo "Read-only key: $(openssl rand -hex 32)"
sudo nano /etc/qdrant/config.yamlLock down permissions (the file contains secrets):
sudo chown qdrant:qdrant /etc/qdrant/config.yaml
sudo chmod 640 /etc/qdrant/config.yamlCreate the temp directory and start the service:
sudo mkdir -p /var/lib/qdrant/temp sudo chown qdrant:qdrant /var/lib/qdrant/temp
sudo systemctl daemon-reload sudo systemctl enable --now qdrant
Step 4: Verify the Installation
Check the service status:
sudo systemctl status qdrantExpected:
● qdrant.service - Qdrant Vector Database
Loaded: loaded (/etc/systemd/system/qdrant.service; enabled)
Active: active (running) since Thu 2026-04-16 09:15:02 UTC; 10s agoStream the logs:
sudo journalctl -u qdrant -fLook for a line similar to:
INFO Qdrant HTTP listening on 6333
INFO Qdrant gRPC listening on 6334
INFO Web UI available at /dashboardNow hit the API (replacing YOUR_API_KEY with the key you generated):
export QDRANT_API_KEY="YOUR_API_KEY"
curl -s -H "api-key: ${QDRANT_API_KEY}" http://127.0.0.1:6333/ | head
Expected:
{"title":"qdrant - vector search engine","version":"1.12.4","commit":"..."}List collections (none yet):
curl -s -H "api-key: ${QDRANT_API_KEY}" http://127.0.0.1:6333/collections{"result":{"collections":[]},"status":"ok","time":0.000012}Step 5: Create Your First Collection (HNSW + Quantization)
A collection in Qdrant is a namespace for points (vector + payload). When you create a collection you declare the vector size, distance metric, HNSW parameters, and optional quantization.
For a typical RAG workload using OpenAI text-embedding-3-small (1536 dimensions, cosine distance) with scalar quantization to cut memory usage by 4x:
curl -s -X PUT \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents \
-d '{
"vectors": {
"size": 1536,
"distance": "Cosine",
"on_disk": false
},
"hnsw_config": {
"m": 16,
"ef_construct": 128,
"full_scan_threshold": 10000
},
"quantization_config": {
"scalar": {
"type": "int8",
"quantile": 0.99,
"always_ram": true
}
},
"optimizers_config": {
"indexing_threshold": 20000,
"memmap_threshold": 200000
},
"on_disk_payload": true
}'Expected:
{"result":true,"status":"ok","time":0.045}Understanding the knobs
size— Dimensionality of your embedding model (768 fornomic-embed-text, 1024 forbge-large, 1536 fortext-embedding-3-small, 3072 fortext-embedding-3-large).distance—Cosine,Dot,Euclid, orManhattan. Cosine is the default for normalized text embeddings.hnsw_config.m— Number of bidirectional links per node. Higher = better recall but more RAM. Defaults to 16.hnsw_config.ef_construct— Search depth during index build. Higher = better graph quality, slower indexing. 100–200 is the typical range.quantization_config.scalar— Compressesfloat32vectors intoint8, cutting RAM by 4x with minimal recall loss.always_ram: truekeeps the quantized vectors in memory for fast first-stage scoring; original vectors can stay on disk for rescoring.on_disk_payload: true— Keeps JSON payload on disk rather than loading every field into RAM. Essential for collections with large text fields.
Sparse vectors for hybrid search
To add a sparse vector index (for BM25-style lexical matching) to the same collection, update it:
curl -s -X PATCH \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents \
-d '{
"sparse_vectors": {
"text-sparse": {
"index": { "on_disk": false }
}
}
}'Named dense vectors (if you want multiple embedding spaces in one collection — e.g. a text encoder and an image encoder) are declared similarly with "vectors": { "text": {...}, "image": {...} }.
Step 6: Upsert Points via REST, gRPC, and Python
A point = id + vector(s) + optional payload (JSON).
REST (bulk upsert)
curl -s -X PUT \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
"http://127.0.0.1:6333/collections/documents/points?wait=true" \
-d '{
"points": [
{
"id": 1,
"vector": [0.12, 0.98, 0.05, 0.44, 0.77, 0.01, 0.33, 0.66]
},
{
"id": 2,
"vector": [0.88, 0.11, 0.42, 0.09, 0.55, 0.73, 0.18, 0.27],
"payload": {
"title": "Qdrant vs Pinecone",
"url": "https://example.com/post-1",
"tags": ["vector-db", "comparison"],
"published_at": 1713139200
}
}
]
}'(In a real workload, replace the truncated 8-dimensional vectors with full-length 1536-dim embeddings from your model.)
Python client
Install and use the official qdrant-client:
pip install qdrant-clientfrom qdrant_client import QdrantClient from qdrant_client.models import PointStruct, VectorParams, Distanceclient = QdrantClient( url="http://127.0.0.1:6333", api_key="YOUR_API_KEY", prefer_grpc=True, # gRPC is 2–3x faster for bulk upserts )
Generate or load your embeddings however you want —
here we assume
docs = [ {"id": 101, "text": "Qdrant is a Rust-written vector DB", "category": "intro"}, {"id": 102, "text": "HNSW is the default ANN index", "category": "algo"}, ]embed(text)returns a list[float] of dim 1536.points = [ PointStruct( id=d["id"], vector=embed(d["text"]), payload={"text": d["text"], "category": d["category"]}, ) for d in docs ]
client.upsert(collection_name="documents", points=points, wait=True)
The Python client automatically negotiates gRPC when prefer_grpc=True is set and port 6334 is reachable. Bulk upserts of 1 M points typically run 2–3x faster over gRPC than over REST.
gRPC directly
If you want to bypass the Python client, the gRPC API is available on port 6334. Proto definitions ship in the qdrant/qdrant repo. Client libraries exist for Go, Rust, Java, Node.js, and .NET.
Step 7: Dense, Sparse, and Hybrid Search
Dense similarity search
curl -s -X POST \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/points/query \
-d '{
"query": [0.12, 0.98, 0.05, 0.44, 0.77, 0.01, 0.33, 0.66],
"limit": 5,
"with_payload": true,
"params": {
"hnsw_ef": 128,
"quantization": { "rescore": true, "oversampling": 2.0 }
}
}'hnsw_ef controls search-time quality (higher = better recall, slower). rescore: true pulls original float32 vectors for the top candidates to correct any quantization error.
Sparse (keyword-style) search
Sparse vectors are {indices, values} pairs — typically produced by models like SPLADE or by a BM25 tokenizer. Once you have upserted sparse vectors under the text-sparse name:
curl -s -X POST \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/points/query \
-d '{
"query": {
"indices": [42, 1337, 9001],
"values": [0.8, 0.5, 0.3]
},
"using": "text-sparse",
"limit": 5
}'Hybrid search with fusion
Combine dense semantic recall with sparse lexical precision using Qdrant's native Query API and Reciprocal Rank Fusion (RRF):
curl -s -X POST \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/points/query \
-d '{
"prefetch": [
{
"query": [0.12, 0.98, 0.05, 0.44, 0.77, 0.01, 0.33, 0.66],
"using": "",
"limit": 50
},
{
"query": { "indices": [42, 1337], "values": [0.8, 0.5] },
"using": "text-sparse",
"limit": 50
}
],
"query": { "fusion": "rrf" },
"limit": 10,
"with_payload": true
}'Each prefetch block runs in parallel, top-50 candidates from each flow into the fusion step, and Qdrant returns a merged ranked list. This single-round-trip hybrid pattern is the fastest way to get BM25-competitive recall without running a separate Elasticsearch cluster.
Step 8: Filtering with Payload Indexes
Qdrant filters can live inside any search request. For small collections, filters work out of the box. For collections above a few hundred thousand points, you want a payload index so filtering stays sub-millisecond.
Create indexes for the fields you filter on most:
# Keyword / tag filter
curl -s -X PUT \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/index \
-d '{ "field_name": "tags", "field_schema": "keyword" }'Numeric range / date filter
curl -s -X PUT \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/index \
-d '{ "field_name": "published_at", "field_schema": "integer" }'Full-text search inside payload
curl -s -X PUT \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/index \
-d '{
"field_name": "title",
"field_schema": {
"type": "text",
"tokenizer": "word",
"min_token_len": 2,
"max_token_len": 20,
"lowercase": true
}
}'Now run a filtered semantic query:
curl -s -X POST \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
http://127.0.0.1:6333/collections/documents/points/query \
-d '{
"query": [0.12, 0.98, 0.05, 0.44, 0.77, 0.01, 0.33, 0.66],
"filter": {
"must": [
{ "key": "tags", "match": { "any": ["vector-db", "rag"] } },
{ "key": "published_at", "range": { "gte": 1704067200 } }
],
"must_not": [
{ "key": "tags", "match": { "value": "deprecated" } }
]
},
"limit": 10,
"with_payload": true
}'Qdrant evaluates filters inside the HNSW traversal (not post-hoc), so filtered queries stay fast even when the filter matches only 0.1% of points — a common weakness of competing vector databases.
Step 9: Secure the API with Keys and TLS
We already set an api_key and read_only_api_key in config.yaml. Now we need to front Qdrant with TLS and block direct public access.
9.1 Firewall: only allow 443 in
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableNote that 6333 and 6334 remain blocked from the public internet — Qdrant already listens on 127.0.0.1 per our config.
9.2 Nginx reverse proxy with Let's Encrypt
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site:
sudo tee /etc/nginx/sites-available/qdrant > /dev/null <<'EOF' server { listen 80; server_name qdrant.yourdomain.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; server_name qdrant.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/qdrant.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/qdrant.yourdomain.com/privkey.pem;
client_max_body_size 128m;
# Long timeouts for big bulk upserts and streaming scroll proxy_read_timeout 600s; proxy_send_timeout 600s; proxy_buffering off;
add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Strict-Transport-Security "max-age=31536000" always;
location / { proxy_pass http://127.0.0.1:6333; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } EOF
sudo ln -s /etc/nginx/sites-available/qdrant /etc/nginx/sites-enabled/ sudo nginx -t sudo certbot --nginx -d qdrant.yourdomain.com --agree-tos --redirect -m [email protected] -n sudo systemctl reload nginx
9.3 gRPC over TLS
If you also need public gRPC on port 6334, add a second stream block to Nginx (requires the stream module) or terminate TLS inside Qdrant itself:
# /etc/qdrant/config.yaml
service:
enable_tls: true
tls:
cert: /etc/letsencrypt/live/qdrant.yourdomain.com/fullchain.pem
key: /etc/letsencrypt/live/qdrant.yourdomain.com/privkey.pem
ca_cert: nullRemember to give the qdrant user read access to the certs (or use a certbot --deploy-hook to copy + chown them after each renewal).
9.4 Test authenticated access
# Without key — should be rejected curl -i https://qdrant.yourdomain.com/collectionsHTTP/2 403 { "status": { "error": "Must provide an API key..." } }
With key — should succeed
curl -i -H "api-key: ${QDRANT_API_KEY}" https://qdrant.yourdomain.com/collections
HTTP/2 200
Use the read_only_api_key for dashboards and analytics clients that should not mutate data.
Step 10: The Web UI (/dashboard)
Qdrant ships a built-in web UI served from the static assets we installed in Step 2.3. Visit:
https://qdrant.yourdomain.com/dashboardThe dashboard prompts for your API key, then lets you:
- Browse all collections and inspect their schema, HNSW params, and quantization config.
- Paginate through points with their payloads and vectors.
- Run ad-hoc queries in a REST console (the same
POST /collections/{name}/points/querybody you would send from code). - Visualize the vector space with UMAP projections for any collection.
- Monitor memory, segment counts, and optimizer status live.
- Create, delete, and alias collections without writing a line of code.
Step 11: Snapshot Backups and Restore
Qdrant snapshots are point-in-time .snapshot tarballs containing the full segment state of a collection. They can be created online (no downtime) and restored on the same or a different Qdrant instance.
Take a snapshot manually
curl -s -X POST \
-H "api-key: ${QDRANT_API_KEY}" \
http://127.0.0.1:6333/collections/documents/snapshotsResponse:
{
"result": {
"name": "documents-1745789012.snapshot",
"creation_time": "2026-04-16T10:30:00",
"size": 524288000
},
"status": "ok"
}Snapshots land in /var/lib/qdrant/snapshots/documents/. List them:
curl -s -H "api-key: ${QDRANT_API_KEY}" \
http://127.0.0.1:6333/collections/documents/snapshotsFull-cluster snapshot
To snapshot every collection + the cluster configuration in one call:
curl -s -X POST \
-H "api-key: ${QDRANT_API_KEY}" \
http://127.0.0.1:6333/snapshotsAutomate daily snapshots + off-site upload
Create /usr/local/bin/qdrant-backup.sh:
sudo tee /usr/local/bin/qdrant-backup.sh > /dev/null <<'EOF' #!/usr/bin/env bash set -euo pipefailAPI_KEY="${QDRANT_API_KEY:?missing}" BUCKET="s3://your-backup-bucket/qdrant" SNAP_DIR="/var/lib/qdrant/snapshots" DATE=$(date -u +%Y%m%dT%H%M%SZ)
1. Snapshot every collection
for COLL in $(curl -s -H "api-key: $API_KEY" http://127.0.0.1:6333/collections \ | python3 -c 'import json,sys; [print(c["name"]) for c in json.load(sys.stdin)["result"]["collections"]]'); do echo "Snapshotting $COLL" curl -s -X POST -H "api-key: $API_KEY" \ "http://127.0.0.1:6333/collections/${COLL}/snapshots" >/dev/null done2. Upload to S3 (requires awscli configured)
aws s3 sync "$SNAP_DIR" "${BUCKET}/${DATE}/" --storage-class STANDARD_IA3. Prune local snapshots older than 7 days
find "$SNAP_DIR" -name '*.snapshot' -mtime +7 -deleteecho "Backup complete: $DATE" EOF
sudo chmod +x /usr/local/bin/qdrant-backup.sh
Install a daily cron job:
sudo tee /etc/cron.d/qdrant-backup > /dev/null <<EOF
QDRANT_API_KEY=${QDRANT_API_KEY}
0 3 * root /usr/local/bin/qdrant-backup.sh >> /var/log/qdrant-backup.log 2>&1
EOFRestore from a snapshot
Upload the .snapshot file to /var/lib/qdrant/snapshots/<collection>/ on the target server, then:
curl -s -X PUT \
-H "api-key: ${QDRANT_API_KEY}" \
-H "Content-Type: application/json" \
"http://127.0.0.1:6333/collections/documents/snapshots/recover" \
-d '{
"location": "file:///var/lib/qdrant/snapshots/documents/documents-1745789012.snapshot",
"priority": "snapshot"
}'Qdrant also supports priority: replica (restore from a peer in a clustered deployment) and HTTP(S) URLs as the snapshot source — useful for pulling a backup directly from S3.
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
Must provide an API key or an Authorization bearer token | api_key set in config but not sent | Add -H "api-key: ${QDRANT_API_KEY}" to every request |
Service refused connection on 6333 | Bound to 127.0.0.1, accessed from outside | Use the Nginx reverse proxy on port 443; do not open 6333 publicly |
Not enough memory during large upserts | HNSW build peaks RAM 2–3x above steady state | Enable scalar quantization with always_ram: true; set on_disk_payload: true; temporarily lower ef_construct |
| Snapshot create returns 500 | snapshots_path unwritable by the qdrant user | sudo chown -R qdrant:qdrant /var/lib/qdrant/snapshots |
Dashboard shows blank page at /dashboard | static_content_dir missing or wrong | Re-download dist-qdrant.zip into /var/lib/qdrant/static and set service.static_content_dir accordingly |
| gRPC works locally but not through Nginx | Default Nginx lacks HTTP/2 + stream block for gRPC | Use grpc_pass on a dedicated listen 6334 ssl http2; server block, or enable service.enable_tls and expose 6334 directly |
| Filtered queries are slow | Payload field has no index | PUT /collections/{name}/index for each field used in filters |
| Memory usage keeps growing | Deletes leave soft-tombstones until optimizer runs | Lower optimizers.deleted_threshold or trigger optimizer via a write request |
sudo journalctl -u qdrant -fCheck disk usage:
sudo du -sh /var/lib/qdrant/storage /var/lib/qdrant/snapshotsFAQ
How much RAM do I need for N vectors at D dimensions?
A rough formula with HNSW and scalar int8 quantization:
RAM ≈ N × (D × 1 byte + 4 × m × 4 bytes) + payload_overheadFor 10 M vectors at 768 dimensions with m=16 and int8 quantization, that is roughly 10M × (768 + 256) bytes ≈ 10 GB. Plus HNSW overhead of 10–20%. A 12 GB CloudCore Professional plan comfortably handles this with room for the OS and a small embedding service.
Without quantization (float32), multiply the vector portion by 4x — which is exactly the case for upgrading to larger plans.
Qdrant vs Weaviate vs Milvus — which should I pick?
- Qdrant — Fastest single-node throughput in most benchmarks, cleanest Query API, best DX for Rust/Python teams, built-in web UI, and great filtered-search performance thanks to in-graph filtering. Best for most VPS deployments. See also Weaviate install guide.
- Weaviate — First-class GraphQL API, built-in modules for OpenAI/Cohere/HF embeddings, and strong multi-tenant features. Best if you want a batteries-included stack that ingests raw text and produces vectors automatically.
- Milvus — The heavyweight distributed option, designed for tens of billions of vectors across a cluster. Complex to operate (etcd + Pulsar/Kafka + MinIO) but unbeatable at scale. Best for production clusters with a dedicated platform team. See the Milvus install guide.
Can I run Qdrant alongside Ollama on the same VPS?
Yes, and this is the canonical self-hosted RAG setup. Qdrant stores your document embeddings; Ollama generates embeddings (via nomic-embed-text) and answers questions. On a 12 GB CloudCore Professional, a Llama 3.1 8B Q4 model + Qdrant with 2 M quantized 768-dim vectors fits comfortably with room to spare. For higher throughput, move Ollama to a GPU VPS and keep Qdrant on a CPU-only plan — they communicate cleanly over HTTP.
How do I migrate from Pinecone to Qdrant?
The Qdrant team publishes a migration script that scrolls through your Pinecone index and upserts batches into Qdrant. Key caveats:
- Pinecone IDs are strings; Qdrant IDs can be strings (UUIDs) or unsigned integers.
- Pinecone namespaces map to separate Qdrant collections (or you can use payload filtering on a single collection).
- Re-create payload indexes for every field you used in Pinecone metadata filters.
Does Qdrant support multi-tenancy?
Yes — two patterns. The payload-based pattern stores all tenants in one collection and filters every query by tenant_id (with a keyword payload index). The collection-per-tenant pattern gives each tenant its own collection with its own HNSW graph. Payload-based is simpler and cheaper up to ~1000 tenants; collection-per-tenant gives stronger isolation and per-tenant backups for larger multi-tenant SaaS deployments.
What embedding model should I use for self-hosted RAG?
For most English-language RAG, nomic-embed-text (768 dim, Apache 2.0, runs locally via Ollama) is the best free option. For multilingual support, intfloat/multilingual-e5-large (1024 dim) is excellent. If you want OpenAI quality without the API, bge-large-en-v1.5 (1024 dim) is competitive and runs fine on CPU via sentence-transformers. Always normalize embeddings before upsert when using cosine distance with these models — and match vector.size in your collection config exactly.
Next Steps
With Qdrant running, TLS'd, and backing up nightly, here is where to go next:
- Pair with Ollama for a fully local RAG stack — see How to Install Ollama on Ubuntu and wire them together with LangChain or LlamaIndex.
- Compare to Weaviate if you prefer GraphQL and built-in embedding modules — How to Install Weaviate on Ubuntu.
- Scale to billions of vectors with a distributed cluster — How to Install Milvus on Ubuntu.
- Read the official docs — the Qdrant documentation covers advanced topics like multitenancy, sharding, replication, distributed snapshots, and the full Query API reference.
- Tune your HNSW parameters with the built-in benchmarking tools to squeeze out recall vs. latency trade-offs for your specific dataset.
Deploy on CloudCore Professional>
6 vCPU, 12 GB RAM, 100 GB NVMe, unmetered bandwidth — EUR 19.99/month. The sweet spot for self-hosted Qdrant serving tens of millions of embeddings.>
Launch a VPS now