AI workflows with a vector database: n8n and Qdrant

n8n with its PostgreSQL and a Qdrant vector database on one server, behind Caddy. Workflows store and search embeddings in Qdrant without a hosted vector service.

Reviewed 14 October 2026

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

AppRole in the stackLoad levelCompose support
n8nRuns the workflows, with PostgreSQL for its own data.LightRun-tested
QdrantStores the embeddings and answers similarity searches.LightSupported

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.

PartvCPURAM (GiB)Disk (GiB)
n8n1.751.8758.5
Qdrant0.50.50.8
System baseline0.11.258.7
Safety headroom0.470.7252.7
Recommended total2.824.3520.7

Caddy is not included in the resource calculation of the Stack Sizer or the Deployment Spec.

Open these apps in the Stack Sizer

Files from the Compose Generator

docker-compose.yml
# 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: {}
.env.example
# 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=
Caddyfile
# 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

Error0
High0
Medium0
Low14
Info0

14 findings: 0 error, 0 high, 0 medium, 14 low, 0 info.

Low14

  1. LowAQ-HLTH-002Line 8, column 5services.n8n

    n8n has no healthcheck.

    Fix: Add a healthcheck, so Compose and you can tell when the service is ready.

  2. LowAQ-HLTH-005Line 8, column 5services.n8n

    n8n does not limit the size of its logs (logging.options.max-size).

    Fix: Limit the log size so logs cannot fill the disk.

  3. LowAQ-SEC-005Line 11, column 7services.n8n.environment

    n8n passes secrets through environment (DB_POSTGRESDB_PASSWORD, N8N_ENCRYPTION_KEY, N8N_RUNNERS_AUTH_TOKEN).

    Fix: Where the image supports it, use secrets: or a _FILE variable instead.

  4. LowAQ-HLTH-002Line 34, column 5services.n8n-runner

    n8n-runner has no healthcheck.

    Fix: Add a healthcheck, so Compose and you can tell when the service is ready.

  5. LowAQ-HLTH-005Line 34, column 5services.n8n-runner

    n8n-runner does not limit the size of its logs (logging.options.max-size).

    Fix: Limit the log size so logs cannot fill the disk.

  6. LowAQ-SEC-005Line 37, column 7services.n8n-runner.environment

    n8n-runner passes secrets through environment (N8N_RUNNERS_AUTH_TOKEN).

    Fix: Where the image supports it, use secrets: or a _FILE variable instead.

  7. LowAQ-HLTH-005Line 45, column 5services.n8n-postgres

    n8n-postgres does not limit the size of its logs (logging.options.max-size).

    Fix: Limit the log size so logs cannot fill the disk.

  8. LowAQ-SEC-005Line 48, column 7services.n8n-postgres.environment

    n8n-postgres passes secrets through environment (POSTGRES_PASSWORD).

    Fix: Where the image supports it, use secrets: or a _FILE variable instead.

  9. LowAQ-HLTH-005Line 64, column 5services.qdrant

    qdrant does not limit the size of its logs (logging.options.max-size).

    Fix: Limit the log size so logs cannot fill the disk.

  10. LowAQ-SEC-005Line 67, column 7services.qdrant.environment

    qdrant passes secrets through environment (QDRANT__SERVICE__API_KEY).

    Fix: Where the image supports it, use secrets: or a _FILE variable instead.

  11. LowAQ-HLTH-002Line 85, column 5services.caddy

    caddy has no healthcheck.

    Fix: Add a healthcheck, so Compose and you can tell when the service is ready.

  12. LowAQ-HLTH-005Line 85, column 5services.caddy

    caddy does not limit the size of its logs (logging.options.max-size).

    Fix: Limit the log size so logs cannot fill the disk.

  13. LowAQ-PORT-005Line 91, column 9services.caddy.ports.0

    This file publishes 3 port mappings. Docker publishes ports past ufw rules.

    Fix: Filter them in the DOCKER-USER chain or at the provider's firewall, or do not publish them.

  14. LowAQ-VOL-005Line 95, column 9services.caddy.volumes.0

    caddy 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.

Open in the journey

Runbook

Before the first start

  1. Copy .env.example to .env next to docker-compose.yml and fill in every value. Compose stops with an error while a required value is missing.
  2. Create N8N_DB_PASSWORD on your own machine, for example with openssl rand -hex 24. ANAROQ never creates passwords or keys.
  3. Create N8N_ENCRYPTION_KEY on your own machine, for example with openssl rand -hex 32. ANAROQ never creates passwords or keys.
  4. Create N8N_RUNNERS_AUTH_TOKEN on your own machine, for example with openssl rand -hex 32. ANAROQ never creates passwords or keys.
  5. Create QDRANT_API_KEY on your own machine, for example with openssl rand -hex 32. ANAROQ never creates passwords or keys.
  6. Keep Caddyfile next to docker-compose.yml; the Caddy container mounts it read-only.
  7. Ports 80 and 443 (TCP, and 443/UDP for HTTP/3) must be reachable from the internet for Caddy's automatic HTTPS.
  8. Before starting, point the A/AAAA records of the domain in N8N_DOMAIN (n8n) to this server; Caddy requests its certificate on first start.
  9. Before starting, point the A/AAAA records of the domain in QDRANT_DOMAIN (qdrant) to this server; Caddy requests its certificate on first start.
  10. Back up N8N_ENCRYPTION_KEY with the database: credentials cannot be decrypted without it.
  11. Start the stack with docker compose up -d and follow it with docker compose ps.

After the first start

  1. 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:6333
  2. Use 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.

  3. 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

  • n8n alone can keep small vector sets in memory; that is lost on restart, so it suits tests, not a knowledge base. n8n
  • Qdrant also runs as a managed service by its makers; that removes a container but keeps the vectors with a provider. Qdrant

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

All self-hosted apps