FlowKit

n8n and PostgreSQL: configuring the database and migrating from SQLite without losing anything

Published 5 August 2026 · 6 min read

By default, a self-hosted n8n instance stores everything — workflows, encrypted credentials, execution history — in a single SQLite file sitting in /home/node/.n8n. That's a perfectly reasonable choice to get started: zero configuration, zero extra services. But as soon as the instance gets serious — hundreds of executions per day, hungry AI workflows, a move to queue mode — that single file becomes the limiting factor. This guide covers both halves of the switch to PostgreSQL: a clean configuration (environment variables, docker-compose) and the migration of your existing data from SQLite, without losing workflows or credentials.

SQLite by default: great to start with, until the day it isn't

SQLite is not a "cut-rate" database: it is the most widely deployed database engine in the world, present in virtually every smartphone and browser, as Gaffney, Prammer, Hipp, Patel and their co-authors point out in their 2022 VLDB paper, "SQLite: Past, Present, and Future" (see the study on Google Scholar). Its strength is precisely its in-process design: the database lives inside the same process as the application, with no separate server.

That's also its limit for n8n. Concretely, the switch to PostgreSQL becomes necessary when:

  • Execution volume explodes. Every execution writes its data to the database; a multi-GB SQLite file slows the UI down and complicates cleaning up executions.
  • Concurrent writes pile up. SQLite serializes writes: one writer at a time. Frequent webhooks plus simultaneous schedules end up in contention. Client-server DBMSs like PostgreSQL are architected precisely for concurrency — lock management, MVCC, connection pooling — as detailed in the reference paper by Hellerstein, Stonebraker and Hamilton, "Architecture of a Database System" (2007) (see it on Google Scholar).
  • You move to queue mode. Queue mode with Redis and workers requires a database shared between the main process and the workers: SQLite isn't supported there, PostgreSQL is required.
  • You want reliable hot backups. pg_dump produces a consistent snapshot while n8n keeps running; copying a SQLite file mid-write risks corruption.

The environment variables that switch n8n to PostgreSQL

All the configuration goes through environment variables (the general mechanism is covered in our n8n environment variables guide):

DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres        # Docker service name or hostname
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n
DB_POSTGRESDB_PASSWORD=a_strong_password
DB_POSTGRESDB_SCHEMA=public        # optional, defaults to "public"

Classic trap: if DB_TYPE is missing or misspelled (postgres instead of postgresdb), n8n doesn't throw an error — it silently falls back to SQLite, and you discover weeks later that the PostgreSQL database stayed empty. Always check the startup logs.

A complete docker-compose: n8n + PostgreSQL

The standard setup, with a persistent volume, a healthcheck and ordered startup (if you're starting from scratch, begin with our n8n Docker installation guide):

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=a_strong_password
      - POSTGRES_DB=n8n
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=a_strong_password
      - DB_POSTGRESDB_SCHEMA=public
      - N8N_ENCRYPTION_KEY=your_existing_key
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - n8n_data:/home/node/.n8n

volumes:
  postgres_data:
  n8n_data:

The condition: service_healthy prevents the crash on first startup: n8n only tries to connect once PostgreSQL is actually ready, not merely launched. The n8n_data volume remains useful even on PostgreSQL: n8n keeps local configuration files there.

N8N_ENCRYPTION_KEY: the variable that decides whether your credentials survive

This is the point that turns a smooth migration into a weekend of manual re-entry. Credentials are encrypted with the N8N_ENCRYPTION_KEY, generated at first startup and stored in the ~/.n8n/config file if you never set it explicitly. Changing databases changes nothing about the encryption: the new PostgreSQL instance must start with exactly the same key as the old one, otherwise every imported credential will be undecryptable. Retrieve it before anything else and pin it explicitly in the compose file — our dedicated N8N_ENCRYPTION_KEY guide covers where to find it and how to keep it safe.

Migrating the data from SQLite

n8n doesn't convert the database in place: the migration goes through the CLI export/import commands. On the old instance (still on SQLite):

# Export all workflows (one JSON file per workflow)
docker exec -it n8n-old n8n export:workflow --backup --output=/home/node/.n8n/backup/workflows/

# Export all credentials
docker exec -it n8n-old n8n export:credentials --backup --output=/home/node/.n8n/backup/credentials/

The --backup flag is equivalent to --all --pretty --separate. Credentials are exported encrypted: that's perfectly fine as long as you reuse the same N8N_ENCRYPTION_KEY. If you can't recover it, export in plain text with n8n export:credentials --all --decrypted — and then treat those files as secrets (temporary storage, deletion after import).

Then, on the new instance wired to PostgreSQL:

docker exec -it n8n-new n8n import:workflow --separate --input=/home/node/.n8n/backup/workflows/
docker exec -it n8n-new n8n import:credentials --separate --input=/home/node/.n8n/backup/credentials/

Two things worth knowing, covered in detail in our guide to importing and exporting n8n workflows: IDs are preserved (an import overwrites any existing workflow with the same ID), and the execution history does not migrate — there is no export command for executions. In the vast majority of cases this isn't a problem: it's perishable debugging data that pruning would have purged anyway.

Post-migration checks

Before decommissioning the old instance:

  • Re-activate your workflows. Imported workflows arrive deactivated: re-enable them one by one (or via n8n update:workflow --all --active=true, then restart) while checking that webhooks re-register properly.
  • Test the sensitive credentials. Open your main credentials and run a test execution: if the encryption key is right, everything decrypts without intervention.
  • Confirm which database is actually in use. In the startup logs, or by checking that the workflow_entity and credentials_entity tables are filling up on the PostgreSQL side (\dt in psql).
  • Shut down the old instance before activating the new one if both point at the same webhooks or the same mailboxes: two active instances would process every event twice.

Day-to-day PostgreSQL maintenance

PostgreSQL doesn't remove the need for upkeep, it just makes it more predictable:

EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168          # purge executions older than 7 days
EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000

Automatic pruning caps the growth of the executions table; PostgreSQL's autovacuum then reclaims the space, where SQLite required a manual VACUUM or DB_SQLITE_VACUUM_ON_STARTUP. Still keep an eye on the size (SELECT pg_size_pretty(pg_database_size('n8n'));) and, now that you're on PostgreSQL, set up proper hot backups — that's the subject of our guide to PostgreSQL backup and restore for n8n.

Managed database or container?

The postgres container in the compose file above suits most instances: simple, free, co-located (near-zero latency). A managed database (RDS, Cloud SQL, Scaleway, OVH) makes sense when you want to delegate backups, high availability and version upgrades — at the cost of a monthly bill and network latency to keep an eye on. Practical rule: start with the container; move to managed the day the database becomes an asset you no longer have time to operate yourself.

Summary

SQLite is an excellent starting point; PostgreSQL is the required step as soon as your n8n instance scales up: write concurrency, queue mode, hot backups. The switch comes down to three moves — the DB_TYPE=postgresdb and DB_POSTGRESDB_* variables, scrupulously keeping the N8N_ENCRYPTION_KEY, and the CLI export/import of workflows and credentials. It's exactly the foundation that production AI workflows demand: the RAG Assistant Pack (€119), with its document ingestion and long-running executions, runs all the more smoothly on a properly sized PostgreSQL instance that's ready to move to queue mode when the day comes.

FAQ

Frequently asked questions

Can I migrate the execution history from SQLite to PostgreSQL?

No, not with the official tooling: n8n's CLI commands export workflows and credentials, not past executions. In practice this is almost never a blocker: execution history is short-lived debugging data that pruning would have purged within weeks anyway. If you legally need a history, export it separately (a SQL query against the SQLite file) before decommissioning the old instance.

Should I change the N8N_ENCRYPTION_KEY during the migration?

No, absolutely not: keep exactly the same key on the new instance. It is what decrypts your credentials, regardless of the database engine underneath. If the new instance starts with a different key, every imported credential becomes unreadable and you will have to either re-import a --decrypted export or re-enter everything by hand.

Is SQLite really unusable in production?

No — for a single-container instance running a few dozen executions per day, SQLite holds up very well and keeps hosting simple. The switch to PostgreSQL becomes necessary when execution volume grows, when several processes need to write in parallel, or when you enable queue mode with workers: at that point, SQLite is no longer supported.

Bundle FlowKit Complet

€269