Self-hosting
How to Back Up and Restore n8n (Without Losing Credentials)
The complete n8n backup strategy: what to back up, a nightly script with rotation, off-server copies, and a restore you've actually tested.
Most self-hosted n8n disasters aren’t hacks. They’re a server that died, a docker volume rm typed in the wrong terminal, or an upgrade that went sideways — followed by the discovery that the “backup” either doesn’t exist or can’t decrypt any of the stored credentials.
That last part is the n8n-specific trap: a database dump alone is not a working backup. n8n encrypts every saved credential with an encryption key that lives outside the database. Back up one without the other and you’ll restore a workflow list full of connections you have to rebuild by hand.
This guide gives you a backup routine that survives a dead server: what to capture, a nightly script with rotation, getting copies off the machine, and — the part almost everyone skips — a restore drill you run once so you know it works.
The two things that must survive
Everything n8n knows lives in exactly two places:
- The database. Workflows, encrypted credentials, execution history, tags, users, settings. With the Compose stack, that’s the
postgres_datavolume. - The encryption key.
N8N_ENCRYPTION_KEYin your.envfile (or, if you never set one, auto-generated inside then8n_datavolume at/home/node/.n8n/config).
The relationship is absolute: credentials in the database are AES-encrypted with that key. Same key + database dump = full recovery. New key + database dump = every credential is ciphertext you can never read again.
Nice-to-haves beyond those two: your docker-compose.yml and Caddyfile (keep them in a private git repo — they contain no secrets if you’ve kept secrets in .env), and any custom nodes or local files your workflows read.
Step 1 — The nightly backup script
Create ~/n8n/backup.sh. It dumps Postgres, snapshots your .env (which contains the encryption key), and prunes anything older than 14 days:
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
BACKUP_DIR=./backups
STAMP=$(date +%F)
KEEP_DAYS=14
mkdir -p "$BACKUP_DIR"
# 1. Database: full logical dump, compressed.
docker compose exec -T postgres \
pg_dump -U n8n -d n8n | gzip > "$BACKUP_DIR/n8n-db-$STAMP.sql.gz"
# 2. Secrets: the .env holds N8N_ENCRYPTION_KEY — without it the dump is useless.
cp .env "$BACKUP_DIR/n8n-env-$STAMP.bak"
chmod 600 "$BACKUP_DIR/n8n-env-$STAMP.bak"
# 3. Rotate: delete backups older than KEEP_DAYS.
find "$BACKUP_DIR" -name 'n8n-*' -mtime +"$KEEP_DAYS" -delete
echo "OK $(date -Is) db=$(du -h "$BACKUP_DIR/n8n-db-$STAMP.sql.gz" | cut -f1)"
Make it executable and schedule it for 03:00 every night:
chmod +x ~/n8n/backup.sh
( crontab -l 2>/dev/null; echo "0 3 * * * ~/n8n/backup.sh >> ~/n8n/backups/backup.log 2>&1" ) | crontab -
A few deliberate choices worth knowing about:
pg_dumpwhile n8n is running is safe. It takes a consistent snapshot; you don’t need to stop the stack or pause workflows.- A logical dump beats copying the volume. Raw copies of
postgres_datataken while Postgres is writing can be corrupt.pg_dumpoutput is also portable across Postgres versions — handy when you restore onto a newer server. - Execution history is the bulk of the size. If dumps balloon into gigabytes, prune old executions (set
EXECUTIONS_DATA_MAX_AGE=168for 7 days) — see our environment variables guide for details.
Step 2 — Get copies off the server
Backups on the same box protect you from mistakes, not from the server itself dying. The cleanest fix is rclone syncing to any object storage — Backblaze B2, Cloudflare R2, S3, even Google Drive:
sudo apt install -y rclone
rclone config # one-time interactive setup, e.g. a remote named "b2"
Then add one line to the end of backup.sh:
rclone sync "$BACKUP_DIR" b2:my-n8n-backups --include 'n8n-*'
That’s the whole 3-2-1 shape that matters for a small server: nightly local copies for quick rollbacks, plus an off-site copy for the day the VPS vanishes.
Step 3 — The restore drill
A backup you’ve never restored is a hypothesis. Run this drill once now, and again after any major change. It takes ten minutes and turns your disaster plan from “probably fine” into “tested.”
You can do it on a scratch VPS or locally — anywhere with Docker.
1. Recreate the stack. Copy your docker-compose.yml, Caddyfile, and the backed-up .env (this is where the original encryption key comes back). Start only Postgres first:
docker compose up -d postgres
2. Load the dump into the fresh, empty database:
gunzip -c n8n-db-2026-06-18.sql.gz | \
docker compose exec -T postgres psql -U n8n -d n8n
3. Start n8n and watch it come up:
docker compose up -d
docker compose logs -f n8n
4. Verify the thing that matters. Log in with your usual owner account (users live in the DB, so it’s all there), open a workflow that uses a credential — Slack, Postgres, an API key, anything — and execute it. If the credential works, your key and dump match and the restore is real.
5. Repoint DNS (in a real disaster): move the A record for n8n.yourdomain.com to the new server’s IP and Caddy will issue a fresh certificate automatically. Webhook URLs don’t change, because they’re based on the domain, not the server.
If you’re on SQLite
Small setups without Postgres keep everything in a single file inside the n8n_data volume. The same two-things rule applies; only the dump command changes. SQLite files shouldn’t be copied while being written, so use its online-backup command:
docker compose exec -T n8n \
sqlite3 /home/node/.n8n/database.sqlite ".backup /home/node/.n8n/backup.sqlite"
docker cp $(docker compose ps -q n8n):/home/node/.n8n/backup.sqlite \
./backups/n8n-db-$(date +%F).sqlite
To restore: place the file back as database.sqlite in a fresh volume, set the same encryption key, start n8n. That said — if your workflows have become important enough that you’re reading a backup guide, that’s usually the sign to move to Postgres anyway.
Bonus: workflow exports as a second layer
Database dumps are all-or-nothing. n8n’s CLI can also export workflows and credentials as JSON, which is useful for restoring one accidentally deleted workflow without rolling back the whole database:
docker compose exec -T n8n n8n export:workflow --all --pretty --output=/tmp/wf.json
docker cp $(docker compose ps -q n8n):/tmp/wf.json ./backups/workflows-$(date +%F).json
Some teams go one step further and commit these JSON exports to a private git repo on a schedule, which gives you diffable workflow history for free.
The checklist
-
N8N_ENCRYPTION_KEYstored in a password manager, independent of the server - Nightly
pg_dump+.envsnapshot via cron, with rotation - Copies synced off-server (rclone → object storage)
- Something alerts you if the backup stops running
- One successful restore drill performed — credentials decrypted and a workflow ran
If maintaining this discipline is exactly the part of self-hosting you don’t want to own, that’s a legitimate reason to let a platform handle the infrastructure layer while you keep your own instance:
Where to go next
With backups solved, the other half of not-losing-your-instance is updating it without breaking it — covered in How to Update n8n Safely. And if you haven’t hardened the server itself yet, start with Securing Self-Hosted n8n.