n8n in production in 2026: Docker, PostgreSQL, a self-hosted AI stack, and 10 real-world automations
n8n in production in 2026: Docker, PostgreSQL, a self-hosted AI stack, and 10 real-world automations

Category: Automation and web tools | Updated: September 21, 2026.
Topic: Production engineering architecture for n8n, Kubernetes and Helm deployment, a self-hosted AI stack, and 10 operational automations
Author: Zoran Knežević | Format: Comprehensive production guide
Last technical review: September 21, 2026.

Verified: Checked on September 21, 2026. The stable/latest n8n release at the time was 2.39.8. Version 2.40.3 was available as a prerelease. Check the current stable release before every production upgrade.

Quick guide

  • I just need a stable Docker deployment: See sections 01 to 06 for architecture, .env, Docker Compose, and Caddy.
  • I need security, backups, and maintenance: See sections 07 to 12 for SSRF, task runners, backups, upgrades, and pruning.
  • I need to scale on Kubernetes (K8s / Helm): See section 13 for the official Helm chart 1.12.0, KEDA autoscaling, Ingress, and PDB.
  • I need a local AI stack without subscriptions: See section 14 for PostgreSQL with pgvector, Ollama, SearXNG, and Crawl4AI.
  • I need practical business examples: See section 17 for 10 operational workflows with SQL tables and code.
  • I have a server problem: See section 19 for troubleshooting and common errors.

TL;DR:

  • A small or medium system can run with Caddy -> n8n -> PostgreSQL on one server.
  • For higher concurrency and horizontal scaling, switch to Queue Mode: Caddy -> n8n main -> Redis -> worker(s) -> PostgreSQL.
  • For multiple servers, dynamic load, and teams experienced with K8s, use the official n8n Helm chart with KEDA worker autoscaling.
  • For local AI, use Ollama. For vector search, use PostgreSQL + pgvector 0.8.6, or dedicated Qdrant for very large databases.

About the examples: The examples in this guide are reference architectures and starting points for production patterns. Test them in your specific environment before using them with real data, billing, or critical systems.


Introduction: From a workflow to a stable system

Building a workflow that successfully reads an API and sends a notification can take a few minutes.

A production system, however, must handle real operational challenges:

  • Server restarts and temporary outages.
  • Unavailable external APIs and HTTP 429 responses (rate limiting).
  • Expired access tokens and changed authorization.
  • Duplicate webhook requests and the need for idempotency.
  • Changes to the HTML structure of monitored websites.
  • Unexpected or inaccurate language model output.
  • Database growth from hundreds of thousands of recorded executions.

That is why self-hosted n8n in production should be set up as reliable backend infrastructure, not as an ad hoc container.

Standardized terms used in this guide

For technical precision, we use consistent terminology throughout:

  • Node: A basic functional block in a workflow, such as HTTP Request, Code, or Postgres.
  • Credentials: Encrypted authentication data, API keys, and OAuth tokens.
  • Workflow: A connected set of nodes that performs a defined task.
  • Worker: A separate process dedicated to executing jobs from a queue.
  • Queue Mode: A distributed architecture where Redis coordinates jobs between the main instance and workers.

Which architecture should you choose?

Before deploying the infrastructure, determine the right level of complexity.

text
OPTION A: Small to medium system (simple production setup)
┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│    Caddy     │────────►│   n8n Main   │────────►│  PostgreSQL  │
│ (HTTPS proxy)│         │(Orchestration│         │  17 or 18   │
└──────────────┘         │ and execution)│        └──────────────┘
                         └──────────────┘

OPTION B: Higher concurrency (Queue Mode distribution)
┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│    Caddy     │────────►│   n8n Main   │────────►│  PostgreSQL  │
│ (HTTPS proxy)│         │ (Webhook/UI) │         │  17 or 18   │
└──────────────┘         └──────┬───────┘         └──────▲───────┘
                                │                        │
                         Enqueue│                        │ Write
                                ▼                        │ state
                         ┌──────────────┐                │
                         │ Redis Queue  │                │
                         └──────┬───────┘                │
                                │                        │
                         Dequeue│                        │
                                ▼                        │
                         ┌──────────────┐                │
                         │ n8n Worker(s)│────────────────┘
                         │ (Execution)  │
                         └──────────────┘

Option A: Small and medium systems

  • Topology: Caddy -> n8n -> PostgreSQL
  • When to use it: Predictable execution rates, low concurrency, and shorter jobs without heavy binary data processing or sudden webhook bursts.
  • Advantages: A small stack can run with relatively modest resources, but actual RAM and CPU needs depend on concurrent executions, Code and AI nodes, payloads, and binary data. Set limits based on measurements of your actual workload. Maintenance is simple because Redis is not required.
  • Limitations: Long-running Code nodes or heavy data processing can slow down the acceptance of new webhooks.

Option B: High concurrency and scaling (Queue Mode)

  • Topology: Caddy -> n8n main -> Redis -> n8n worker(s) -> PostgreSQL
  • When to use it: Many concurrent webhook calls, large-file processing, or dozens of jobs running in parallel.
  • Advantages: The main process accepts a webhook immediately and sends it to the Redis queue, while workers process jobs independently.
  • Trade-off: Adds a Redis instance and workers that also need monitoring and updates.

Choosing a database: PostgreSQL versus SQLite

SQLite is practical for testing and small instances. For production installations that run continuously and have multiple workflows or users, n8n recommends PostgreSQL.

Current n8n documentation actively maintains support for PostgreSQL 17 and 18. Check the documentation for compatibility with older versions before installing.

Reasons to use PostgreSQL in production:

  • Concurrency: SQLite locks the entire database during writes, which can cause bottlenecks when work runs in parallel.
  • Reliable backups: PostgreSQL supports consistent, live logical backups with pg_dump without stopping the service.
  • Extensibility: The database can be extended with modules such as pgvector for vector storage.

Protecting secrets and the role of N8N_ENCRYPTION_KEY

All credentials stored in the database, including passwords, API tokens, and SSH keys, are encrypted with the instance's master key, set through the N8N_ENCRYPTION_KEY variable.

Important distinction:
If you lose the original N8N_ENCRYPTION_KEY, the database and workflow definitions are not automatically lost, but n8n can no longer decrypt credentials and secrets encrypted with that key. You will need to recreate the credentials if you cannot recover the original key.

Rules for managing the encryption key:

  1. Generate a cryptographically strong key at least 32 bytes long with openssl rand -hex 32.
  2. Store it separately from the server, in a password-protected password manager or a secret vault such as HashiCorp Vault.
  3. In a Queue Mode installation, the main instance and every worker must use the exact same N8N_ENCRYPTION_KEY.
  4. Newer n8n features offer rotation of internal data encryption keys. This does not replace the N8N_ENCRYPTION_KEY master key. It is an internal process that requires a full database dump before activation because it is an irreversible migration.

Environment configuration: The .env file

Create a .env file in the working directory and restrict its permissions with chmod 600 .env:

bash
# Pin major and minor image tags
N8N_VERSION=2.39.8
POSTGRES_VERSION=18
REDIS_VERSION=8-alpine
CADDY_VERSION=2.9-alpine

# Domain and network settings
N8N_DOMAIN=n8n.example.com
GENERIC_TIMEZONE=Europe/Belgrade
TZ=Europe/Belgrade

# Master encryption key (generate with: openssl rand -hex 32)
N8N_ENCRYPTION_KEY=replace_this_secret_key_before_starting_the_infrastructure

# PostgreSQL settings
POSTGRES_DB=n8n
POSTGRES_USER=n8n_admin
POSTGRES_PASSWORD=replace_with_a_strong_postgresql_password

# Redis settings
REDIS_PASSWORD=replace_with_a_strong_redis_password

# Security token for the external task runner, if used
RUNNERS_AUTH_TOKEN=replace_with_a_strong_runner_token

Complete production Docker Compose stack

This Compose file defines the entire system: Caddy as a reverse proxy with automatic HTTPS, PostgreSQL 18, Redis 8, the n8n main instance, and an n8n worker, with log rotation and health checks.

yaml
services:
  caddy:
    image: caddy:${CADDY_VERSION}
    restart: unless-stopped
    environment:
      N8N_DOMAIN: ${N8N_DOMAIN}
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web_network
    depends_on:
      - n8n
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  postgres:
    image: postgres:${POSTGRES_VERSION}
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGDATA: /var/lib/postgresql/data
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
    networks:
      - backend_network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    image: redis:${REDIS_VERSION}
    restart: unless-stopped
    command:
      - redis-server
      - --requirepass
      - ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - backend_network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  n8n:
    image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
    restart: unless-stopped
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}

      EXECUTIONS_MODE: queue

      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379
      QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}

      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}

      N8N_HOST: ${N8N_DOMAIN}
      N8N_EDITOR_BASE_URL: https://${N8N_DOMAIN}/
      N8N_PROTOCOL: https
      N8N_WEBHOOK_URL: https://${N8N_DOMAIN}/
      N8N_PROXY_HOPS: 1

      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${TZ}

      EXECUTIONS_DATA_PRUNE: "true"
      EXECUTIONS_DATA_MAX_AGE: 168
      EXECUTIONS_DATA_PRUNE_MAX_COUNT: 50000

      N8N_METRICS: "true"
      N8N_SSRF_PROTECTION_ENABLED: "true"
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"

      # Set the concurrency limit based on workload and available resources
      N8N_CONCURRENCY_PRODUCTION_LIMIT: "15"

    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - n8n_data:/home/node/.n8n
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness | grep -q '\"status\":\"ok\"' || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    networks:
      - backend_network
      - egress_network
      - web_network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  worker:
    image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
    restart: unless-stopped
    command: worker
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}

      EXECUTIONS_MODE: queue

      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379
      QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}

      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}

      QUEUE_HEALTH_CHECK_ACTIVE: "true"
      N8N_SSRF_PROTECTION_ENABLED: "true"
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - n8n_data:/home/node/.n8n
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s
    networks:
      - backend_network
      - egress_network
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  caddy_data:
  caddy_config:
  postgres_data:
  redis_data:
  n8n_data:

networks:
  backend_network:
    internal: true
  egress_network:
  web_network:

Network topology and egress: The worker has no published inbound port, but it needs outbound internet access to run workflow integrations. For this reason, the worker and main n8n instance are connected to egress_network, while the database and Redis remain isolated on backend_network without direct internet access.

Caddyfile configuration

Create a Caddyfile in the same directory as compose.yml:

caddyfile
{$N8N_DOMAIN} {
    reverse_proxy n8n:5678 {
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-Host {host}
    }
}

Important notes about the network and proxy layer

  • N8N_PROXY_HOPS=1: This setting tells n8n that there is exactly one trusted proxy, Caddy. If you use Cloudflare in proxied mode in front of Caddy, set the number of trusted hops to 2.
  • Deprecated variables: Starting with n8n 2.35.0, WEBHOOK_URL is marked as deprecated. Use N8N_WEBHOOK_URL=https://${N8N_DOMAIN}/ instead.
  • PostgreSQL 18 PGDATA: PostgreSQL 18 changed the internal data directory structure, so explicitly setting PGDATA: /var/lib/postgresql/data is required for volume persistence.

First deployment and validation steps

After preparing .env, Caddyfile, and compose.yml, start the stack with these commands:

bash
# 1. Check the syntax and configured variables
docker compose config

# 2. Pull the pinned images
docker compose pull

# 3. Start the containers in the background
docker compose up -d

# 4. Check service status and health checks
docker compose ps

# 5. Review initial logs and database migrations
docker compose logs -f n8n

# 6. Check the internal readiness endpoint from inside the container
docker compose exec -T n8n \
  wget -qO- http://127.0.0.1:5678/healthz/readiness

# Optional: Check externally after setting up DNS and the Caddy TLS certificate
curl -fsS https://${N8N_DOMAIN}/healthz/readiness

The expected readiness response is {"status":"ok"}. If you do not want to expose the health endpoint publicly through the reverse proxy, use only the internal docker compose exec check for operational monitoring.


Optional profile for maximum isolation (External Task Runner)

When workflows run JavaScript or Python inside Code nodes, running that code in the main process carries some risk on multi-user systems.

For these environments, n8n offers an External Task Runner architecture:

  • Add N8N_RUNNERS_MODE=external and N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN} to the main n8n container.
  • Add a separate ghcr.io/n8n-io/runners:${N8N_VERSION} container with a restrictive security profile:
    • read_only: true locks the container filesystem against writes.
    • security_opt: [no-new-privileges:true] prevents privilege escalation.
    • Run it as an unprivileged user.

Important: The main process (n8n), workers (worker), and runners (runners) must all use identical code versions after every update to avoid protocol incompatibilities.


Managing execution data (Execution Pruning)

A common misconception is that execution data is archived permanently by default.

Retention rule:
Execution pruning removes execution history according to defined age and record-count rules. If certain data must be retained permanently for business records or audits, write it directly to a dedicated database such as PostgreSQL. Do not rely on n8n execution history for long-term retention.

The Compose configuration sets:

text
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168
EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000

This retains data for up to 7 days (168 hours) or a maximum of 50,000 records. For processes that run every second, consider setting:

text
EXECUTIONS_DATA_SAVE_ON_ERROR=all
EXECUTIONS_DATA_SAVE_ON_SUCCESS=none

This stores only executions that encountered an error in the database.


Security: SSRF protection and n8n Security Audit

Built-in SSRF protection

The N8N_SSRF_PROTECTION_ENABLED=true setting prevents HTTP Request nodes in workflows from sending requests to:

  • localhost and 127.0.0.1 inside the container network.
  • Local RFC 1918 networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16).
  • Cloud metadata services (169.254.169.254).

If access to an internal service is legitimately required, do not use the deprecated N8N_SSRF_ALLOWED_HOSTS setting, which is no longer in use.
The current official variables are:

  • For a hostname allowlist: N8N_SSRF_ALLOWED_HOSTNAMES=internal-api.local,service.lan.
  • For IP addresses or CIDR ranges: N8N_SSRF_ALLOWED_IP_RANGES=192.168.1.50/32,10.10.0.0/24.

The allowlist takes precedence during evaluation, but always define only the minimum internal host or range the workflow actually needs.

n8n Security Audit

Run the built-in audit with:

bash
docker compose exec n8n n8n audit

The Security Audit reports:

  • Expressions in SQL Execute Query fields, expressions in Query Parameters fields, and unused Query Parameters fields.
  • Filesystem nodes with disk access.
  • Risky built-in nodes.
  • External and community or custom nodes.
  • Unprotected public webhooks.
  • Missing security settings.
  • An outdated instance version.

Warning: A Security Audit does not replace a professional penetration test, SAST/DAST analysis, or a dedicated SQL vulnerability scanner.


Backups, disaster recovery, and the 3-2-1 rule

text
BACKUP FLOW:
┌─────────────────────┐
│ PostgreSQL 18 database│──► pg_dump (stdout) ──► Local .dump file
└─────────────────────┘
┌─────────────────────┐
│ n8n encryption and env│──► Encrypted storage ──► Password manager / vault
└─────────────────────┘
┌─────────────────────┐
│ Workflow and creds JSON│──► CLI export ───────► Git / external archive
└─────────────────────┘
                               │
                               ▼
                  Copy off-site (S3 / NAS)

The n8n export:workflow --backup command exports workflow definitions only. Export credentials separately with n8n export:credentials --backup.

PostgreSQL database dump (primary source of truth)

Use the -T option (no pseudo-TTY) so terminal control characters are not written into the binary output:

bash
docker compose exec -T postgres pg_dump \
  -Fc \
  -U n8n_admin \
  -d n8n \
  > /var/backups/n8n/n8n-db-$(date +%F_%H%M%S).dump

Export workflows and credentials to separate directories

bash
# Export workflow definitions
docker compose exec n8n n8n export:workflow --backup --output=/backup/workflows/

# Export encrypted credentials
docker compose exec n8n n8n export:credentials --backup --output=/backup/credentials/

The 3-2-1 rule and RPO / RTO

  • 3 copies of the data: The primary database on the server, a local .dump snapshot, and an off-site copy.
  • 2 different media: A local SSD and external object storage or a NAS.
  • 1 off-site copy: Protects against a physical failure at the provider.
  • RPO (Recovery Point Objective): The maximum acceptable data loss. If you create a dump every 6 hours, your RPO is 6 hours.
  • RTO (Recovery Time Objective): The time required to restore the system from scratch after a failure, for example 30 minutes with a prepared Compose file and restore script.

The frequency of recovery drills depends on system criticality and defined RPO/RTO requirements. For serious systems, schedule automated periodic tests on a clean test instance.


Safe upgrade and rollback procedure

Follow these steps when moving to a new n8n version:

  1. Create a backup: Take a fresh pg_dump of the database.
  2. Secure secrets: Confirm that the .env file and N8N_ENCRYPTION_KEY are available.
  3. Review changes: Read the official release notes for the target version to identify breaking changes.
  4. Change the version: Update the version number in .env, for example N8N_VERSION=2.40.0.
  5. Synchronize services: Ensure that the worker and runners containers move to the same version.
  6. Pull and apply the update:
    bash
    docker compose pull
    docker compose up -d
    
  7. Review migrations: Follow startup logs with docker compose logs -f n8n to confirm that database migrations complete without errors.
  8. Validate: Check the readiness endpoint (/healthz/readiness) and run one representative test workflow.

Rollback warning: Reverting to a previous Docker image is not always enough if the newer version has already applied irreversible database schema migrations. Recovery then requires restoring the previous SQL backup with pg_restore.


Binary data in Queue Mode installations

Understand how storage is divided in n8n:

  • PostgreSQL: Stores workflow structure, state, and metadata.
  • Redis: Stores job queues and temporary messages.
  • Local volume (n8n_data): Stores configuration and local binary files.

If you run workers on multiple physical servers, a shared local volume at /home/node/.n8n is not a universal horizontal scaling solution.
External S3 binary storage in n8n is a self-hosted Enterprise feature. n8n officially supports AWS S3. Documentation describes other S3-compatible systems, such as MinIO and Ceph, as technically possible but not officially supported. If you handle large files such as video, archives, or large CSV files, design storage outside the n8n payload itself, for example by downloading directly from a URL or using a dedicated storage service.


Kubernetes (K8s): When Docker Compose is no longer enough

Docker Compose provides an excellent foundation for a single VPS or small server. More demanding production environments eventually outgrow the capacity of one virtual machine.

Why Kubernetes is not required for every team

For one VPS, a small team, and up to a few dozen ordinary workflows:

text
Caddy -> n8n -> PostgreSQL

or:

text
Caddy -> n8n main -> Redis -> workers -> PostgreSQL

Docker Compose is simpler, easier to understand, and more practical to operate.

Engineering rule:
If you are just learning n8n and have one server, Kubernetes is probably solving a problem you do not have yet.

Introduce Kubernetes (K8s) when there are clear technical and organizational reasons:

  • Your organization already runs and actively maintains Kubernetes clusters.
  • You need more n8n worker replicas distributed across several physical nodes.
  • Workload varies significantly during the day and needs autoscaling.
  • You experience large sudden bursts of webhook traffic.
  • You need rolling upgrades without downtime.
  • You need advanced K8s guarantees such as PodDisruptionBudget and NetworkPolicy.
  • You use different node pools, for example CPU-intensive nodes for workers and memory-optimized nodes for the database.
  • You need standardized management of secrets, Ingress traffic, and telemetry through a company observability stack.
  • You require a full high-availability model.

Official n8n Helm chart (n8n-hosting)

An important development in 2026 is that n8n now officially maintains and recommends a production Helm chart in its n8n-hosting repository, distributed through an OCI registry:
oci://ghcr.io/n8n-io/n8n-helm-chart/n8n

Instead of relying on unrelated community charts, the official chart supports:

  • Standalone deployment and distributed Queue Mode.
  • Scaling the number of worker pods.
  • Dedicated webhookProcessor pods for high-volume webhook intake.
  • Task runner sidecar containers for code isolation.
  • Ingress and TLS integration, including cert-manager support.
  • Health and readiness probes (/healthz and /healthz/readiness).
  • Horizontal Pod Autoscaler (HPA) and KEDA event-driven autoscaling.
  • PodDisruptionBudget and NetworkPolicy.
  • Node placement, affinity, and anti-affinity rules.
  • Multi-main configuration. Note: n8n multi-main HA requires an appropriate Enterprise license.
  • Connections to external PostgreSQL and Redis services.
  • External object storage configuration for binary data.

Requirements for an n8n Kubernetes deployment

According to the current official Helm chart documentation, checked on September 21, 2026, the official repository lists stable chart 1.12.0, released on September 16, 2026. Minimum system requirements include:

  • Helm 3.12+
  • Kubernetes 1.25+

Chart version and application version are different:
Helm charts and the n8n application have separate release cycles. Chart 1.12.0 was released and tested with appVersion 2.39.6, while the stable n8n channel at the time of review was 2.39.8. If you manually override image.tag, test the new combination before production.

Architecture in Kubernetes Queue Mode

text
                             INTERNET
                                 |
                                 v
                      Ingress / Load Balancer
                                 |
                    +------------+------------+
                    |                         |
                 n8n main              webhook processors
                (Editor/API)                (webhooks only)
                    |                         |
                    +------------+------------+
                                 | Enqueue
                                 v
                               Redis
                           (Queue broker)
                                 |
                        +--------+--------+
                        | Dequeue         |
                        v                 v
                    worker-1          worker-2          worker-N
                    +-------+         +-------+         +-------+
                    |Worker |         |Worker |         |Worker |
                    |+Runner|         |+Runner|         |+Runner|
                    +-------+         +-------+         +-------+
                        |                 |                 |
                        +-----------------+-----------------+
                                          |
                                          v
                                      PostgreSQL
                                     (Shared database)

When using maximum code isolation, each worker pod can contain an n8n worker container and a companion task runner sidecar container of the same version.

Two operating modes in the official chart: Standalone versus Queue Mode

  1. Standalone mode:

    • Runs a single n8n pod.
    • Can use SQLite with a PersistentVolumeClaim (PVC).
    • Does not require Redis or worker pods.
    • Suits quick tests, internal tools, or simple K8s environments where high availability is not the primary goal. It is not an HA production architecture.
  2. Queue Mode:

    • The official model for scalable production.
    • Requires an external PostgreSQL and Redis service, an n8n main pod, and worker pods.
    • The official chart assumes PostgreSQL and Redis are prepared outside the chart or provided as external services.

Database choice: in-cluster or managed?
Running PostgreSQL and Redis inside the K8s cluster gives you full control, but your team takes on the operational work of configuring replication, storage classes, backups, and recovery. If you already use a cloud platform such as AWS, GCP, or Azure, managed services such as AWS RDS and ElastiCache or GCP Cloud SQL and Memorystore greatly reduce operational risk.

Installing and upgrading n8n with Helm

Install the chart directly from the OCI registry and pin its version:

bash
# Install the official chart at pinned version 1.12.0
helm install n8n \
  oci://ghcr.io/n8n-io/n8n-helm-chart/n8n \
  --namespace n8n \
  --create-namespace \
  --version 1.12.0 \
  -f values.yaml

For a later upgrade, run:

bash
# Upgrade after creating a database backup
helm upgrade n8n \
  oci://ghcr.io/n8n-io/n8n-helm-chart/n8n \
  --namespace n8n \
  --version 1.12.0 \
  -f values.yaml

Never use a floating latest tag in production. Always pin both the Helm chart version and the n8n application image version in values.yaml.

Minimal values.yaml for n8n K8s Queue Mode

The following example shows a basic configuration for Queue Mode:

yaml
# By default, chart 1.12.0 uses appVersion: 2.39.6
# To use stable version 2.39.8, set it explicitly as an override:
image:
  tag: "2.39.8" # Explicit override of the chart appVersion

# Distributed mode configuration
queueMode:
  enabled: true
  workerReplicaCount: 2
  workerConcurrency: 10

# External PostgreSQL database
database:
  type: postgresdb
  useExternal: true
  host: "postgres.internal.lan"
  port: 5432
  database: "n8n"
  user: "n8n_admin"
  passwordSecret:
    name: "n8n-db-credentials"
    key: "password"

# External Redis queue server
redis:
  enabled: true
  useExternal: true
  host: "redis.internal.lan"
  port: 6379
  passwordSecret:
    name: "n8n-redis-credentials"
    key: "password"

# Manage secrets through an existing Kubernetes Secret
secretRefs:
  existingSecret: "n8n-core-secrets"

# Additional non-sensitive environment variables
config:
  extraEnv:
    N8N_WEBHOOK_URL: "https://n8n.example.com/"
    N8N_PROXY_HOPS: "1"
    N8N_SSRF_PROTECTION_ENABLED: "true"

Do not store secrets in Git: Secret management in K8s

Never put sensitive variables, especially N8N_ENCRYPTION_KEY, in plain text in values.yaml files stored in Git repositories.

For a production deployment, use the official pattern where the chart reads secrets from a previously created Kubernetes Secret through secretRefs.existingSecret:

bash
kubectl create secret generic n8n-core-secrets \
  --from-literal=N8N_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
  --from-literal=N8N_HOST="n8n.example.com" \
  --from-literal=N8N_PORT="5678" \
  --from-literal=N8N_PROTOCOL="http"

Pass database and Redis passwords through the dedicated database.passwordSecret and redis.passwordSecret mappings.

Understand the differences in protection levels:

  • ConfigMap: Intended only for non-sensitive configuration parameters.
  • Kubernetes Secret: A standard object, but values are only base64-encoded by default. It requires encryption-at-rest at the etcd database level and strict RBAC rules.
  • External secret managers: Professional environments use the External Secrets Operator (ESO) or a HashiCorp Vault agent to retrieve actual secrets from AWS Secrets Manager, Google Secret Manager, or HashiCorp Vault directly into a pod at startup.

Ingress, webhook processors, and TLS termination

In a Kubernetes environment, replace Caddy from the Compose example with a standard Ingress controller:

  • Ingress controller: ingress-nginx, Traefik, or cloud solutions such as AWS ALB and GCLB.
  • TLS certificates: Issue and renew them automatically through cert-manager connected to Let's Encrypt.

Dedicated webhook processors

One key benefit of the official chart is the option to enable dedicated webhookProcessor pods.
If the system receives hundreds of webhook requests per second, split traffic at the Ingress layer:

  • User interface and API calls go to the n8n main pod.
  • Public webhook calls (/webhook/*) go directly to webhookProcessor pods, which only enqueue the job in Redis and immediately return an HTTP acknowledgment.
  • Worker pods process jobs from Redis in the background without slowing down the main interface.

Autoscaling: HPA versus KEDA

Kubernetes has two complementary approaches to autoscaling:

  1. Horizontal Pod Autoscaler (HPA):

    • Scales pods based on resource usage such as CPU and memory.
    • Works well for main pods and API processors when CPU use grows linearly with the number of users.
  2. KEDA (Kubernetes Event-driven Autoscaling):

    • The official n8n chart has direct KEDA integration for workers.
    • Scaling is based not on CPU utilization but on the length of the Redis queue:
      text
      Empty Redis queue    -> 2 workers (minimum capacity)
      Queue grows to 50    -> 5 workers (automatic scale-up)
      Sudden burst of 500  -> 15 workers (maximum scale)
      Jobs completed       -> Gradually scale down to 2 workers
      

Scaling limitation: Adding more n8n workers will not speed up processing if the bottleneck is a slow database, a congested Redis instance, or an external API returning HTTP 429 rate-limit responses.

Health checks and probes (liveness and readiness)

The official chart configures appropriate probes automatically:

  • Main pod:
    • Liveness probe: /healthz, to determine whether the process is stuck and the container needs to restart.
    • Readiness probe: /healthz/readiness, to determine whether the database is connected and the pod can receive traffic.
  • Worker pod: A worker has no Kubernetes Service or public HTTP probe. By default, the official chart uses an exec liveness and readiness check to confirm that the n8n worker process is running. Monitor Redis queue health and actual execution throughput separately through metrics and observability.

PodDisruptionBudget (PDB) and NetworkPolicy hardening

  • PodDisruptionBudget (PDB): A rule that tells Kubernetes to retain a minimum number of active worker replicas, for example minAvailable: 1, during node maintenance operations such as a node drain. A PDB provides stability only when the system already has multiple replicas.
  • NetworkPolicy: Restricts network traffic inside the cluster. The policy ensures that only n8n main, webhookProcessor, and worker pods can communicate with Redis and the database, while blocking direct external access. Network policies require a compatible CNI plugin such as Calico or Cilium.
  • ServiceAccount hardening: If the n8n process does not use the Kubernetes API to orchestrate other containers, a good security practice is to disable automatic service token mounting with automountServiceAccountToken: false.

Stateful services and PersistentVolume configuration

  • Standalone mode: SQLite requires a stable PersistentVolumeClaim so data is not lost when a pod is replaced.
  • Queue Mode: All state is stored in PostgreSQL. Worker pods are ephemeral. Do not use an individual pod's local disk as shared storage across replicas.
  • Multi-main note: multiMain.enabled=true is for Enterprise license holders only. In this case, configure Ingress with sticky sessions (session affinity) so a client session stays on the same node.

Rolling updates and graceful worker shutdown

When a new version is released or configuration changes, Kubernetes replaces old pods with new ones.

Workers may be running long workflows at that time. The official chart currently sets a grace period, defaulting to terminationGracePeriodSeconds: 60, and a preStop hook that sleeps for 10 seconds to stop new jobs from being accepted. Longer workflows may require adjusting the graceful shutdown timeout and Kubernetes termination period. Do not assume the default 60 seconds is enough for every workload. When it receives a SIGTERM signal, a worker stops fetching new jobs from Redis, completes active executions, and then shuts down safely, preventing jobs from being interrupted mid-process.

Table: Docker Compose or Kubernetes?

Scenario / criterionDocker ComposeKubernetes (Helm)
One VPS serverRecommended, simple and stableUsually unnecessary complexity
Predictable workload without sudden burstsRecommendedUnnecessary operational overhead
Team already uses K8sOptionalNatural, standardized choice
Horizontal worker scalingLimited to one hostExcellent, spread across multiple nodes
Autoscaling based on job queueRequires custom scriptsBuilt in through KEDA
Rolling updates without interruptionBasic, container restartNative, built-in K8s primitives
Recovery from a hardware node failureRequires manual interventionAutomatic rescheduling to a healthy node
Distribution across multiple machinesComplex, requires network storageNatively supported
Operational maintenance complexityLow, a few YAML filesConsiderably higher, requires K8s expertise

Three deployment tiers: From simple to enterprise cluster

text
TIER 1: Simple (small team, moderate workload, controlled concurrency)
Caddy -> n8n -> PostgreSQL

TIER 2: Scalable on one server (higher concurrency)
Caddy -> n8n main -> Redis -> n8n workers -> PostgreSQL

TIER 3: Kubernetes cluster (multi-node, HA, and autoscaling)
Ingress -> n8n main and webhook processors -> Redis -> autoscaled workers (KEDA) -> PostgreSQL

Golden engineering rule:
Do not move to the next tier just because it exists. Move only when the previous tier becomes a real technical limit for your business.

Monitoring and backups in a Kubernetes environment

  • What to monitor in K8s: Alongside standard n8n application metrics (/metrics), monitor pod restart counts (restartCount), failed readiness probes, Redis queue depth, active database connections, OOMKilled events caused by insufficient memory, pending pods, and KEDA scaling events.
  • Backups in K8s:
    Important warning: A Helm release file or values.yaml is not a backup. A disk snapshot alone is not a substitute for a transactionally consistent database backup.
    In Kubernetes, the primary source of truth is still a consistent pg_dump of PostgreSQL, the N8N_ENCRYPTION_KEY stored securely in a dedicated secret management system, and a regularly tested restore process in an isolated environment.
  • GitOps integration: If your organization follows GitOps principles, Helm chart configuration and manifests can be safely orchestrated with tools such as Argo CD or Flux, provided secrets are never stored in plain text in the Git repository.

Docker Compose and Kubernetes are not competing religions. Compose works well when one or a few machines can handle the workload reliably. Kubernetes becomes useful when the challenges are replica count, workload placement, autoscaling, and cluster-level availability. n8n now has an official Helm path for both scenarios, but add complexity only when it provides a concrete benefit.


Self-hosted AI stack without SaaS subscriptions

text
SELF-HOSTED AI AND RETRIEVAL ECOSYSTEM:
+--------------------------------------------------------+
|                    n8n ORCHESTRATOR                     |
+-------+--------------+---------------+-----------------+
        |              |               |
        v              v               v
 +-------------+ +-------------+ +-------------+
 | PostgreSQL  | |   Ollama    | |  Crawl4AI   |
 | + pgvector  | | (Local LLM  | | (Structured |
 | (Vectors)   | | + embeddings)| | Markdown)  |
 +-------------+ +-------------+ +-------------+
        |              |               |
        v              v               v
 +-------------+ +-------------+ +-------------+
 |   SearXNG   | | changedetect| |    ntfy     |
 | (Private    | |   ion.io    | | (Private    |
 |  search)    | | (DOM monitor)| | notifications|
 +-------------+ +-------------+ +-------------+

n8n licensing in practice

  • Internal automation for your own company or personal use: The n8n Sustainable Use License covers these cases without a subscription.
  • Hosting workflows and client data on your own server: n8n lists the Enterprise license as the appropriate model.
  • Embedding the n8n canvas in a commercial product: Check the commercial Embed or OEM model.
  • Consulting work: If you set up an instance on a client's infrastructure, the client's use case applies.
    (Note: This is technical guidance, not legal advice.)

PostgreSQL + pgvector versus dedicated Qdrant

  • Basic deployment: Use the standard image postgres:${POSTGRES_VERSION}.
  • RAG and vector deployment: Requires an image with the pgvector module installed, such as pgvector/pgvector:0.8.6-pg18.
    The command CREATE EXTENSION IF NOT EXISTS vector; succeeds only if the extension has already been installed in the Postgres container.
  • When to choose Qdrant: If the system grows to hundreds of thousands of vectors and requires constant hybrid search, dedicated Qdrant, with REST on port 6333, gRPC on port 6334, and an Apache 2.0 license, provides specialized memory optimization.

Local AI: Ollama

Ollama lets you run open models locally, including Llama, Mistral, Qwen, and mxbai-embed-large. n8n offers official nodes: Ollama Chat Model, Ollama Model, and Embeddings Ollama. In 2026, Ollama 0.30 added Vulkan support by default, enabling GPU acceleration on a wider range of hardware.

Web search and extraction: SearXNG and Crawl4AI

  • SearXNG: A private metasearch engine.
    Critical note: In settings.yml, the output format must be explicitly enabled (search.formats: [html, json]). Otherwise, an n8n request with format=json receives HTTP 403 Forbidden. SearXNG is also a metasearch layer and does not exempt you from the usage policies of the underlying search engines.
  • Crawl4AI: Starting with version 0.9.0, it uses a secure-by-default model. It requires CRAWL4AI_API_TOKEN and a loopback connection or a protected TLS proxy. It converts web pages into clean Markdown optimized for language models.

Architecture comparison: What should you choose?

Component / technologyUse it whenDo not introduce it if
SQLiteQuick local tests, isolated prototypesThe system runs continuously and receives concurrent webhooks
PostgreSQLAny production installation with multiple usersServer resources are strictly limited to less than 1 GB of RAM
Regular ModeSmall systems, low execution rate, controlled concurrencyWebhooks arrive in bursts and cannot wait
Queue Mode (Redis)High concurrency and distributing work across multiple workersYou have a simple personal blog monitor and little workload
Kubernetes (Helm)Multi-node orchestration, KEDA autoscaling, or an existing K8s environmentYou have just one small VPS and a few simple workflows
pgvectorSmall to medium RAG systems in the same databaseVector search is the primary function of the entire business
QdrantMillions of vectors and heavy concurrent search requirementsSearching a few hundred local PDFs is enough
HTTP Request nodeThe target site has an API or clean static HTMLThe site requires complex client-side JavaScript execution
Crawl4AIYou need to prepare a website as Markdown for an LLMA normal JSON response from the official API is enough
PlaywrightDynamic pages that require clicks and sessionsThe data is available through a REST or GraphQL request
Ollama (local LLM)Private data, low cost per token, or classificationYou need state-of-the-art mathematical or multi-step reasoning
Cloud LLM (API)Final synthesis of complex reports from a small sampleYou are processing confidential internal contracts or gigabytes of logs
ntfy / GotifyYou want your own notification server for phonesYour team already works primarily in Slack or Discord

API, RSS, or scraping: In what order?

Before writing automations that collect data from the web, follow this order of preference:

  1. Official API: The most stable source, with a defined data format.
  2. Webhook: Ideal when the source reports changes itself, without polling.
  3. RSS / Atom feed: A standardized, lightweight format for tracking updates.
  4. Open data or periodic export: Official datasets made available for download.
  5. Public HTTP request: Read publicly available HTML while respecting server resources.
  6. Browser automation (Playwright): Use only when JavaScript rendering is necessary.

The technical ability to download a page does not automatically grant permission to collect or redistribute it without limits. Do not use automations to bypass authentication, paywalls, CAPTCHA tests, or access controls. The robots.txt standard is an instruction for crawlers, not a complete legal agreement.


Ten operational n8n automations

All examples below follow the production principles described earlier.


Workflow zero: Central Error Handler (SYS - Error Handler)

A global safeguard that catches unhandled errors from all workflows.

text
[Error Trigger] → [Edit Fields] → [Postgres: Log Error] → [ntfy: Alert]

SQL table for recording incidents:

sql
CREATE TABLE automation_errors (
    id BIGSERIAL PRIMARY KEY,
    workflow_name TEXT NOT NULL,
    execution_id TEXT NOT NULL,
    error_message TEXT NOT NULL,
    node_name TEXT,
    payload JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_errors_created ON automation_errors(created_at DESC);

Notifications contain identifiers only, never sensitive payloads:

text
SYSTEM ALERT
Workflow: {{ $json.workflow.name }}
Node: {{ $json.execution.error.node.name }}
Error: {{ $json.execution.error.message }}
Execution ID: {{ $json.execution.id }}

Workflow 1: Universal website change monitor

Monitor a web page and act only when the target content changes.

text
[Schedule Trigger] → [Postgres: Get URLs] → [Loop] → [HTTP Request]
                                                           |
                                                           v
[Postgres: Update] ← [ntfy Alert] ← [Ollama Diff] ← [Changed?]

Table of monitored pages:

sql
CREATE TABLE monitored_pages (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    url TEXT UNIQUE NOT NULL,
    selector TEXT NOT NULL DEFAULT 'body',
    current_hash VARCHAR(64),
    current_content TEXT,
    etag TEXT,
    last_modified TEXT,
    last_checked_at TIMESTAMPTZ,
    last_changed_at TIMESTAMPTZ,
    enabled BOOLEAN NOT NULL DEFAULT true
);

Normalize the text already selected from the DOM by an HTML node:

javascript
/* Normalize text previously extracted by the HTML node */
const raw = String($json.extracted_text ?? '');
const cleanText = raw
  .replace(/\s+/g, ' ')
  .replace(/\u00a0/g, ' ')
  .trim();

return [{
  json: {
    ...$json,
    normalized_text: cleanText
  }
}];

Run the semantic comparison only if the SHA-256 hash of the new text differs from the previous one.


Workflow 2: Price tracker with 90-day statistics

Track prices while guarding against parsing errors and adding statistical context.

sql
CREATE TABLE products (
    id BIGSERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    url TEXT UNIQUE NOT NULL,
    currency VARCHAR(10) NOT NULL DEFAULT 'EUR',
    target_price NUMERIC(12,2),
    enabled BOOLEAN DEFAULT true
);

CREATE TABLE product_prices (
    id BIGSERIAL PRIMARY KEY,
    product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE,
    price NUMERIC(12,2) NOT NULL CHECK (price >= 0),
    captured_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_prices_history ON product_prices(product_id, captured_at DESC);

Locale-aware price parsing in a Code node:

javascript
/* Parse formats such as '1,299.99', '1.299,99', '1299.99', or '1299,99' */
function parsePriceLocale(str) {
  if (!str) return null;
  let clean = String(str).replace(/[^0-9.,]/g, '').trim();
  
  if (clean.includes('.') && clean.includes(',')) {
    if (clean.indexOf('.') < clean.indexOf(',')) {
      /* European format: 1.299,99 -> 1299.99 */
      clean = clean.replace(/\./g, '').replace(',', '.');
    } else {
      /* US format: 1,299.99 -> 1299.99 */
      clean = clean.replace(/,/g, '');
    }
  } else if (clean.includes(',')) {
    clean = clean.replace(',', '.');
  }
  
  const val = parseFloat(clean);
  return Number.isFinite(val) ? val : null;
}

const price = parsePriceLocale($json.raw_price_string);
if (!price || price <= 0 || price > 500000) {
  throw new Error(`Invalid price detected: "${$json.raw_price_string}"`);
}

return [{ json: { ...$json, parsed_price: price } }];

Parameterized query for statistics (minimum, maximum, average):

sql
SELECT
    MIN(price) AS lowest_90d,
    MAX(price) AS highest_90d,
    ROUND(AVG(price), 2) AS avg_90d
FROM product_prices
WHERE product_id = $1
  AND captured_at >= now() - interval '90 days';

Workflow 3: Morning intelligence digest

Collect information overnight, remove duplicates, and send a summary at 07:00.

Illustrative structured data format:

json
{
  "source": "Tech News RSS",
  "title": "Example Project 4.2.0 released as a stable version",
  "url": "https://example.com/releases/4.2.0",
  "published_at": "2026-09-20T04:00:00Z",
  "content": "A short raw description of changes in the new release...",
  "category": "devops"
}

Deduplicate items before calling the language model by calculating a SHA-256 hash of the normalized title and URL.


Workflow 4: Job radar with deterministic scoring

AI extracts data into a defined schema. Strict code calculates the score.

JSON Schema for structured extraction:

json
{
  "title": "string",
  "company": "string",
  "remote": true,
  "location_type": "worldwide",
  "salary_min": 60000,
  "salary_max": 90000,
  "currency": "EUR",
  "technologies": ["TypeScript", "PostgreSQL", "Docker"],
  "seniority": "senior",
  "language_requirements": ["English"],
  "location_restrictions": [],
  "summary": "Role description..."
}

Jobs table with score range validation:

sql
CREATE TABLE jobs (
    id BIGSERIAL PRIMARY KEY,
    source TEXT NOT NULL,
    external_id TEXT NOT NULL,
    url TEXT NOT NULL,
    title TEXT NOT NULL,
    company TEXT NOT NULL,
    location_type VARCHAR(20) NOT NULL DEFAULT 'unknown',
    remote BOOLEAN DEFAULT false,
    salary_min NUMERIC,
    salary_max NUMERIC,
    currency VARCHAR(10),
    technologies JSONB,
    seniority TEXT,
    score INTEGER CHECK (score IS NULL OR (score >= 0 AND score <= 100)),
    discovered_at TIMESTAMPTZ DEFAULT now(),
    UNIQUE(source, external_id)
);

Code node for calculating scores:

javascript
const j = $json;
let score = 0;

if (j.remote === true) score += 20;
if (j.location_type === 'worldwide') score += 15;
if (j.location_type === 'europe') score += 10;
if (j.salary_min !== null || j.salary_max !== null) score += 15;

const tech = (j.technologies || []).map(t => String(t).toLowerCase());
if (tech.includes('typescript')) score += 15;
if (tech.includes('postgresql') || tech.includes('postgres')) score += 15;
if (tech.includes('docker')) score += 10;

score = Math.min(score, 100);

return [{
  json: {
    ...j,
    score: score
  }
}];

Workflow 5: Ecosystem-aware dependency and CVE radar

Protect projects against security vulnerabilities in installed packages.

Dependencies table by project:

sql
CREATE TABLE project_dependencies (
    project_name TEXT NOT NULL,
    ecosystem TEXT NOT NULL, /* npm, pypi, cargo, go */
    package_name TEXT NOT NULL,
    installed_version TEXT NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY(project_name, ecosystem, package_name)
);

Important note about version comparisons:
Do not use naive regular expressions to compare versions. Validation logic must follow the rules of the specific ecosystem, such as SemVer for npm and PEP 440 for Python. For reliable checks, use official machine-readable vulnerability databases such as GitHub Security Advisories or OSV.dev. Use AI only to summarize vulnerability descriptions and upgrade steps.


Workflow 6: Email copilot with loop protection and approval

AI prepares a draft reply to an email, but sending is blocked until the author approves it.

Safeguards:

  • Loop protection: Ignore messages with Auto-Submitted, precedence: bulk, or a sender containing no-reply or mailer-daemon.
  • Do not reply to yourself: Check that the sender is not your own address.
  • Approval timeout: An approval request expires after 48 hours if no person responds.
  • Redact sensitive data: Remove confidential data such as IBANs and passwords before sending a prompt to a language model.

Workflow 7: Local RAG over documents (PostgreSQL + pgvector)

Extract, chunk, and search documents without sending them to external servers.

sql
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_chunks (
    id BIGSERIAL PRIMARY KEY,
    document_id TEXT NOT NULL,
    document_hash VARCHAR(64) NOT NULL,
    chunk_index INTEGER NOT NULL,
    content TEXT NOT NULL,
    metadata JSONB NOT NULL,
    embedding vector(1024), /* Dimension must match the embedding model */
    created_at TIMESTAMPTZ DEFAULT now(),
    UNIQUE(document_hash, chunk_index)
);

CREATE INDEX idx_chunks_hnsw ON document_chunks 
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

Metadata for every chunk should include filename, document_id, page, section, and chunk_index.
Exact nearest-neighbor search does not use ANN approximation when retrieving the nearest vectors, but that does not mean the embedding model or semantic result is 100% accurate. The HNSW index involves trade-offs in performance, memory use, index build time, and recall in larger document collections. Check your embedding model's dimensions before creating the column, for example 1024, 768, or 1536.


Workflow 8: Idempotent webhook with a lease mechanism

Prevents the same payment or order from being processed more than once after repeated network requests.

text
WEBHOOK EVENT STATES:
  [received] -> [processing (lease lock)] -> [completed]
       |                    |
       v                    v
[duplicate -> 200]   [lease expired / error] -> [retryable_error] -> [dead_letter]

Table with a lease mechanism and retry count:

sql
CREATE TABLE webhook_events (
    provider TEXT NOT NULL,
    event_id TEXT NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'received',
    payload JSONB,
    retry_count INT DEFAULT 0,
    locked_at TIMESTAMPTZ,
    last_attempt_at TIMESTAMPTZ,
    next_retry_at TIMESTAMPTZ,
    first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at TIMESTAMPTZ,
    last_error TEXT,
    PRIMARY KEY(provider, event_id)
);

Atomic reservation in the database:

sql
INSERT INTO webhook_events (provider, event_id, status, payload, locked_at, last_attempt_at)
VALUES ($1, $2, 'processing', $3::jsonb, now(), now())
ON CONFLICT (provider, event_id) DO UPDATE
  SET retry_count = webhook_events.retry_count + 1,
      last_attempt_at = now()
  WHERE webhook_events.status = 'retryable_error'
    OR (webhook_events.status = 'processing' AND webhook_events.locked_at < now() - interval '10 minutes')
RETURNING event_id, status;

If the status is already completed, immediately return HTTP 200 without executing the action again.
Verify the signature against the raw body payload exactly as required by the specific provider's documentation. If the signature schema includes a timestamp, as it does for some payment providers, apply that provider's recommended replay window or tolerance. Do not introduce a universal time limit for providers whose signatures do not contain a timestamp.


Workflow 9: Uptime and API monitor with debouncing

Avoid false alarms by checking the response body and requiring several consecutive failed checks.

Reliability rules:

  • Enable the option in the HTTP Request node to return the status code and headers.
  • Hysteresis (debounce): Mark a service as down only after 2 to 3 consecutive failed checks, one minute apart.
  • External monitoring: n8n cannot reliably be its own only monitor. Set up a lightweight external tool, such as Uptime Kuma on a separate server, to check the n8n /healthz/readiness endpoint.

Workflow 10: Autonomous research agent

A modular agent split into sub-workflows:

  • RESEARCH - Search: Sends a query to SearXNG and filters the results.
  • RESEARCH - Fetch: Retrieves content in stages: HTTP, then Crawl4AI, then Playwright.
  • RESEARCH - Verify: Cross-checks claims before the final synthesis.
  • Always require human approval for actions that change production data.

Engineering patterns for long-term stability

Global correlation ID

Use a proper UUID instead of random strings:

javascript
/* Generate a correlation ID */
const correlationId = crypto.randomUUID();

return items.map(item => ({
  json: {
    ...item.json,
    correlation_id: correlationId
  }
}));

A realistic understanding of rate limiting

Configuring batching in the HTTP Request node, for example 10 items per 1,000 ms interval, limits sending to about one batch in the specified interval. Actual throughput also depends on network response time and the behavior of the remote server. The Retry-After header takes absolute precedence.

Limits of Structured Output Parsers

A Structured Output Parser enforces and checks the expected schema, but parsing can still fail, and syntactically valid JSON can contain semantically incorrect values. Always define a fallback branch for manual review of invalid JSON:
schema/parser failure -> limited retry -> manual/fallback queue.

Interpreting AI confidence values

If a model returns confidence: 0.94, that is not a mathematical probability of 94%. It is an internal heuristic estimate. Measure system reliability against a real, labeled test dataset using precision and recall metrics.

Git source control and environments

In the Community edition, use a manual flow: develop on a DEV instance, export to JSON, store the files in a private Git repository, and import them into the PROD instance. n8n Enterprise and Business plans include integrated Git Environments features. Before every commit, make sure sensitive tokens have not been left in the JSON definitions.


Common production problems (troubleshooting)

  1. Caddy cannot issue a TLS certificate:
    • Cause: DNS has not propagated, or the provider's firewall blocks ports 80 and 443.
    • Solution: Check the domain's A record with dig +short yourdomain.com and make sure the server's security groups do not block the ports.
  2. Webhook URL returns an error or an incorrect address:
    • Cause: N8N_WEBHOOK_URL is missing or configured incorrectly.
    • Solution: Set the full HTTPS address with a trailing slash: N8N_WEBHOOK_URL=https://n8n.example.com/.
  3. Client IP address is incorrect in logs:
    • Cause: N8N_PROXY_HOPS does not match the proxy setup.
    • Solution: Set N8N_PROXY_HOPS=1 for one reverse proxy (Caddy), or 2 if Cloudflare proxy is in front of it.
  4. PostgreSQL data is unavailable after changing versions:
    • Cause: The image tag changed without a migration, or PGDATA=/var/lib/postgresql/data is missing.
    • Solution: Restore the old database version, create a pg_dump, start the new version, and import the data.
  5. Credentials report a decryption error:
    • Cause: N8N_ENCRYPTION_KEY changed or is incorrect.
    • Solution: Restore the original key used during the initial installation to the .env file.
  6. Redis authentication error:
    • Cause: The password in the n8n environment does not match Redis's --requirepass parameter.
    • Solution: Make sure QUEUE_BULL_REDIS_PASSWORD and REDIS_PASSWORD have the same value.
  7. A worker does not pick up jobs from the queue:
    • Cause: The worker cannot access the Redis container over the network or uses a different database from the main instance.
    • Solution: Check connectivity on backend_network and the worker's health check status.
  8. extension "vector" is not available error:
    • Cause: The standard PostgreSQL image is in use instead of an image with pgvector.
    • Solution: Change the image in compose.yml to pgvector/pgvector:0.8.6-pg18.
  9. SearXNG returns HTTP 403 for /search?format=json:
    • Cause: JSON output is not enabled in the configuration.
    • Solution: Add formats: [html, json] under the search: section in settings.yml.
  10. Crawl4AI rejects requests:
    • Cause: Version 0.9.0 and later requires authentication and rejects connections without a token.
    • Solution: Define CRAWL4AI_API_TOKEN in the service environment and pass it as a Bearer token in the n8n request header.
  11. Kubernetes pod readiness failure and worker queue interruption:
    • Cause: The main pod fails its readiness probe because the database is unavailable, or worker pods cannot reach external Redis because of a restrictive NetworkPolicy.
    • Solution: Check pod logs with kubectl logs -n n8n <pod-name>, verify that NetworkPolicy allows traffic on ports 5432 and 6379, and inspect the endpoint status with kubectl describe pod -n n8n <pod-name>.

Pre-launch checklist

Data and validation

  • Validate input types and syntax before processing.
  • Implement protection against duplicate network requests (idempotency).
  • Store business data permanently in a dedicated database, outside n8n execution history.

Network and API requests

  • Set reasonable timeouts on all HTTP Request nodes.
  • Configure batching to prevent exceeding external API limits.
  • Respect the Retry-After header for HTTP 429 responses.

Security and credentials

  • Enable SSRF protection (N8N_SSRF_PROTECTION_ENABLED=true).
  • Define allowed internal hosts through N8N_SSRF_ALLOWED_HOSTNAMES and N8N_SSRF_ALLOWED_IP_RANGES.
  • Store N8N_ENCRYPTION_KEY securely outside the server.
  • Give user accounts and API keys only the minimum required privileges.
  • Restrict .env file permissions with chmod 600.

AI and models

  • Validate model output against the expected JSON schema.
  • Define a fallback branch for an invalid model response.
  • Require human approval for actions with significant business impact, such as deletion, billing, or sending messages.

Reliability and recovery

  • Set up a central SYS - Error Handler workflow.
  • Back up the database regularly according to the 3-2-1 backup rule.
  • Test database recovery from a backup in a clean test environment.

Operations and monitoring

  • Configure Docker log rotation to prevent the disk from filling up.
  • Set up external monitoring that remains accessible if the server is unavailable.
  • Give every operation a correlation ID to make log diagnosis easier.

Sources and further reading (reviewed September 21, 2026)


Conclusion

Docker Compose and Kubernetes are not competing religions. Compose is excellent when one or a few machines can handle the work reliably. Kubernetes becomes useful when the challenge is replica count, workload placement, autoscaling, and cluster-level availability. n8n now has an official Helm path for both scenarios, but introduce extra complexity only when it provides a concrete benefit.

Connecting all the components in this guide gives you an integrated system: The visual interface makes workflows easier to inspect and orchestrate. SQL preserves relational integrity and history. JavaScript provides precise mathematical and text transformations. Local AI models help interpret unstructured data. A person retains control wherever decisions have real business consequences.

The result is a reliable, secure, scalable system under your control, 24 hours a day, 365 days a year.