Qdrant
A Conn3x circuit that runs Qdrant, the vector database, on your own hosts. It’s the self-hosted counterpart to Qdrant Cloud and its Terraform provider: the same database, on a box you already pay for, with your embeddings never leaving it.
It downloads a pinned Qdrant release, checks its SHA-256, and runs it under systemd as its own qdrant user. You set the ports, the listen address, where the data lives and an optional API key; the circuit writes /etc/qdrant/config.yaml from them and restarts Qdrant when it changes. Out of the box Qdrant listens only on 127.0.0.1 and sends no telemetry, so nothing is reachable from outside until you say so.
If you’re building retrieval on local models, it pairs well with circuit-ollama: Ollama makes the embeddings, Qdrant stores and searches them.
variable "qdrant_api_key" { type = string sensitive = true}
module "qdrant" { source = "github.com/conn3xorg/circuit-qdrant?ref=v0.1.0"
hosts = { vectors1 = "203.0.113.10" } ssh_private_key_file = "~/.ssh/id_ed25519"
listen_address = "0.0.0.0" api_key = var.qdrant_api_key}
output "qdrant_urls" { value = module.qdrant.urls}Then run conn3x init and conn3x apply, with the key in TF_VAR_qdrant_api_key. A second conn3x plan says No changes.
Clients send the key in an api-key header:
curl -H "api-key: $TF_VAR_qdrant_api_key" http://203.0.113.10:6333/collectionsThe key travels in plain HTTP. Across anything but a private network, put Qdrant behind a TLS proxy, such as circuit-caddy, and keep listen_address at 127.0.0.1.
For hosts created in the same plan, pass host objects with the host key you generated, so Conn3x checks it without a known_hosts entry:
hosts = { for name, d in digitalocean_droplet.vectors : name => { address = d.ipv4_address host_key = tls_private_key.host[name].public_key_openssh }}To try it on a throwaway container first, see examples/local.
What it does
Section titled “What it does”On each host, in order:
- Installs
curl,tarandgzipif they’re missing. - Creates the
qdrantsystem user, with home/var/lib/qdrantand no login shell, and createsstorage_pathandsnapshots_pathowned by it, mode0750. - Downloads
qdrant-ARCH-unknown-linux-musl.tar.gzfrom the GitHub release into/opt/qdrant/VERSION/and unpacks theqdrantbinary there. If the checksum doesn’t matchchecksums, the apply fails and nothing is unpacked. The musl build is static, so the same binary runs on every distribution. - Writes
/etc/qdrant/config.yaml, owned byqdrantwith mode0600, and/etc/systemd/system/qdrant.service. A change to either restarts Qdrant. - Starts Qdrant now and at boot, and waits until its
/readyzendpoint answers.
The config file is write-only: it reaches the host, but the plan and state hold only an HMAC of it, since it can carry the API key. A change to any input shows in the plan as content_hmac changing. See Secrets for the HMAC key Conn3x uses.
The unit runs Qdrant with ProtectSystem=strict, so it can write only to its home, storage_path and snapshots_path.
Before the circuit runs, the conn3x_host data source reads each host’s architecture over SSH. x86_64 and aarch64 each get their own build; other architectures fail the plan.
What it doesn’t do
Section titled “What it doesn’t do”- It doesn’t open a firewall port. Open
http_port, andgrpc_portif your clients use gRPC, with circuit-firewall or a step of your own. - It doesn’t set up TLS, a read-only key, JWT access or clustering. Qdrant supports all four; set them up on top if you need them.
- It doesn’t create collections. That’s your application’s job, over the API.
- It doesn’t take backups. Qdrant writes snapshots to
snapshots_pathwhen you ask it to over the API; copying them somewhere safe is up to you. - Deleting the module leaves Qdrant installed and running, with its data.
Upgrading
Section titled “Upgrading”Set qdrant_version and checksums together. The release page lists each asset’s SHA-256:
module "qdrant" { source = "github.com/conn3xorg/circuit-qdrant?ref=v0.1.0"
hosts = { vectors1 = "203.0.113.10" } qdrant_version = "1.20.0" checksums = { x86_64 = "b03a64616a1f41ddab78b480a318466541765f19835318113b1821cbea8df52c" aarch64 = "bd8a458b8ef8006ce2c603c5bd6d1d973e39d787f794008dd959112e747c3bb7" }}The new release unpacks beside the old one, the unit points at it, and Qdrant restarts on the same data. The old directory stays until you remove it. Read Qdrant’s release notes before a jump of more than one minor version.
Inputs
Section titled “Inputs”| Name | Type | Default | Description |
|---|---|---|---|
hosts |
any | Required | Hosts to configure, by name. Each value is an address, such as "203.0.113.10", or a host object such as { address = "203.0.113.10", host_key = "ssh-ed25519 AAAA..." }. A host object can also set user, port, private_key_file and jump_hosts. The two forms can be mixed. |
ssh_user |
string | "root" |
User to log in as. |
ssh_port |
number | 22 |
SSH port. |
ssh_private_key_file |
string | null |
Path to a private key. When null, Conn3x uses the SSH agent. |
ssh_known_hosts_file |
string | null |
known_hosts file to check host keys against. When null, ~/.ssh/known_hosts. Not used for a host object with host_key. |
ssh_become |
bool | false |
Run commands as root through sudo. Set it when ssh_user isn’t root. |
qdrant_version |
string | "1.20.0" |
Qdrant release to install, without the leading v. |
checksums |
map(string) | The 1.20.0 checksums | SHA-256 of the musl release tarball for x86_64 and aarch64. |
listen_address |
string | "127.0.0.1" |
Address the REST and gRPC APIs listen on. "0.0.0.0" listens on every address. |
http_port |
number | 6333 |
Port of the REST API. |
grpc_port |
number | 6334 |
Port of the gRPC API. |
api_key |
string, sensitive | null |
Key every request must send in an api-key header, at least 16 characters. Sensitive, and kept out of state. When null, the APIs take requests without one. |
storage_path |
string | "/var/lib/qdrant/storage" |
Directory Qdrant keeps its collections in. |
snapshots_path |
string | "/var/lib/qdrant/snapshots" |
Directory Qdrant writes snapshots to. |
telemetry_disabled |
bool | true |
Stop Qdrant sending anonymous usage statistics. Set false to send them. |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
config_file |
Path of the configuration file, /etc/qdrant/config.yaml. |
urls |
Each host’s REST API URL by host name, such as { vectors1 = "http://203.0.113.10:6333" }. With listen_address on every address, the host’s own address; otherwise listen_address. |
Requirements
Section titled “Requirements”Requires Conn3x v0.6.5 or later, systemd on each host, and outbound HTTPS to github.com.
Tested on
Section titled “Tested on”Rocky Linux 10, Fedora 45, Ubuntu 24.04 and Debian 13. CI applies examples/local to each on amd64 with an API key, checks that the second plan has no changes, then creates a collection over the REST API, upserts two points, searches and gets the nearer one back, and checks that a request without the key gets 401.
The same checks passed by hand on an Ubuntu 24.04 droplet and a Rocky Linux 10 droplet with SELinux enforcing, both with 1 vCPU and 2 GB of memory. Qdrant listened on 127.0.0.1 only and ran as unconfined_service_t on Rocky Linux, with no denials in the audit log. After a reboot it came back with the stored point, and the plan had no changes.
License
Section titled “License”MPL-2.0. See LICENSE. Qdrant itself is Apache-2.0.
Source
Section titled “Source”- Repository: conn3xorg/circuit-qdrant. This page covers v0.1.0; the repository’s tags list every release.
- Examples: examples/local.
- Related circuits: Ollama, Caddy, Firewall, Open WebUI, LiteLLM, Presidio, Langfuse, Agent box.