Why these apps together
A team automates answers from its own documents (retrieval-augmented generation) in n8n and wants the vectors on its own server, next to the workflows that write and read them.
The app pages deploy n8n and Qdrant apart. Together, n8n's Qdrant credential needs an address it can reach and the API key Qdrant requires: over the network Caddy shares with both, n8n calls Qdrant by service name, so the vectors never cross the internet. This page sizes both on one server and gives the address, the key and what the two share.
Apps in this stack
How the apps connect
- n8n to Qdrant:
http://qdrant:6333 - n8n's Qdrant Vector Store node calls Qdrant's REST API over Caddy's edge network, by service name, with the API key from QDRANT_API_KEY.
Resources for the whole server
Computed by the Stack Sizer's engine for these apps and load levels: what the apps need, the system baseline and the safety headroom. The same input in the Stack Sizer gives the same numbers.
| Part | vCPU | RAM (GiB) | Disk (GiB) |
|---|---|---|---|
| n8n | 1.75 | 1.875 | 8.5 |
| Qdrant | 0.5 | 0.5 | 0.8 |
| System baseline | 0.1 | 1.25 | 8.7 |
| Safety headroom | 0.47 | 0.725 | 2.7 |
| Recommended total | 2.82 | 4.35 | 20.7 |
Caddy is not included in the resource calculation of the Stack Sizer or the Deployment Spec.
Files from the Compose Generator
# Generated by the ANAROQ Compose Generator: https://anaroq.com/tools/compose-generator
# Engine @anaroq/compose 1.0.0, catalog 2026.10.10, exposure caddy.
# Apps: n8n 1.1.0, qdrant 1.0.0 (supported, not run-tested).
# No secrets in this file: copy .env.example to .env and fill it in before `docker compose up -d`.
services:
n8n:
image: "docker.n8n.io/n8nio/n8n:2.42.6"
restart: "unless-stopped"
environment:
DB_TYPE: "postgresdb"
DB_POSTGRESDB_HOST: "n8n-postgres"
DB_POSTGRESDB_PORT: "5432"
DB_POSTGRESDB_DATABASE: "n8n"
DB_POSTGRESDB_USER: "n8n"
DB_POSTGRESDB_PASSWORD: "${N8N_DB_PASSWORD:?set N8N_DB_PASSWORD in .env}"
N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY:?set N8N_ENCRYPTION_KEY in .env}"
N8N_RUNNERS_MODE: "external"
N8N_RUNNERS_AUTH_TOKEN: "${N8N_RUNNERS_AUTH_TOKEN:?set N8N_RUNNERS_AUTH_TOKEN in .env}"
N8N_RUNNERS_BROKER_LISTEN_ADDRESS: "0.0.0.0"
N8N_HOST: "${N8N_DOMAIN:?set N8N_DOMAIN in .env}"
N8N_PROTOCOL: "https"
N8N_PROXY_HOPS: "1"
N8N_WEBHOOK_URL: "https://${N8N_DOMAIN:?set N8N_DOMAIN in .env}/"
volumes:
- "n8n-data:/home/node/.n8n"
networks:
- "n8n-internal"
- "edge"
depends_on:
n8n-postgres:
condition: "service_healthy"
n8n-runner:
image: "n8nio/runners:2.42.6"
restart: "unless-stopped"
environment:
N8N_RUNNERS_AUTH_TOKEN: "${N8N_RUNNERS_AUTH_TOKEN:?set N8N_RUNNERS_AUTH_TOKEN in .env}"
N8N_RUNNERS_TASK_BROKER_URI: "http://n8n:5679"
networks:
- "n8n-internal"
depends_on:
n8n:
condition: "service_started"
n8n-postgres:
image: "postgres:18.6"
restart: "unless-stopped"
environment:
POSTGRES_USER: "n8n"
POSTGRES_PASSWORD: "${N8N_DB_PASSWORD:?set N8N_DB_PASSWORD in .env}"
POSTGRES_DB: "n8n"
volumes:
- "n8n-postgres-data:/var/lib/postgresql"
networks:
- "n8n-internal"
healthcheck:
test:
- "CMD-SHELL"
- "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"
interval: "10s"
timeout: "5s"
retries: 5
start_period: "30s"
qdrant:
image: "qdrant/qdrant:v1.19.2-unprivileged"
restart: "unless-stopped"
environment:
QDRANT__SERVICE__API_KEY: "${QDRANT_API_KEY:?set QDRANT_API_KEY in .env}"
volumes:
- "qdrant-storage:/qdrant/storage"
- "qdrant-snapshots:/qdrant/snapshots"
networks:
- "qdrant-internal"
- "edge"
healthcheck:
test:
- "CMD"
- "bash"
- "-c"
- "exec 3<>/dev/tcp/127.0.0.1/6333 && printf 'GET /readyz HTTP/1.0\\r\\n\\r\\n' >&3 && grep -q ' 200 ' <&3"
interval: "10s"
timeout: "5s"
retries: 6
start_period: "30s"
caddy:
image: "caddy:2.11.7-alpine"
restart: "unless-stopped"
environment:
N8N_DOMAIN: "${N8N_DOMAIN:?set N8N_DOMAIN in .env}"
QDRANT_DOMAIN: "${QDRANT_DOMAIN:?set QDRANT_DOMAIN in .env}"
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- "./Caddyfile:/etc/caddy/Caddyfile:ro"
- "caddy-data:/data"
- "caddy-config:/config"
networks:
- "edge"
networks:
n8n-internal: {}
qdrant-internal: {}
edge: {}
volumes:
n8n-data: {}
n8n-postgres-data: {}
qdrant-storage: {}
qdrant-snapshots: {}
caddy-data: {}
caddy-config: {}
# Generated by the ANAROQ Compose Generator: https://anaroq.com/tools/compose-generator
# Engine @anaroq/compose 1.0.0, catalog 2026.10.10, exposure caddy.
# Copy this file to .env and fill in every value. Never commit .env.
# ANAROQ does not create passwords or keys: generate them on your own machine.
# --- n8n ---
# Password of the n8n PostgreSQL user (POSTGRES_PASSWORD).
# Secret, required. For example: openssl rand -hex 24
N8N_DB_PASSWORD=
# Key n8n uses to encrypt stored credentials. Keep it with your backups: without it a restored database cannot decrypt them.
# Secret, required. For example: openssl rand -hex 32
N8N_ENCRYPTION_KEY=
# Shared token between n8n and its task runners.
# Secret, required. For example: openssl rand -hex 32
N8N_RUNNERS_AUTH_TOKEN=
# Public host name of n8n behind the reverse proxy, for example n8n.example.com.
# Required.
N8N_DOMAIN=
# --- qdrant ---
# Admin API key (QDRANT__SERVICE__API_KEY). Every REST and gRPC request must send it in the api-key header; the health endpoints stay open.
# Secret, required. For example: openssl rand -hex 32
QDRANT_API_KEY=
# Public host name of the Qdrant REST API and Web UI behind the reverse proxy, for example vectors.example.com.
# Required.
QDRANT_DOMAIN=
# Generated by the ANAROQ Compose Generator: https://anaroq.com/tools/compose-generator
# Engine @anaroq/compose 1.0.0, catalog 2026.10.10, exposure caddy.
# Each site address comes from .env through the Caddy container's environment.
{$N8N_DOMAIN} {
reverse_proxy n8n:5678
}
{$QDRANT_DOMAIN} {
reverse_proxy qdrant:6333
}
Generated by the Compose Generator's engine for these apps behind Caddy. The files hold no secrets: copy .env.example to .env and fill it in.
Compose Analyzer result
14 findings: 0 error, 0 high, 0 medium, 14 low, 0 info.
Low14
- LowAQ-HLTH-002Line 8, column 5
services.n8nn8n has no
healthcheck.Fix: Add a healthcheck, so Compose and you can tell when the service is ready.
- LowAQ-HLTH-005Line 8, column 5
services.n8nn8n does not limit the size of its logs (
logging.options.max-size).Fix: Limit the log size so logs cannot fill the disk.
- LowAQ-SEC-005Line 11, column 7
services.n8n.environmentn8n passes secrets through
environment(DB_POSTGRESDB_PASSWORD, N8N_ENCRYPTION_KEY, N8N_RUNNERS_AUTH_TOKEN).Fix: Where the image supports it, use
secrets:or a_FILEvariable instead. - LowAQ-HLTH-002Line 34, column 5
services.n8n-runnern8n-runner has no
healthcheck.Fix: Add a healthcheck, so Compose and you can tell when the service is ready.
- LowAQ-HLTH-005Line 34, column 5
services.n8n-runnern8n-runner does not limit the size of its logs (
logging.options.max-size).Fix: Limit the log size so logs cannot fill the disk.
- LowAQ-SEC-005Line 37, column 7
services.n8n-runner.environmentn8n-runner passes secrets through
environment(N8N_RUNNERS_AUTH_TOKEN).Fix: Where the image supports it, use
secrets:or a_FILEvariable instead. - LowAQ-HLTH-005Line 45, column 5
services.n8n-postgresn8n-postgres does not limit the size of its logs (
logging.options.max-size).Fix: Limit the log size so logs cannot fill the disk.
- LowAQ-SEC-005Line 48, column 7
services.n8n-postgres.environmentn8n-postgres passes secrets through
environment(POSTGRES_PASSWORD).Fix: Where the image supports it, use
secrets:or a_FILEvariable instead. - LowAQ-HLTH-005Line 64, column 5
services.qdrantqdrant does not limit the size of its logs (
logging.options.max-size).Fix: Limit the log size so logs cannot fill the disk.
- LowAQ-SEC-005Line 67, column 7
services.qdrant.environmentqdrant passes secrets through
environment(QDRANT__SERVICE__API_KEY).Fix: Where the image supports it, use
secrets:or a_FILEvariable instead. - LowAQ-HLTH-002Line 85, column 5
services.caddycaddy has no
healthcheck.Fix: Add a healthcheck, so Compose and you can tell when the service is ready.
- LowAQ-HLTH-005Line 85, column 5
services.caddycaddy does not limit the size of its logs (
logging.options.max-size).Fix: Limit the log size so logs cannot fill the disk.
- LowAQ-PORT-005Line 91, column 9
services.caddy.ports.0This file publishes 3 port mappings. Docker publishes ports past ufw rules.
Fix: Filter them in the
DOCKER-USERchain or at the provider's firewall, or do not publish them. - LowAQ-VOL-005Line 95, column 9
services.caddy.volumes.0caddy bind-mounts the relative path ./Caddyfile.
Fix: That works, but the data lives in the directory you run Compose from: back it up.
The Compose Analyzer (ruleset 1.0.0) checked docker-compose.yml: 0 error, 0 high, 0 medium and 14 low findings. A recipe is published only without error and high findings.
Continue in the tools
The link opens the Compose Generator with these apps, options and exposure, and with the Deployment Spec of this sizing, so cloud-init and the Runbook start from the same state.
Runbook
Before the first start
- Copy
.env.exampleto.envnext todocker-compose.ymland fill in every value. Compose stops with an error while a required value is missing. - Create N8N_DB_PASSWORD on your own machine, for example with
openssl rand -hex 24. ANAROQ never creates passwords or keys. - Create N8N_ENCRYPTION_KEY on your own machine, for example with
openssl rand -hex 32. ANAROQ never creates passwords or keys. - Create N8N_RUNNERS_AUTH_TOKEN on your own machine, for example with
openssl rand -hex 32. ANAROQ never creates passwords or keys. - Create QDRANT_API_KEY on your own machine, for example with
openssl rand -hex 32. ANAROQ never creates passwords or keys. - Keep
Caddyfilenext todocker-compose.yml; the Caddy container mounts it read-only. - Ports 80 and 443 (TCP, and 443/UDP for HTTP/3) must be reachable from the internet for Caddy's automatic HTTPS.
- Before starting, point the A/AAAA records of the domain in N8N_DOMAIN (n8n) to this server; Caddy requests its certificate on first start.
- Before starting, point the A/AAAA records of the domain in QDRANT_DOMAIN (qdrant) to this server; Caddy requests its certificate on first start.
- Back up N8N_ENCRYPTION_KEY with the database: credentials cannot be decrypted without it.
- Start the stack with
docker compose up -dand follow it withdocker compose ps.
After the first start
Create the Qdrant credential
In n8n, add a Qdrant API credential with the URL below and, as API Key, the value of QDRANT_API_KEY from .env.
http://qdrant:6333Use it in a workflow
Add a Qdrant Vector Store node with that credential and a collection name; insert documents first, then retrieve from the same collection with the same embeddings model.
Check the collection
Open Qdrant's dashboard at its host name and /dashboard, sign in with the API key, and check that the collection holds points.
What the apps share
- Qdrant also gets a public host name through Caddy, for its dashboard. Every request needs the API key, so treat QDRANT_API_KEY like a password; n8n itself does not need Qdrant to be public.
- The embeddings come from a model n8n calls (hosted or your own); its size fixes the vector dimension, and a Qdrant collection keeps the dimension it was created with.
- Back up n8n's PostgreSQL and its encryption key together, and Qdrant's storage with a snapshot: the credential that holds Qdrant's key is encrypted with N8N_ENCRYPTION_KEY.
Alternatives
Basis
- Catalog
- 2026.10.10
- Sizing policy
- 2026.10.0
- Compose engine
- @anaroq/compose 1.0.0
- Analyzer ruleset
- 1.0.0
Sources
How profiles, sizes and Compose files are built is described in the methodology. Read the methodology