Migrate n8n from SQLite to PostgreSQL on DigitalOcean with a tested cutover
Move a self-hosted n8n instance from SQLite to DigitalOcean Managed PostgreSQL with a planned cutover, restricted database access, and a recovery test.
Companion GitHub repositoryCompanion n8n PostgreSQL migration checksTylorMayfield/n8n-digitalocean-postgres-migrationView on GitHubDecide whether to migrate
SQLite works for a quiet n8n installation. Use PostgreSQL when several people administer workflows, execution history matters, or an outage has a real cost. It moves n8n data out of the Droplet. It does not make a risky workflow safe or replace a recovery plan.
An empty PostgreSQL database can let n8n start without your workflows, credentials, or history. Schedule a maintenance window, keep the n8n encryption key, and leave the SQLite data alone until the team accepts the cutover.
You need DigitalOcean administrator access, SSH access to the Linux Droplet running n8n, and a separate place to test a restore. A Droplet is the virtual server. A trusted source is a network rule that allows a server or administrator address to connect to the database.
- A tested recovery archive and its matching n8n encryption key.
- A maintenance window that pauses incoming webhooks and scheduled production work.
- A named owner who can verify credentials and approve the final cutover.
Capture a recovery point before changing the database
Start with a backup you can restore. Record the n8n version, Compose image tag, public URL, encryption-key location, volume name, and final backup timestamp. Read one encrypted archive in the isolated recovery environment from the private n8n guide. Keep the same n8n version for the migration. Do not combine a database move with an n8n upgrade.
If you used the DigitalOcean Marketplace n8n app from the companion recovery guide, the deployment directory is usually /opt/n8n-docker-caddy. If you use another setup, find the Compose file before editing anything. The service name from docker compose config --services replaces n8n in the later commands.
Before continuing: Inspect the returned paths before changing directories. The command only lists candidate Compose files.
# Run on the n8n Droplet. The Marketplace app normally uses /opt/n8n-docker-caddy.
sudo find /opt /srv /home -maxdepth 4 -type f \( -name compose.yml -o -name docker-compose.yml \) -print
cd /opt/n8n-docker-caddy # Replace only if find returned a different n8n deployment.
sudo docker compose config --services
sudo sed -n '1,160p' .envgit clone https://github.com/TylorMayfield/n8n-digitalocean-postgres-migration.git
cd n8n-digitalocean-postgres-migration
cp migration.env.example migration.env
# Set N8N_DATA_DIR, BACKUP_FILE, and POSTGRES_URL, then run:
./scripts/preflight.sh migration.envReplace before use: N8N_DATA_DIR, BACKUP_FILE, POSTGRES_URL
Create a restricted PostgreSQL database
Create the cluster first. Do not change n8n yet. This guide uses a Standard Edition PostgreSQL cluster because its connection details include a CA certificate that n8n can mount. If you choose Advanced Edition, stop here and adapt the TLS setup to its system trust store instead of copying the CA-file steps below.
- In your DigitalOcean project, open Databases and click Create Database. You can also use Create, then Managed Database. On the Create Database Cluster page, select PostgreSQL and a version. The engine and major version cannot be changed after creation.
- Choose the same datacenter region as the n8n Droplet. Select the plan and storage that fit the current workload, enter n8n-postgres as the cluster name, choose the project that contains n8n, then click Create Database Cluster. Wait for the cluster status to become Online.
- Open the new cluster, then select Users & Databases. In Databases, enter n8n in Add new database and click Save. In Users, enter n8n_app in Add new user and click Save. Use n8n_app for n8n. Leave doadmin for administration and recovery work.
- Select Network Access, click Add Trusted Sources, choose Quick select Droplets, and select the n8n Droplet. Click Add Trusted Sources. For a one-time command-line check, add your own current IPv4 address from the same dialog. Remove that address after the check. Do not add 0.0.0.0/0.
- Return to the cluster Overview and open Connection Details. Copy the host, port, database name, n8n_app user name, and password into a password manager. Download the CA certificate and copy it to the n8n Droplet later. Do not paste any of these values into Git, workflow notes, or screenshots.
Check the first response
- Expected result
- An administrator can open a known workflow, decrypt a known credential, complete one harmless manual test, and connect to the new PostgreSQL database as the dedicated application user.
- Stop if
- Stop if the backup cannot be read, the encryption key is unavailable, the database permits unrestricted access, credentials fail to decrypt, or any production workflow can create new state before validation completes.
- Next step
- Record the cutover and recovery-test result, remove temporary administrator database access, and retain the old SQLite recovery point for the approved period.
Perform a planned cutover
Stop n8n and block inbound traffic that creates new execution state. n8n’s Server CLI can export entities from SQLite and import them into an empty PostgreSQL database. It carries workflows and credentials, but excludes execution-history data tables unless you explicitly request them. Keep the encryption key unchanged. If the export does not finish cleanly, stop.
The .env file sits beside the Compose file. It holds values that Compose passes into the n8n container. Add the PostgreSQL values there. Then add the matching environment entries and a read-only CA-certificate mount to the n8n service in compose.yml or docker-compose.yml. A .env file by itself does not give a running container new variables unless the Compose file references them.
Before continuing: This changes the database n8n uses at startup. Preserve the existing .env and Compose files, keep the existing encryption key, and use the new database only after the entity export succeeds.
# Run in the Compose directory you found earlier. Back up both files before editing.
if [[ -f compose.yml ]]; then COMPOSE_FILE=compose.yml; elif [[ -f docker-compose.yml ]]; then COMPOSE_FILE=docker-compose.yml; else echo 'No Compose file'; exit 1; fi
sudo cp .env .env.sqlite-backup
sudo cp "$COMPOSE_FILE" "$COMPOSE_FILE.sqlite-backup"
sudo nano .env
# Add these lines to .env. Replace every placeholder with a value from DigitalOcean Connection Details.
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=YOUR_CLUSTER_HOST
DB_POSTGRESDB_PORT=YOUR_CLUSTER_PORT
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n_app
DB_POSTGRESDB_PASSWORD=YOUR_N8N_APP_PASSWORD
DB_POSTGRESDB_SCHEMA=public
DB_POSTGRESDB_SSL_ENABLED=true
DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED=true
DB_POSTGRESDB_SSL_CA_FILE=/run/secrets/do-postgres-ca.crt
# Then run sudo nano $COMPOSE_FILE. Inside the existing n8n service, add these entries without deleting its current environment or volumes.
services:
n8n:
environment:
DB_TYPE: ${DB_TYPE}
DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST}
DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT}
DB_POSTGRESDB_DATABASE: ${DB_POSTGRESDB_DATABASE}
DB_POSTGRESDB_USER: ${DB_POSTGRESDB_USER}
DB_POSTGRESDB_PASSWORD: ${DB_POSTGRESDB_PASSWORD}
DB_POSTGRESDB_SCHEMA: ${DB_POSTGRESDB_SCHEMA}
DB_POSTGRESDB_SSL_ENABLED: ${DB_POSTGRESDB_SSL_ENABLED}
DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED: ${DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED}
DB_POSTGRESDB_SSL_CA_FILE: ${DB_POSTGRESDB_SSL_CA_FILE}
volumes:
- ./secrets/do-postgres-ca.crt:/run/secrets/do-postgres-ca.crt:roReplace before use: YOUR_CLUSTER_HOST, YOUR_CLUSTER_PORT, YOUR_N8N_APP_PASSWORD
Before continuing: Run the import only once against a new, empty PostgreSQL database. It writes the exported n8n entities to that database.
# Run in the Compose directory. Replace n8n only if config --services showed a different service name.
if [[ -f compose.yml ]]; then COMPOSE_FILE=compose.yml; elif [[ -f docker-compose.yml ]]; then COMPOSE_FILE=docker-compose.yml; else echo 'No Compose file'; exit 1; fi
sudo install -d -m 700 migration-entities secrets
# Save the CA certificate copied from DigitalOcean Connection Details to secrets/do-postgres-ca.crt.
sudo chmod 600 secrets/do-postgres-ca.crt
sudo tee compose.migration.yml > /dev/null <<'YAML'
services:
n8n:
volumes:
- ./migration-entities:/migration
YAML
# With the old SQLite .env still in place, stop n8n and export data into the host directory.
sudo docker compose -f "$COMPOSE_FILE" stop n8n
sudo docker compose -f "$COMPOSE_FILE" -f compose.migration.yml run --rm --no-deps n8n \
n8n export:entities --outputDir=/migration
# Update .env and Compose with the PostgreSQL settings above, then import into the empty database.
sudo docker compose -f "$COMPOSE_FILE" -f compose.migration.yml run --rm --no-deps n8n \
n8n import:entities --inputDir=/migration
sudo docker compose -f "$COMPOSE_FILE" config > /tmp/n8n-postgres-resolved.yml
sudo docker compose -f "$COMPOSE_FILE" up -dProve the result before reopening production traffic
Sign in as an administrator. Open a known workflow, decrypt a known credential, and complete one harmless manual run. Keep production workflows disabled until these checks pass. If a credential fails to decrypt, restore the old service. Do not reset credentials during the investigation.
Then test database recovery in the control panel. Open Databases, select the n8n cluster, choose Actions, then Restore from backup. Choose a recent point, give the restored cluster a test name, and select Restore to New Cluster. When it is online, add only your administrator address under Network Access, copy its new connection details, and run a read-only psql check. This proves the database recovery point, not that every workflow is safe to run.
Before continuing: Use the restored cluster's connection details, not the production connection, for the recovery check.
# On the n8n Droplet, check the running service and its startup log.
sudo docker compose ps
sudo docker compose logs --tail=100 n8n
# On an administrator machine with the PostgreSQL client installed, use the restored cluster's new Connection Details.
psql "postgresql://n8n_app:YOUR_PASSWORD@RESTORED_CLUSTER_HOST:RESTORED_CLUSTER_PORT/n8n?sslmode=require" \
-c 'select current_database(), current_user, now();'
# Then open one workflow, decrypt one credential, and run one harmless manual test. Record the result.Replace before use: YOUR_PASSWORD, RESTORED_CLUSTER_HOST, RESTORED_CLUSTER_PORT
Know what Managed PostgreSQL does not cover
Managed PostgreSQL runs the database and provides backups and infrastructure recovery. You still own access rules, workflow logic, credentials, and the decision to run a workflow after recovery. Review that split after the first restore test, not during an outage.
If n8n handles important customer data or irreversible actions, document a recovery-time target and acceptable data loss. The database is now part of an operational system, not a cosmetic upgrade.