Use case · Vector database

Qdrant self hosted on a dedicated server: sizing, Docker Compose, keys and snapshots

Self-hosting Qdrant means running the official qdrant/qdrant image (or the binary) on a 64-bit Linux host with block storage (preferably SSD or NVMe) under a POSIX file system, an API key set before first start, and ports 6333 (REST) and 6334 (gRPC) bound to 127.0.0.1 with TLS in front of anything that leaves the box. The open-source build is Apache 2.0 and has no authentication until you configure it. Memory is the sizing constraint: the capacity-planning formula below turns vector count and dimensions into RAM and disk before you pick a machine.

See the machines

What Qdrant asks of the hardware

Qdrant needs a 64-bit CPU, a real block device and three TCP ports. The installation page does not give a minimum RAM or core count; it says CPU and memory depend on the number of vectors, their dimensions, payloads and their indexes, replication and quantization. What it does fix:

RequirementInstallation pageWhat it means here
CPU architecturex86_64/amd64 or AArch64/arm64; 32-bit not supportedIntel, AMD and Ampere Altra lines all qualify
StorageBlock-level access with a POSIX-compatible file system; NFS and object storage such as S3 do not workLocal disks on the machine, not a network share
Disk typeSSD or NVMe recommended when vectors are offloaded to diskAvoid spinning disks; the production checklist says HDDs add significant latency
Ports6333 HTTP API plus health and metrics, 6334 gRPC, 6335 distributed deploymentA single node needs only 6333 and 6334

The troubleshooting page adds one operating limit: each collection segment holds files open, and a busy instance can hit Too many files open (OS error 24). Its fix for Docker is raising nofile to 10000, which the Compose file below does.

Working out RAM and disk

The capacity-planning page sizes each collection separately and sums them. Its base unit is points × replication factor; dense vectors cost base × dimensions × bytes per dimension (4 for float32), the HNSW graph about base × m × 2 × 4 bytes × 1.2 with m at 16 by default, and the ID tracker 52 bytes per point. Dense vectors and the HNSW index sit in the cached memory tier by default; payloads default to cold and cost disk, not RAM. Add roughly 20% headroom on both RAM and disk for page cache, the optimizer, the WAL and snapshots.

One million 768-dimension float32 vectors, unindexed 1 KB payloadsRAMDisk
Docs' worked example, replication factor 2, with headroom≈ 7.33 GB≈ 10.76 GB
Same data on one node, replication factor 1 (our arithmetic, same formula)≈ 3.7 GB≈ 5.4 GB
Docs' example with 4-bit TurboQuant pinned and originals cold (dense vectors only, RF 2)≈ 0.72 GB instead of 5.72 GBClose to unchanged

Quantization costs some accuracy; the docs say to check recall against unquantized results because some embedding models do not quantize well. Moving vectors to cold keeps them in memory-mapped files and lets the OS cache the hot pages, and the docs give a rule of thumb: store half as many vectors in RAM and search latency roughly doubles. Keep the HNSW index out of cold if you can. A single node typically tops out around 100 million vectors, a ceiling the docs treat as rough. Plan disk for restores too: the docs say restoring a snapshot needs twice the disk space the source collection uses, and the restore below also keeps the old storage directory and the snapshot file on disk beside the restored copy until you clean up, so budget for all three at once.

Why one tenant per box suits it

cached and cold structures live in the kernel page cache, so search speed depends on how much of it Qdrant gets. On a dedicated server nothing else competes for that cache: every core, all the memory, the disks and the port belong to one customer, with root and no hypervisor. The disks are local block devices, which is the storage the requirements page asks for. The port on lines marked Unmetered has no transfer allowance and no overage fee, so copying snapshot archives off the machine every night is not a line on the invoice.

Lines that fit

  • A RAG corpus up to a few million vectors: the Xeon E3-1585L v5 line (4 cores, 16 to 64 GB, NVMe-SSD) or the 16-core Xeon D-1587 line, which takes 16 to 128 GB and one 1.92 TB NVMe-SSD. Compare drive options on the NVMe dedicated servers page.
  • Large collections held in RAM: the dual-socket Xeon Gold 6130 and E5-2630L v3 lines take up to 1 TB of memory; the four-socket Xeon Gold 6230 goes to 2 TB for a working set that has to stay on one machine. Pick SSDs in the configurator.
  • arm64: the 64-core Ampere Altra Q64-22, 32 GB to 1 TB, built to order. The v1.20.0 image is published for amd64 and arm64. Skip the X-Gene cartridge: it boots only Ubuntu 16.04 and 18.04, too old for the Docker Engine this setup checks for.
  • Avoid lines whose default disk is plain SATA (the Atom C2750 and Opteron 6366 HE rows) unless you replace it with SSD.

A racked machine is ready to log into about 30 minutes once payment is approved; a built-to-order configuration takes 4 to 24 hours. Every racked single-, dual- and quad-socket server has an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC); among cartridges only the m510 and m710x do, the rest have power control only. IPv6 is added at checkout at no charge.

Install with Docker Compose

The security page is explicit: a self-hosted instance listens on all interfaces with no authentication until you change that. So the keys exist before the container first starts, and both ports listen on loopback alone. Before running it, the host needs Docker's own apt packages: the Compose plugin, and an Engine no older than 28; from Engine 28 on, a 127.0.0.1 port mapping is reachable only from the host. As root or with sudo:

sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH && sudo ufw --force enable
sudo install -d -m 700 /srv/qdrant /srv/qdrant/storage /srv/qdrant/snapshots
if sudo test -e /srv/qdrant/qdrant.env; then echo "qdrant.env exists: keeping its keys"
else a=$(openssl rand -hex 32) && r=$(openssl rand -hex 32) && [ -n "$a" ] && [ -n "$r" ] \
  && sudo install -m 600 /dev/null /srv/qdrant/qdrant.env \
  && printf 'QDRANT__SERVICE__API_KEY=%s\nQDRANT__SERVICE__READ_ONLY_API_KEY=%s\nQDRANT__TELEMETRY_DISABLED=true\n' "$a" "$r" \
     | sudo tee /srv/qdrant/qdrant.env >/dev/null; fi
sudo tee /srv/qdrant/compose.yaml >/dev/null <<'EOF'
services:
  qdrant:
    image: qdrant/qdrant:v1.20.0
    restart: unless-stopped
    env_file: /srv/qdrant/qdrant.env
    ports:
      - "127.0.0.1:6333:6333"
      - "127.0.0.1:6334:6334"
    volumes:
      - /srv/qdrant/storage:/qdrant/storage
      - /srv/qdrant/snapshots:/qdrant/snapshots
    ulimits:
      nofile: {soft: 10000, hard: 10000}
EOF
v=$(sudo docker version --format '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] \
  && sudo docker compose -f /srv/qdrant/compose.yaml up -d \
  || echo "STOPPED: Docker Engine older than 28, or start failed"

The block refuses to overwrite an existing qdrant.env, so a second run keeps the keys your applications hold. The keys come from openssl, go into a file created at mode 600, and pass through printf, a shell builtin, so they never show in ps; docker inspect still reveals them to whoever may run it. The docs' own 6333:6333 mapping, with no address, would publish on 0.0.0.0 and [::], and Docker-published ports skip ufw's rules on IPv4. Compared with the Compose file on the installation page, ours pins a version instead of latest, binds both ports to 127.0.0.1, drops the expose list (6335 is for clusters), replaces the inline configs block with environment variables, drops container_name, mounts absolute host paths for storage plus a snapshot directory, adds the nofile limit and uses unless-stopped instead of always. The environment variable names follow the configuration page's QDRANT__SECTION__KEY rule; QDRANT__TELEMETRY_DISABLED=true is the documented opt-out from anonymized usage statistics.

Check it: curl -fsS http://127.0.0.1:6333/readyz answers without a key (the health endpoints always do), while curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:6333/collections must not print 200. An application on the same machine connects to http://127.0.0.1:6333 with the api-key header.

Keys, TLS and the Web UI

The admin key (api_key) can do everything, including recovering a collection from a URL, which makes Qdrant fetch that URL; keep it server side. The read-only key can query, scroll, count and download snapshots but cannot create, delete or upsert. For per-collection rights, put QDRANT__SERVICE__JWT_RBAC=true in qdrant.env, re-create the container with docker compose -f /srv/qdrant/compose.yaml up -d --force-recreate (a plain restart keeps the old environment), and sign HS256 JWTs yourself with the admin key (any JWT library, or Access Tokens in the Web UI), with an access claim of r or rw per collection and an exp; changing the admin key invalidates every token signed with it.

The docs warn that a key sent over plain HTTP leaks. Loopback traffic never leaves the machine; for clients elsewhere, put TLS in front. Caddy from the Ubuntu 24.04 (or newer) archive obtains a certificate on its own once the hostname's DNS points at the server. This Caddyfile is ours, not Qdrant's: it proxies REST only, and returns 404 for the bundled Web UI under /dashboard.

sudo apt install -y caddy
sudo ufw allow 80,443/tcp
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
qdrant.example.com {
	@ui path /dashboard /dashboard/*
	respond @ui 404
	reverse_proxy 127.0.0.1:6333
}
EOF
sudo systemctl reload caddy

Use the Web UI over your own SSH tunnel instead: ssh -N -L 6333:127.0.0.1:6333 you@your-server; a local browser then reaches http://localhost:6333/dashboard. gRPC on 6334 stays local in this layout.

Snapshots, restore and upgrades

On a single node, POST /snapshots writes a full storage snapshot, every collection plus aliases, as one .snapshot file under /qdrant/snapshots, which is /srv/qdrant/snapshots on the host. The script below takes one nightly and archives the Compose file and keys beside it. The key goes to curl on stdin, not as an argument.

sudo install -d -m 700 /var/backups/qdrant
sudo tee /usr/local/sbin/qdrant-backup.sh >/dev/null <<'EOF'
set -euo pipefail
umask 077
d=$(date +%F)
sed -n 's/^QDRANT__SERVICE__API_KEY=/api-key: /p' /srv/qdrant/qdrant.env \
  | curl -fsS -X POST -H @- http://127.0.0.1:6333/snapshots > /var/backups/qdrant/snapshot-$d.tmp
mv /var/backups/qdrant/snapshot-$d.tmp /var/backups/qdrant/snapshot-$d.json
tar -czf /var/backups/qdrant/config-$d.tar.gz -C /srv/qdrant compose.yaml qdrant.env
EOF
printf '30 3 * * * root bash /usr/local/sbin/qdrant-backup.sh >> /var/backups/qdrant/backup.log 2>&1\n' \
  | sudo tee /etc/cron.d/qdrant-backup >/dev/null

Copy /srv/qdrant/snapshots and /var/backups/qdrant to another machine with your own job, using rsync -a and never --delete. Snapshots accumulate: remove old ones with DELETE /snapshots/NAME once the off-site copy is verified. Restores work only into the same or the next minor version, so keep the image tag from the archived compose.yaml.

  1. Restore, as root: find the file named in that day's JSON with find /srv/qdrant/snapshots -name 'NAME', then stop the service: docker compose -f /srv/qdrant/compose.yaml down.
  2. Set the current data aside, refusing if an old copy is in the way: test ! -e /srv/qdrant/storage.previous && mv /srv/qdrant/storage /srv/qdrant/storage.previous && install -d -m 700 /srv/qdrant/storage && echo moved. Continue only when the output reads moved; otherwise deal with the old storage.previous first.
  3. Start once with the restore flag, which the docs say is the only way to restore a full snapshot: docker compose -f /srv/qdrant/compose.yaml run --rm --service-ports qdrant ./qdrant --storage-snapshot /qdrant/snapshots/FILE, where FILE is the snapshot's file name and /qdrant/snapshots is the container side of /srv/qdrant/snapshots.
  4. From a second shell, list /collections with the admin key. Either way, first stop the restore run with Ctrl+C in its shell and wait for it to exit. If the data is right, run up -d. If not: docker compose -f /srv/qdrant/compose.yaml down, rm -rf /srv/qdrant/storage, mv /srv/qdrant/storage.previous /srv/qdrant/storage, up -d. Delete storage.previous only when the restored instance is serving correctly.

Upgrades go one minor version at a time, each to the latest patch of that minor: 1.18 to 1.20 means passing through the newest 1.19.x first, because a single node cannot skip the data migrations. Take a snapshot, change the tag in compose.yaml, then docker compose -f /srv/qdrant/compose.yaml pull and up -d; a single node has a short downtime while it restarts. Update the server before the client SDKs and keep them within one minor version of it.

Licence and limits

The engine is Apache License 2.0 (the LICENSE file in the qdrant/qdrant repository). Our general reading, not legal advice: it is permissive, so you can run and modify it freely, and if you pass copies on you keep the licence text and copyright notices; it carries no duty to publish your source.

One server is one copy of the data. The docs recommend at least three nodes and a replication factor of two or more for high availability; with every collection replicated, a rolling restart upgrades the cluster without downtime. Their deployment comparison lists automatic shard rebalancing and full-cluster backup and disaster recovery under their managed cloud, not under the Helm chart. The embeddings usually come from a model you also host: see LLM hosting for that half, or Dify on its own server for a complete retrieval app.

Questions

Is the self-hosted Qdrant engine free to use?

The open-source engine is released under the Apache License 2.0, so a server you run it on yourself owes no licence fee. The vendor's managed cloud and its Private Cloud enterprise operator are separate commercial offerings. On a dedicated server you pay for the machine, not for Qdrant.

How much RAM does a Qdrant server need?

It depends on the vectors, not on a fixed minimum. Use the capacity-planning formula: points × replication factor × dimensions × bytes per dimension for dense vectors, plus the HNSW index, ID tracker and payload indexes, then about 20% headroom. The docs' example of one million 768-dimension float32 vectors with replication factor 2 comes to about 7.33 GB. Quantization and the cold tier cut it further.

How do I set an API key on self-hosted Qdrant?

Set service.api_key in the configuration file or QDRANT__SERVICE__API_KEY in the environment, and add a read_only_api_key for query-only clients. Every request must then carry the api-key header (or Authorization: Bearer). The docs require TLS in front so the key is not sent in plain text.

Can Qdrant keep its data on an NFS share or in S3?

No. The installation page says Qdrant needs block-level storage with a POSIX-compatible file system and will not run on NFS mounts or on object stores like S3; block-level network storage such as iSCSI is acceptable. Snapshot files alone can be written to S3-compatible storage through the snapshot configuration.

Size the memory, then pick the line

Run the capacity formula against your collections and configure a machine with SSD or NVMe and the RAM it calls for.

See the machines

Related on Server Room

Other use cases