How to Migrate n8n from SQLite to PostgreSQL Without Losing Workflows
Introduction
Every self-hosted n8n instance starts life on SQLite, and for a long time that is fine. Then one day the editor slows down, concurrent executions start stepping on each other, or you want queue mode, and SQLite becomes the ceiling you keep hitting. PostgreSQL is the only other database n8n supports, and the official docs state it plainly: “By default, n8n uses SQLite. n8n also supports PostgreSQL. n8n deprecated support for MySQL and MariaDB from n8n 1.0.” This guide moves a working SQLite-backed instance to PostgreSQL with n8n’s own export and import tooling, preserving workflows, credentials, users, and optionally execution history. It is written for self-hosters on Docker Compose or npm who can afford a short maintenance window and want the migration done once, correctly.
Quick answer
Export everything with n8n export:entities --outputDir=./n8n-export, provision a fresh PostgreSQL, set DB_TYPE=postgresdb plus the DB_POSTGRESDB_* connection variables, let n8n create the schema on first boot, then run n8n import:entities --inputDir=./n8n-export --truncateTables=true.
Evidence
PostgreSQL is not a fashionable choice, it is the default choice. In the Stack Overflow 2025 Developer Survey, PostgreSQL was the most used database at 55.6% of respondents, and it topped both the most-admired list (66% of users want to keep using it) and the most-desired list (47% of non-users want to adopt it), marking the third consecutive year it led all three categories. The ecosystem around n8n is growing at the same pace: the n8n repository on GitHub counted 208,133 stars and 61,194 forks as of October 21, 2026, which means the Postgres path you are moving to is the one the maintainers and the community actually exercise.
The official n8n documentation describes the migration tooling in one sentence that should settle any debate about the supported approach: “You can export your database entities from n8n using the CLI. This tooling allows you to export all entity types from one database type, such as SQLite, and import them into another database type, such as Postgres.” The full command reference lives on the n8n Server CLI documentation page, and every variable you will set is listed on the database environment variables page. Finally, the official Docker Compose install guide gives the strategic verdict on when to move: “SQLite is fine for trying things out, but for a production instance that must handle more than a handful of users or workflows running around the clock use Postgres instead.” If that sentence describes your instance, this tutorial is for you.
Prerequisites
- A working self-hosted n8n instance (Docker Compose or npm) currently on SQLite, at a recent 2.x release. The
export:entitiesandimport:entitiescommands are part of the current Server CLI. - Shell access to the machine or container running n8n, with permission to stop and restart it.
- A maintenance window. There is no supported zero-downtime path, so pick a quiet hour and tell your users.
- Either Docker Compose (recommended) or a reachable PostgreSQL server. The official guide’s compose example uses the
postgres:18image, which is a solid default as of October 2026. - Your backup discipline from our npm-to-Docker migration guide applies here too: nothing gets deleted until the new database proves itself.
Step 1: Stop n8n and back up the SQLite world
The export must capture a quiet database, so stop n8n first. On Docker Compose run docker compose stop n8n from the directory holding your compose file. On npm, stop the process however you normally run it, whether that is a systemd service (sudo systemctl stop n8n), a process manager, or Ctrl+C in a terminal.
Now copy the entire data directory. For npm installs that is ~/.n8n, which holds the SQLite file, the config file with your encryption key, and everything else n8n persists:
cp -a ~/.n8n ~/n8n-sqlite-backup-2026-10-21
For Docker, your data lives in the volume you mounted (often a named volume like n8n-data, or a bind mount). Back it up with a bind-mount copy or your usual volume backup routine, and also copy the compose file and .env somewhere safe. Verify the backup exists and has a plausible size before you continue. This folder is your rollback plan for the next few weeks: keep it until the Postgres instance has survived real production load.
Step 2: Export every entity from SQLite
n8n’s Server CLI reads the stopped instance’s database directly. From the machine running n8n:
n8n export:entities --outputDir=./n8n-export
By default this excludes execution history data tables, because the docs note they can be very large. If your audit trail matters, include them explicitly:
n8n export:entities --outputDir=./n8n-export --includeExecutionHistoryDataTables=true
For Docker, run the CLI inside the container (the docs’ own pattern is docker exec -u node -it <n8n-container-name> <n8n-cli-command>) and then copy the export out:
docker exec -u node n8n n8n export:entities --outputDir=/tmp/n8n-export
docker cp n8n:/tmp/n8n-export ./n8n-export
Check the result before moving on. List the export directory, confirm it contains files with plausible sizes, and open one JSON file to spot-check that a workflow name you recognize appears inside. Count roughly how many workflows you have, because you will compare that number after the import. If the export directory is empty or tiny, fix the export now, while the SQLite original is untouched.
Step 3: Provision a fresh, empty PostgreSQL
Create a PostgreSQL that nothing else uses yet. In Docker Compose, add a service modeled on the official install guide:
services:
postgres:
image: postgres:18
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGDATA: /var/lib/postgresql/data
volumes:
- db-storage:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
volumes:
db-storage:
Put the credentials in your .env file alongside the rest of your secrets:
POSTGRES_USER=n8n
POSTGRES_PASSWORD=choose-a-long-random-password
POSTGRES_DB=n8n
Start only the database first and wait for the healthcheck to pass (docker compose up -d postgres, then docker compose ps). If you use a managed provider such as Neon, Supabase, or Amazon RDS instead, create an empty database and keep its host, port, credentials, and SSL requirements handy. The database name default in n8n’s documentation is n8n, and matching it keeps the configuration boring in the best way.
Step 4: Point n8n at PostgreSQL
Add the database variables to the n8n service in your compose file (or to the environment of your npm/systemd setup). The variable names come straight from the official database configuration reference:
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}
DB_POSTGRESDB_POOL_SIZE: 4
Three details matter here. First, DB_POSTGRESDB_HOST must be the Compose service name (postgres), because inside the n8n container localhost means the container itself, not your database. Second, the documented default pool size is 2, and the docs warn that “increasing it may help with resource utilization, but too many connections may degrade performance”, so 4 is a sane middle ground for a moderately busy instance, not a magic number. Third, for npm installs these are ordinary environment variables in your shell profile or systemd unit file, and DB_POSTGRESDB_HOST can be localhost there because n8n and Postgres share the machine.
Step 5: Boot n8n once so it builds the schema
Start n8n against the empty database and let it do its first-boot work:
docker compose up -d n8n
n8n creates all of its tables automatically on this first boot against the empty database. Wait until the editor responds in your browser, then stop n8n again (docker compose stop n8n). The import step needs a structurally complete but empty database, and the docs are explicit about this: “The database is expected to be empty prior to import, this can be forced with the –truncateTables parameter.” If the editor never loads, read the container logs before touching anything else; a connection error at this stage is almost always the host name from Step 4.
Step 6: Import the entities into PostgreSQL
With n8n stopped and the schema in place, run the import:
n8n import:entities --inputDir ./n8n-export --truncateTables true
For Docker, copy the export into the container first, then run the CLI inside it:
docker cp ./n8n-export n8n:/tmp/n8n-export
docker exec -u node n8n n8n import:entities --inputDir /tmp/n8n-export --truncateTables true
The official docs confirm the scope of this command: “Current supported database types include: SQLite, Postgres.” The --truncateTables flag empties the freshly created tables so the import lands cleanly, which is safe here precisely because the database holds nothing but the schema n8n just built. Watch the command output for per-entity success lines and, critically, for any error lines. An import that reports errors halfway is not a partial success, it is a redo: wipe the database, fix the cause, and import again.
Step 7: Start n8n and verify everything survived
Start n8n and log in with your existing owner account. If the login fails, the Server CLI offers a documented escape hatch: n8n user-management:reset returns user management to its pre-setup state so you can create a fresh owner account and continue the verification.
Work through this checklist before you declare victory:
- Workflow count matches. Compare the number of workflows in the UI against the count you noted in Step 2. Every workflow should be present.
- Workflows open cleanly. Open your three most complex workflows and confirm every node renders with its parameters intact.
- Credentials decrypt. Open a credential and use the node’s test or credential check. Decryption failures here mean the encryption key changed between export and import (see Troubleshooting).
- Webhooks and triggers are stable. Because exports preserve entity IDs, webhook paths and sub-workflow references did not move. Re-publish the workflows you want running, since imported workflows do not necessarily resume their previous published state on their own.
- Restart once more. On single-instance setups, previously active cron triggers can keep running after an import until the instance restarts, a known quirk the docs call out explicitly. One clean restart settles the scheduler.
Keep the SQLite backup from Step 1 for at least a few weeks. Only when the Postgres instance has handled real production traffic, including your busiest scheduled hour, should you consider the migration finished.
Troubleshooting
import:entities fails because the target database is not empty
The docs expect an empty database and say so directly: the import can be forced with the --truncateTables parameter. If you skipped that flag, rerun the command with --truncateTables true. If the database already holds real data you care about, stop and import into a different, empty database instead; truncating is destructive by design.
Credentials fail to decrypt after the import
n8n encrypts credentials with the key stored in your instance config, and a mismatched key is the classic post-migration failure. The fix is to keep the same N8N_ENCRYPTION_KEY on the new instance, which happens automatically if you carried your config over from the SQLite setup. If the key genuinely changed, the docs describe the fallback: export credentials with --decrypted from the old instance, because that flag exists to “migrate from one installation to another that has a different secret key in the configuration file”. Treat decrypted exports as highly sensitive, delete them after the import, and re-import the encrypted set from a fresh export afterwards.
n8n cannot reach PostgreSQL from the container
Typical errors are ECONNREFUSED or getaddrinfo ENOTFOUND postgres. From inside the n8n container, localhost is the container itself, so DB_POSTGRESDB_HOST must be the Compose service name (usually postgres) or the managed provider’s hostname. Confirm the database container is healthy (docker compose ps should show the healthcheck as passing) before starting n8n, and add a depends_on with condition: service_healthy to your n8n service so startup order is enforced on every reboot.
Nothing runs after the import
Imports of workflows can arrive unpublished, so open the workflows list and re-publish the ones that should be running. Then restart n8n once. The docs document a real quirk here: on non multi-main instances, previously active cron triggers keep running after an import until the instance restarts, so the restart is not superstition, it is the documented cleanup step.
SSL errors against managed PostgreSQL providers
Managed providers usually require encrypted connections. Set DB_POSTGRESDB_SSL_ENABLED=true, and if certificate verification fails, prefer supplying the provider’s CA certificate through DB_POSTGRESDB_SSL_CA. Setting DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED=false silences the error but disables verification, so treat it as a debugging step, not a permanent configuration.
FAQ
See the front matter for the full FAQ entries rendered on the page. The short version: execution history is opt-in via --includeExecutionHistoryDataTables=true, you can migrate back to SQLite with the same commands in reverse, entity IDs (and therefore webhook URLs) are preserved, there is no supported zero-downtime path so use a maintenance window, this migration is the prerequisite for queue mode, and n8n user-management:reset recovers a lost owner login.
Conclusion
Migrating n8n from SQLite to PostgreSQL is a one-way door only if you let it be one. With a full backup, the documented export:entities and import:entities pair, and a verification checklist, the move takes under an hour and your workflows, credentials, and IDs come through intact. Once Postgres is running, the next upgrade worth doing is horizontal: our n8n queue mode scaling guide shows how to spread executions across Redis-backed workers, and if you also want n8n to talk to Postgres as a data source rather than just store its state there, the n8n Postgres tutorial covers the node from first connection to parameterized queries. If you originally installed n8n via npm and this migration has you rethinking the whole stack, the Docker Compose setup guide and our Ubuntu VPS install guide complete the picture.
Frequently asked questions
Will my execution history survive the migration to PostgreSQL?
Only if you ask for it. The n8n export:entities command excludes execution history data tables by default because they can be very large. Add --includeExecutionHistoryDataTables=true to the export command to carry execution history into Postgres with everything else.
Can I migrate back from PostgreSQL to SQLite later?
Yes. The import:entities command supports both SQLite and Postgres as source and target, so you run the same procedure in reverse: export from Postgres, point n8n at SQLite, and import. Keep your SQLite backup folder until you are certain the Postgres instance is healthy.
Do workflows keep their IDs and webhook URLs after the import?
Yes. The export files preserve each workflow's and credential's ID, so webhook paths, sub-workflow references, and shared credentials stay stable. The official docs warn that imports overwrite entities with matching IDs, which is exactly what you want when the target database is empty.
Can I migrate with zero downtime?
There is no supported zero-downtime path. The SQLite file is locked by the running n8n process, and the import must land in an empty database, so plan a maintenance window: stop n8n, export, switch the database, import, and restart. Most instances finish in under an hour.
Does this migration unlock n8n queue mode?
Yes. Queue mode, which spreads executions across Redis-backed worker processes, requires PostgreSQL as its database. If horizontal scaling is on your roadmap, migrating to Postgres is the prerequisite step, and our n8n queue mode scaling guide picks up right where this tutorial ends.
What if I cannot log in after the import?
Run n8n user-management:reset from the Server CLI. The official docs describe it as returning user management to its pre-setup state, which lets you create a fresh owner account and then verify that your imported workflows and credentials are intact.