Automated Encrypted Backups for n8n & PostgreSQL to AWS S3 and Cloudflare R2

Running an enterprise automation backbone without an automated, offsite disaster recovery pipeline is a catastrophic operational risk. If a volume corrupts, a host instance terminates unexpectedly, or an erroneous database migration executes, losing your n8n SQLite or PostgreSQL database wipes out all active credentials, custom nodes, execution logs, and workflow canvases.

A production-grade backup strategy requires three properties: consistent non-blocking database dumps, zero-trust symmetric encryption at rest, and immutable offsite replication to an S3-compatible bucket (such as AWS S3 or Cloudflare R2).

Prerequisite: Ensure your workflows and persistence layer are isolated via Docker. If you haven’t deployed your production cluster yet, follow our complete guide on How to Deploy n8n in Queue Mode with Docker Compose.

Disaster Recovery Architecture Diagram

<!-- Node 1: Cron / Scheduler -->
<rect x="30" y="40" width="180" height="65" rx="8" fill="#1E293B" stroke="#38BDF8" stroke-width="1.5"/>
<text x="120" y="68" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">Systemd / Cron</text>
<text x="120" y="88" fill="#38BDF8" font-size="11" text-anchor="middle">Scheduled Trigger (02:00 UTC)</text>

<!-- Path 1 -> 2 -->
<line x1="210" y1="72" x2="260" y2="72" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>

<!-- Node 2: pg_dump -->
<rect x="260" y="40" width="220" height="65" rx="8" fill="#0F172A" stroke="#2563EB" stroke-width="1.5"/>
<text x="370" y="68" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">PostgreSQL Container</text>
<text x="370" y="88" fill="#93C5FD" font-size="11" text-anchor="middle">pg_dump (Custom Stream Format)</text>

<!-- Path 2 -> 3 -->
<line x1="480" y1="72" x2="530" y2="72" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>

<!-- Node 3: GPG Encryption -->
<rect x="530" y="40" width="220" height="65" rx="8" fill="#1E293B" stroke="#F59E0B" stroke-width="1.5"/>
<text x="640" y="68" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">GPG Symmetric Layer</text>
<text x="640" y="88" fill="#FBBF24" font-size="11" text-anchor="middle">AES-256 Cipher Stream</text>

<!-- Path 3 down to Storage -->
<path d="M 640 105 L 640 180 L 410 180" fill="none" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>

<!-- Node 4: Rclone Engine -->
<rect x="190" y="150" width="220" height="65" rx="8" fill="#0F172A" stroke="#334155" stroke-width="1.5"/>
<text x="300" y="178" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">Rclone Sync Utility</text>
<text x="300" y="198" fill="#94A3B8" font-size="11" text-anchor="middle">TLS Multipart Transmission</text>

<!-- Path to Targets -->
<path d="M 300 215 L 300 270 L 190 270" fill="none" stroke="#10B981" stroke-width="2" marker-end="url(#arr-green)"/>
<path d="M 300 215 L 300 270 L 410 270" fill="none" stroke="#10B981" stroke-width="2" marker-end="url(#arr-green)"/>

<!-- Target 1: Cloudflare R2 -->
<rect x="20" y="240" width="170" height="65" rx="8" fill="#1E293B" stroke="#10B981" stroke-width="1.5"/>
<text x="105" y="268" fill="#34D399" font-size="13" font-weight="600" text-anchor="middle">Cloudflare R2</text>
<text x="105" y="288" fill="#94A3B8" font-size="11" text-anchor="middle">Zero Egress Fees</text>

<!-- Target 2: AWS S3 -->
<rect x="410" y="240" width="170" height="65" rx="8" fill="#1E293B" stroke="#10B981" stroke-width="1.5"/>
<text x="495" y="268" fill="#34D399" font-size="13" font-weight="600" text-anchor="middle">AWS S3 Glacier</text>
<text x="495" y="288" fill="#94A3B8" font-size="11" text-anchor="middle">Immutable Lock Archive</text>

Step 1: Install and Configure Rclone

Rclone is the industry standard for synchronizing files against S3-compatible cloud storage with native chunking, retries, and cryptographic checks.

Install Rclone on your Docker host machine:

sudo -v ; curl https://rclone.org/install.sh | sudo bash

Run the interactive wizard or directly create the configuration file at ~/.config/rclone/rclone.conf:

[remote_backup]
type = s3
provider = Cloudflare
access_key_id = YOUR_R2_OR_AWS_ACCESS_KEY
secret_access_key = YOUR_R2_OR_AWS_SECRET_KEY
endpoint = https://<ACCOUNT_ID>.r2.cloudflarestorage.com
acl = private

Test your remote connectivity:

rclone lsd remote_backup:

Step 2: The Production Backup Script

Create a dedicated system utility directory and script:

sudo mkdir -p /opt/scripts
sudo nano /opt/scripts/backup-n8n.sh

Paste the following bash script, which streams the database dump directly through GPG encryption before saving to disk and offloading to S3/R2:

#!/usr/bin/env bash
set -eo pipefail

# --- CONFIGURATION ---
BACKUP_DIR="/opt/n8n-cluster/backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
PG_CONTAINER="n8n-cluster-postgres-1"
PG_USER="n8n_cluster_admin"
PG_DB="n8n_production"
GPG_PASSPHRASE="YOUR_STRICTLY_SECURE_ENCRYPTION_PASSPHRASE"
REMOTE_TARGET="remote_backup:nocodeops-backups/n8n"
RETENTION_DAYS=14

mkdir -p "${BACKUP_DIR}"

echo "[$(date)] Starting hot dump for database: ${PG_DB}..."

# 1. Hot Dump streamed directly through GPG AES-256
docker exec -t "${PG_CONTAINER}" pg_dump -U "${PG_USER}" -d "${PG_DB}" -F c \
  | gpg --symmetric --batch --yes --passphrase "${GPG_PASSPHRASE}" --cipher-algo AES256 \
  > "${BACKUP_DIR}/n8n_db_${TIMESTAMP}.dump.gpg"

echo "[$(date)] Database encrypted successfully."

# 2. Backup n8n encryption key and environment config
tar -czf - /opt/n8n-cluster/.env \
  | gpg --symmetric --batch --yes --passphrase "${GPG_PASSPHRASE}" --cipher-algo AES256 \
  > "${BACKUP_DIR}/n8n_env_${TIMESTAMP}.tar.gz.gpg"

# 3. Offsite Sync via Rclone
echo "[$(date)] Uploading archives to offsite object storage..."
rclone copy "${BACKUP_DIR}" "${REMOTE_TARGET}" --include "*_${TIMESTAMP}.*"

# 4. Local Retention Cleanup
echo "[$(date)] Cleaning local backups older than ${RETENTION_DAYS} days..."
find "${BACKUP_DIR}" -type f -mtime +"${RETENTION_DAYS}" -name "*.gpg" -delete

echo "[$(date)] Backup and sync pipeline finished successfully."

Make the script executable and restrict permissions to the root user:

sudo chmod 700 /opt/scripts/backup-n8n.sh

Step 3: Automate Execution via Systemd or Crontab

Avoid relying on internal application timers; use the host system scheduler to ensure backups trigger even if Docker containers reboot.

Edit root crontab:

sudo crontab -e

Add an execution schedule running daily at 02:00 UTC, piping stdout and stderr to an audit log:

0 2 * * * /opt/scripts/backup-n8n.sh >> /var/log/n8n-backup.log 2>&1

Disaster Recovery: Restoring an Encrypted Backup

To recover your n8n cluster on a new server from offsite storage:

  1. Pull the encrypted files:
rclone copy remote_backup:nocodeops-backups/n8n/n8n_db_20260916_020000.dump.gpg ./

2. Decrypt the database dump:

gpg --decrypt --batch --passphrase "YOUR_STRICTLY_SECURE_ENCRYPTION_PASSPHRASE" \
  n8n_db_20260916_020000.dump.gpg > n8n_restore.dump

3. Restore clean state into PostgreSQL container:

docker exec -i n8n-cluster-postgres-1 pg_restore -U n8n_cluster_admin -d n8n_production --clean --no-owner < n8n_restore.dump

Frequently Asked Questions

Why use pg_dump -F c instead of raw SQL dumps?

The custom format (-F c) compresses the dump archive, supports parallel restore streams via pg_restore, and permits selective table restoration without syntax parsing errors.

Why choose Cloudflare R2 over AWS S3?

Cloudflare R2 is fully S3-compatible and charges zero egress bandwidth fees. If you need to download a 50 GB database archive for local staging or recovery testing, R2 incurs zero data transfer penalties compared to standard AWS data egress rates.

What happens if the N8N_ENCRYPTION_KEY is lost?

If your .env file containing the N8N_ENCRYPTION_KEY is lost, your workflows will restore, but all encrypted third-party credentials (API keys, OAuth tokens, SSH keys) become permanently unreadable. Backing up the encrypted .env alongside your database dump is mandatory.


Publicaciones Similares

3 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *