VPS Snaps

How to back up and restore n8n

A complete n8n backup is the .n8n folder, which holds the config file with the encryption key and, by default, the SQLite database database.sqlite, plus a pg_dump if you run n8n on PostgreSQL. Without the encryption key, a restored instance cannot decrypt any saved credential. n8n export:workflow and n8n export:credentials add readable JSON copies, but they leave out users, execution history and variables, so they don't replace the full backup.

12 min readUpdated Checked against official documentation

What a complete n8n backup includes

n8n's own guide splits a complete backup into two parts: the .n8n folder, and the PostgreSQL database if you use one instead of SQLite.

WhatBack up?Why
.n8n/configYes, privatelyA small JSON file with encryptionKey, the key every saved credential is encrypted with.
.n8n/database.sqliteYes, with VACUUM INTOThe default database: workflows, credentials, users, variables, settings and execution history.
PostgreSQL databaseYes, with pg_dumpTakes the place of the SQLite file when DB_TYPE=postgresdb.
.n8n/nodes/YesCommunity nodes, installed from the editor or with npm.
.n8n/binaryData/, .n8n/storage/Yes, if presentExecution files, when binary or execution data is kept on the filesystem.
N8N_CUSTOM_EXTENSIONS directoriesYesCustom nodes kept outside the folder. Workflows that use them break without them.
Compose file and .envYesThe image version, the database connection and any custom N8N_ENCRYPTION_KEY.
S3 or Azure binary data storageIn the bucketNot on the server.

On an npm install the folder is ~/.n8n of the user who runs n8n. N8N_USER_FOLDER changes the parent: with N8N_USER_FOLDER=/opt/n8n, the folder is /opt/n8n/.n8n. In Docker it is a volume mounted at /home/node/.n8n. n8n's docker run example names it n8n_data; Compose files use other names and add the project name in front, so look yours up. Replace n8n with your container's name from docker ps throughout:

Terminal
docker inspect --format '{{json .Mounts}}' n8n

Then get the volume's path on the host, which the commands below read from. With Docker's default data directory, a volume named n8n_data is at /var/lib/docker/volumes/n8n_data/_data:

Terminal
docker volume inspect --format '{{ .Mountpoint }}' n8n_data

Save the encryption key

n8n encrypts each credential before writing it to the database. On first start it generates a random key and saves it in .n8n/config, unless N8N_ENCRYPTION_KEY is set. Without that key, a restored database or credential export cannot be decrypted, and the key cannot be recovered from the data. Print it:

Terminal
docker exec n8n cat /home/node/.n8n/config

Store the encryptionKey value in your password manager, apart from the backups. Three rules from n8n's docs and source:

  • If both the file and N8N_ENCRYPTION_KEY exist, they must match. Otherwise n8n refuses to start with Mismatching encryption keys.
  • If the file is missing and the variable is not set, n8n generates a new key without asking, and every existing credential then fails to decrypt.
  • In queue mode, the main instance and every worker need the same N8N_ENCRYPTION_KEY.

Set N8N_ENCRYPTION_KEY in your Compose .env to the value already in config. The key then lives in your deployment files too, so losing the volume doesn't lose the key.

If you turn on n8n's encryption key rotation (N8N_ENV_FEAT_ENCRYPTION_KEY_ROTATION), take a full backup first. n8n documents it as one-way: the only way back is a database backup from before you enabled it.

Copy the SQLite database safely

Since n8n 2.0, the SQLite driver always uses write-ahead log mode: recent writes sit in database.sqlite-wal until a checkpoint moves them into database.sqlite. n8n's docs say to stop n8n before copying the folder, or to use a tool that takes a consistent snapshot of the SQLite file. SQLite's own shell is that tool. Install it on the host and run it as root against the file in the volume:

Terminal
apt install sqlite3
Terminal
install -d -m 700 /var/backups/n8n
Terminal
sqlite3 /var/lib/docker/volumes/n8n_data/_data/database.sqlite ".timeout 10000" "VACUUM INTO '/var/backups/n8n/database.sqlite'"

VACUUM INTO writes a compacted copy from one consistent read while n8n keeps running. It suits n8n better than .backup, which starts over whenever another connection writes during the copy, and n8n writes execution records all day. The target file must not exist. .timeout 10000 waits up to 10 seconds for a lock. Check the result; a healthy copy prints ok:

Terminal
sqlite3 /var/backups/n8n/database.sqlite "PRAGMA quick_check;"

Execution history is most of a busy database. By default n8n deletes executions older than 336 hours (14 days) and keeps at most 10,000; EXECUTIONS_DATA_MAX_AGE and EXECUTIONS_DATA_PRUNE_MAX_COUNT change that. SQLite backups covers the copy methods in detail.

Back up PostgreSQL instead

With DB_TYPE=postgresdb, everything except the key and local files is in PostgreSQL. In n8n's withPostgres Compose example the database service is called postgres and has POSTGRES_USER and POSTGRES_DB in its environment. From the Compose project folder:

Terminal
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > /var/backups/n8n/n8n-$(date +%F).dump
  • -T turns off the pseudo-terminal that docker compose exec allocates by default, which would mangle the binary dump.
  • sh -c in single quotes makes the variables expand inside the container, where they are set.
  • -Fc writes the compressed custom format that pg_restore reads.

Back up the .n8n volume as well: it still holds the key. The Docker Compose database guide and the pg_dump guide cover the details.

Export workflows and credentials with the CLI

n8n's CLI writes workflows and credentials as JSON: handy for keeping workflows in git, moving them to another instance, or getting one back without a full restore. --backup is shorthand for --all --pretty --separate, one readable file per item. In Docker, run it as the node user, write into the container, then copy the files out:

Terminal
docker exec -u node n8n n8n export:workflow --backup --output=/tmp/n8n-export/workflows/
Terminal
docker exec -u node n8n n8n export:credentials --backup --output=/tmp/n8n-export/credentials/
Terminal
docker cp n8n:/tmp/n8n-export ./n8n-export-2026-10-03
Terminal
docker exec -u node n8n rm -rf /tmp/n8n-export
  • On an npm install, run the same commands without docker exec. Use a separate folder for each type.
  • Credentials stay encrypted with the instance key and are useless without it.
  • --published exports each workflow's published version instead of the current draft.
  • The exports leave out users and roles, execution history, variables and instance settings, including the key. They are not a full backup.

--decrypted writes every credential in plain text: API keys, OAuth tokens, database passwords. Use it only to move credentials to an instance with a different key, and delete the file as soon as the import is done.

Terminal
docker exec -u node n8n n8n export:credentials --all --decrypted --output=/tmp/decrypted.json

Automate the full backup

This root script copies the SQLite database with VACUUM INTO, checks it, packs it with the rest of the .n8n volume, and encrypts the archive to a GPG public key, because the archive holds both the encrypted credentials and the key that opens them. Encrypting backups shows how to set up the key. The n8n version goes into the file name, for the restore.

/usr/local/bin/n8n-backup.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

CONTAINER="n8n"
VOLUME="n8n_data"
BACKUP_DIR="/var/backups/n8n"
KEY="<gpg-key-fingerprint>"
KEEP_DAYS=14

DATA="$(docker volume inspect --format '{{ .Mountpoint }}' "$VOLUME")"
VERSION="$(docker exec "$CONTAINER" n8n --version 2>/dev/null || echo unknown)"
OUT="$BACKUP_DIR/n8n-$(date +%Y-%m-%d_%H%M)-$VERSION.tar.gz.gpg"
STAGE="$(mktemp -d)"

trap 'rm -rf "$STAGE"; rm -f "$OUT.partial"' EXIT
mkdir -p "$BACKUP_DIR"

sqlite3 "$DATA/database.sqlite" ".timeout 10000" "VACUUM INTO '$STAGE/database.sqlite'"
result=$(sqlite3 "$STAGE/database.sqlite" "PRAGMA quick_check;")
if [ "$result" != "ok" ]; then
  echo "quick_check failed: $result" >&2
  exit 1
fi
chown --reference="$DATA/database.sqlite" "$STAGE/database.sqlite"

tar -czf - --numeric-owner -C "$STAGE" database.sqlite \
  -C "$DATA" --exclude='./database.sqlite*' . \
  | gpg --batch --encrypt --recipient "$KEY" --output "$OUT.partial"
mv "$OUT.partial" "$OUT"

find "$BACKUP_DIR" -name 'n8n-*.tar.gz.gpg' -type f -mtime +"$KEEP_DAYS" -delete
Terminal
chmod 755 /usr/local/bin/n8n-backup.sh
/etc/cron.d/n8n-backup
40 2 * * * root /usr/local/bin/n8n-backup.sh >> /var/log/n8n-backup.log 2>&1
  • chown --reference gives the database copy the owner of the original, the image's node user (UID 1000), so n8n can open it after a restore. --numeric-owner stores that number rather than a name.
  • The exclude pattern skips the live database.sqlite, -wal and -shm in the volume; the copy added from the staging folder has no ./ in its name and stays in.
  • For PostgreSQL, swap the sqlite3 and chown lines for the pg_dump command (run from the Compose folder) writing to $STAGE/n8n.dump, and pack that file instead.

Copy the encrypted files off the server each night, for example with rclone.

Restore the full instance

Restore into a new volume, so the old one stays untouched until the restore checks out. On a machine with the private key:

Terminal
docker volume create n8n_data_restored
Terminal
gpg --decrypt n8n-2026-10-03_0240-2.41.6.tar.gz.gpg | docker run --rm -i -v n8n_data_restored:/data alpine tar -xzf - --numeric-owner -C /data
Terminal
docker run --rm -v n8n_data_restored:/data alpine chown -R 1000:1000 /data

-i passes the decrypted stream into the helper container; the chown makes sure the node user owns every file. Then start n8n on the restored volume, with the version in the file name and your usual options:

Terminal
docker rm -f n8n
Terminal
docker run -d --name n8n -p 5678:5678 -v n8n_data_restored:/home/node/.n8n n8nio/n8n:2.41.6

With Compose, point the volume key your n8n service uses at the restored volume, pin N8N_VERSION in .env, and run docker compose up -d:

compose.yml
volumes:
  n8n_data:
    external: true
    name: n8n_data_restored

With PostgreSQL, load the dump before n8n starts, or n8n creates empty tables first. Restore the .n8n volume as above for the key, then start only the database, load the dump and start the rest:

Terminal
docker compose up -d postgres
Terminal
docker compose exec -T postgres sh -c 'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < n8n-2026-10-03.dump
Terminal
docker compose up -d

Match versions. A newer n8n updates the database schema when it starts, so restore onto the version that made the backup, then upgrade if you want to. Going back to an older version after a migration means running n8n db:revert on the newer one, once per migration. docker exec n8n n8n --version prints the running version.

Import workflows and credentials

To move workflows to another instance, or bring back items from a CLI export, import them. On a fresh instance, finish the owner setup in the browser first: the import commands need an owner account. Copy the files in, hand them to the node user, import, and restart:

Terminal
docker cp ./n8n-export-2026-10-03 n8n:/tmp/n8n-import
Terminal
docker exec -u 0 n8n chown -R 1000:1000 /tmp/n8n-import
Terminal
docker exec -u node n8n n8n import:workflow --separate --input=/tmp/n8n-import/workflows/
Terminal
docker exec -u node n8n n8n import:credentials --separate --input=/tmp/n8n-import/credentials/
Terminal
docker restart n8n
  • docker cp creates root-owned files; the chown lets node read them.
  • Items keep their IDs, so an item with the same ID on the target is overwritten.
  • Imported workflows arrive unpublished; publish them in the editor. --activeState=fromJson keeps each file's state, but only in queue or multi-main mode.
  • n8n's docs list a known issue: on a single instance, schedule triggers of a previously active workflow keep running after an import until n8n restarts. Hence the restart.
  • Encrypted credentials import only into an instance with the same key. Otherwise import a --decrypted export, which n8n encrypts again with the new instance's key.

Test the restore without running your workflows

A restored copy is a working n8n with your credentials: once it starts, schedule and polling triggers run against your real systems. So restore into a test volume named n8n_restore_test with the commands above, and unpublish every workflow before the copy starts. The CLI works while n8n is stopped:

Terminal
docker run --rm -v n8n_restore_test:/home/node/.n8n n8nio/n8n:2.41.6 unpublish:workflow --all

On n8n 1.x the command is update:workflow --all --active=false. Then check the key: decrypting every credential to /dev/null proves it matches without writing plain text anywhere. It ends with Successfully exported and a count, or fails with Credentials could not be decrypted:

Terminal
docker run --rm -v n8n_restore_test:/home/node/.n8n n8nio/n8n:2.41.6 export:credentials --all --decrypted --output=/dev/null

Now start the copy on a port bound to 127.0.0.1, so webhooks and outside visitors can't reach it, and open it through an SSH tunnel from your computer:

Terminal
docker run -d --name n8n-restore-test -p 127.0.0.1:5679:5678 -v n8n_restore_test:/home/node/.n8n n8nio/n8n:2.41.6
Terminal
curl -sf http://127.0.0.1:5679/healthz
Terminal
ssh -N -L 5679:127.0.0.1:5679 root@<server-ip>

Open http://localhost:5679, log in with your usual account, and check that the workflows are there, a few credentials open without errors and the execution list has its history. Then delete the copy:

Terminal
docker rm -f n8n-restore-test
Terminal
docker volume rm n8n_restore_test

With PostgreSQL, restore the dump into a separate database and point the test copy's DB_POSTGRESDB_* settings at it. Never start a test copy against the production database.

Testing restores covers making this a schedule; Docker volume backups covers volumes in general.

Common errors

ErrorFix
Credentials could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.The instance runs with another key. Restore .n8n/config from the backup, or set N8N_ENCRYPTION_KEY to the original key.
Mismatching encryption keysN8N_ENCRYPTION_KEY differs from the key in .n8n/config. Keep the one your credentials were encrypted with and make the other match.
EACCES: permission denied on a file in /home/node/.n8nRestored files belong to root. Run chown -R 1000:1000 on the volume.
No workflows found with specified filtersThere is nothing to export, or the CLI ran as a user other than node and read another user folder. Use docker exec -u node.
Failed to find ownerThe import ran before the owner account existed. Finish setup in the browser, then import.
The "--activeState=fromJson" flag can only be used when n8n is running in queue or multi-main mode.Drop the flag and publish the workflows in the editor.

Frequently asked questions

Where does n8n store its data?
In the .n8n folder of the user running it: ~/.n8n by default, or /home/node/.n8n in the Docker image, usually on a named volume. With the default SQLite database that folder holds everything, including the encryption key. With PostgreSQL, the folder holds the key and the database holds the rest.
Can I restore n8n credentials without the encryption key?
No. Credentials are encrypted with the key in .n8n/config or N8N_ENCRYPTION_KEY, and n8n cannot decrypt them without it. Keep a copy of the key outside the server.
Is n8n export:workflow --all a full backup?
No. It exports workflows, and export:credentials exports credentials. Users, execution history, variables and settings are left out. Back up the .n8n folder and the database as well.
Can I back up n8n while it is running?
Yes. Copy database.sqlite with sqlite3's VACUUM INTO or .backup, which read a consistent snapshot, or run pg_dump for PostgreSQL. Only a plain file copy of the folder needs n8n stopped.
How do I move n8n to a new server?
Restore the .n8n folder, and the PostgreSQL dump if you use one, on the new server and start the same n8n version. Workflows, credentials, users and history come along. Then point your domain at the new server.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: