Move n8n from SQLite to Postgres
28 Nov 2025n8n defaults to SQLite. That’s fine for trying it out and dead wrong for anything you actually depend on. SQLite locks the entire database file on writes, so once you have more than a couple of workflows running concurrently — or one workflow with heavy execution logging — you start seeing SQLITE_BUSY errors and workflows that silently stop saving execution data. Postgres fixes this. Here’s the migration without losing your existing workflows.
Why SQLite breaks first
n8n writes to its database constantly: every workflow execution, every node’s output (if you have execution data saving turned on), every credential change. SQLite handles one writer at a time. Two workflows finishing around the same moment is enough to trigger lock contention. It gets worse under queue mode, where multiple workers all try to write execution results back to the same SQLite file — that setup basically requires Postgres to function correctly at all.
1. Stand up Postgres
If you’re running n8n via Docker Compose, add a Postgres service:
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: your_secure_password
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
2. Back up the existing SQLite database first
Before touching anything, copy the current database file. If the migration goes sideways, this is your rollback:
docker compose exec n8n cp /home/node/.n8n/database.sqlite /home/node/.n8n/database.sqlite.bak
docker cp $(docker compose ps -q n8n):/home/node/.n8n/database.sqlite.bak ./database.sqlite.bak
3. Export your workflows and credentials
n8n’s own export is more reliable than trying to move the database file directly, because it doesn’t depend on both databases having identical internal schemas:
docker compose exec n8n n8n export:workflow --all --output=/home/node/.n8n/backup/workflows.json
docker compose exec n8n n8n export:credentials --all --output=/home/node/.n8n/backup/credentials.json
Copy both files out of the container to somewhere safe.
4. Point n8n at Postgres
Set the database environment variables on the n8n service:
services:
n8n:
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: your_secure_password
depends_on:
- postgres
Restart n8n. On startup, it runs its own migrations against the new, empty Postgres database — no manual schema work needed.
docker compose up -d
docker compose logs -f n8n
Watch the logs for migration errors before moving on. A clean start shows a series of Migration X completed lines.
5. Re-import workflows and credentials
docker compose exec n8n n8n import:workflow --input=/home/node/.n8n/backup/workflows.json
docker compose exec n8n n8n import:credentials --input=/home/node/.n8n/backup/credentials.json
Credentials are encrypted with n8n’s encryption key — if you changed or lost N8N_ENCRYPTION_KEY between the old and new setup, decryption fails on import and every credential comes across broken. Keep the same key.
6. Verify before you delete anything
Open the UI, confirm workflows are present, and manually trigger one to make sure execution logging actually writes to Postgres:
docker compose exec postgres psql -U n8n -d n8n -c "SELECT COUNT(*) FROM execution_entity;"
A non-zero count after a test run confirms the new database is live and working. Only then remove the old SQLite file and its backup copies from anywhere you don’t need them.
What you get once Postgres is in place
Concurrent workflow executions stop fighting over a file lock. It also unlocks queue mode — running n8n with separate worker processes pulling jobs off a shared queue — which SQLite fundamentally cannot support since workers on different processes can’t safely share a single SQLite file the way they can share a network database connection. If you’re planning to scale past a single n8n instance, this migration is the prerequisite step, not an optional one.