Skip to content
SelfHostingN8N

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:

  1. The database. Workflows, encrypted credentials, execution history, tags, users, settings. With the Compose stack, that’s the postgres_data volume.
  2. The encryption key. N8N_ENCRYPTION_KEY in your .env file (or, if you never set one, auto-generated inside the n8n_data volume 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_dump while 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_data taken while Postgres is writing can be corrupt. pg_dump output 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=168 for 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_KEY stored in a password manager, independent of the server
  • Nightly pg_dump + .env snapshot 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.