← Library
runbook

n8n Workflow Backup and Restore

n8n, backup, automation, workflows, disaster-recovery

Purpose

Protect your n8n workflow automations from data loss. n8n stores workflows, credentials, and execution history in a database (SQLite by default, PostgreSQL if configured). Losing this data means rebuilding every workflow from scratch — and if you have dozens of automations, that's days of work.

What to Back Up

Data Location Priority
Workflows Database Critical — this is your automation logic
Credentials Database (encrypted) Critical — API keys, OAuth tokens, webhook secrets
Execution history Database Low — useful for debugging but not essential
Environment variables .env file or Compose High — encryption key, database connection
Custom nodes ~/.n8n/custom/ or mounted volume Medium — if you use community nodes

The most important thing to back up is the N8N_ENCRYPTION_KEY. Credentials are encrypted with this key. If you restore a database backup but use a different encryption key, all credential references will fail silently — workflows will run but auth will break everywhere.

Method 1: API Export (Workflow JSON)

n8n's REST API can export all workflows as JSON. This is the most portable format — you can import these into any n8n instance.

#!/bin/bash
BACKUP_DIR="/path/to/backups/n8n"
TIMESTAMP=$(date +%Y%m%d-%H%M)
N8N_URL="http://localhost:5678"
N8N_API_KEY="<your-n8n-api-key>"

mkdir -p "$BACKUP_DIR/$TIMESTAMP"

# Export all workflows
curl -s -H "X-N8N-API-KEY: $N8N_API_KEY" \
  "$N8N_URL/api/v1/workflows?limit=250" | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for wf in data.get('data', []):
    name = wf['name'].replace('/', '-').replace(' ', '_')
    with open(f'$BACKUP_DIR/$TIMESTAMP/{name}__{wf[\"id\"]}.json', 'w') as f:
        json.dump(wf, f, indent=2)
    print(f'Exported: {wf[\"name\"]} ({wf[\"id\"]})')
"

echo "Backup complete: $BACKUP_DIR/$TIMESTAMP"
ls -la "$BACKUP_DIR/$TIMESTAMP" | wc -l

Limitation: API export does not include credentials. The workflow JSON references credential IDs, but the actual secrets are in the database. You need the database backup for full restore.

Method 2: Database Backup (Complete)

SQLite (Default)

# Find the database
docker exec n8n ls -la /home/node/.n8n/database.sqlite

# Backup using SQLite's .backup command (safe while n8n is running)
docker exec n8n sqlite3 /home/node/.n8n/database.sqlite \
  ".backup '/home/node/.n8n/database-backup.sqlite'"

# Copy to host
docker cp n8n:/home/node/.n8n/database-backup.sqlite \
  /path/to/backups/n8n/database-$(date +%Y%m%d).sqlite

# Clean up
docker exec n8n rm /home/node/.n8n/database-backup.sqlite

PostgreSQL

docker exec n8n-postgres pg_dump -U n8n n8n > /path/to/backups/n8n/n8n-$(date +%Y%m%d).sql

Back Up the Encryption Key

# From your docker-compose.yml or .env — find and save the encryption key
grep N8N_ENCRYPTION_KEY /path/to/n8n/.env >> /path/to/backups/n8n/encryption-key.txt
chmod 600 /path/to/backups/n8n/encryption-key.txt

Store this separately and securely. Without it, database backups are useless for credential recovery.

Restore Procedures

Restore from API Export (Workflows Only)

# Import a single workflow
curl -X POST -H "X-N8N-API-KEY: $N8N_API_KEY" \
  -H "Content-Type: application/json" \
  -d @workflow-file.json \
  "$N8N_URL/api/v1/workflows"

After import, you'll need to re-create all credentials manually and re-link them in each workflow. This is tedious but works for migrating to a completely new instance.

Restore from Database (Full)

# Stop n8n
docker stop n8n

# Replace the database
docker cp /path/to/backups/n8n/database-20260329.sqlite \
  n8n:/home/node/.n8n/database.sqlite

# CRITICAL: Ensure the encryption key matches
# The N8N_ENCRYPTION_KEY in your .env must be the same one
# used when the backup was created

# Start n8n
docker start n8n

Verify by opening the n8n UI and checking that workflows load and credentials connect.

Automation

Schedule daily backups combining both methods:

#!/bin/bash
# Daily n8n backup — runs at 2am via cron/launchd
set -e

BACKUP_DIR="/path/to/backups/n8n/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# 1. Database backup (complete — includes credentials)
docker exec n8n sqlite3 /home/node/.n8n/database.sqlite \
  ".backup '/home/node/.n8n/backup.sqlite'"
docker cp n8n:/home/node/.n8n/backup.sqlite "$BACKUP_DIR/database.sqlite"
docker exec n8n rm /home/node/.n8n/backup.sqlite

# 2. API export (portable — human-readable workflow JSON)
# [API export script from above]

# 3. Integrity check
sqlite3 "$BACKUP_DIR/database.sqlite" "PRAGMA integrity_check;" | grep -q "ok" && \
  echo "Integrity: OK" || echo "WARNING: Database integrity check failed"

# 4. Prune backups older than 30 days
find /path/to/backups/n8n -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

Important: Never Restart n8n During Backup

Before running any backup that interacts with the n8n container, check for running executions:

curl -s -H "X-N8N-API-KEY: $N8N_API_KEY" \
  "$N8N_URL/api/v1/executions?status=running&limit=10" | \
  python3 -c "import json,sys; d=json.load(sys.stdin); print(f'{len(d.get(\"data\",[]))} running')"

If workflows are actively executing, wait for them to finish. Restarting n8n (or even heavy database operations) during execution can kill long-running workflows — I've lost TTS audio generation jobs this way.

What Good Looks Like

A healthy n8n backup strategy produces:

Running StdOut? Contribute a sanitized doc from your knowledge base — or get started:

Self-Host $149