URL: https://mkdocs.justinsforge.com/memory/general/reference_forge_backup_strategy/
Forge Backup Strategy¶
Forge lives at /home/justinwieb/forge on Console (VM 103, 192.168.86.50), a 24 GB root disk that is currently 89% full (20 GB used, 2.6 GB free). The repo is ~266 MB on disk. Git remote: https://github.com/JustinWieb/forge.git. This document covers what needs backing up, where each piece goes, and concrete cron entries with retention windows.
1. What Is Already Covered¶
Git Remote (GitHub)¶
git remote -v confirms origin https://github.com/JustinWieb/forge.git. Everything committed to the repo and pushed is durable. This covers:
scripts/(all Python, bash, and helper scripts)brands/(READMEs, Shopify locales, brand config)infra/(systemd units, logrotate config, n8n scaffolds, context-api source)system-map/(architecture.md, fleet.md, steering.md, google-drive.md)CLAUDE.md,FORGE-DOCTRINE.md,LESSONS.md,README.mdmemory/(handoffs, daily logs, general topic files that are checked in)data/time_categories.json,data/README.md, and other small committed data files
Gap: Anything with unstaged or uncommitted changes is NOT protected. The current git status shows numerous modified files (locales, READMEs, LESSONS.md) and deleted files staged but not committed. A commit discipline gap means the remote lags the working tree.
Finn Workspace Nightly Rsync¶
A nightly rsync at 02:00 on Finn copies /mnt/pve/fast-storage/ (the 8TB NVMe) to /mnt/storage/workspace-backup/ with versioned deltas at /mnt/storage/workspace-backup-versions/YYYY-MM-DD/. However, Forge itself is NOT on the NVMe; it lives on Console's local disk (/home/justinwieb/), which is an Ubuntu VM disk image on Finn but not in the /mnt/pve/fast-storage/ share. Console's root disk is NOT caught by this rsync.
2. What Is NOT Currently Backed Up¶
| Asset | Location | Risk |
|---|---|---|
forge/ working tree (uncommitted changes) |
Console /home/justinwieb/forge/ |
Lost if Console VM disk corrupts |
~/.claude/ (project memory, skills, settings) |
Console /home/justinwieb/.claude/ |
All session memory, feedback files, reference files |
~/.forge-secrets/ |
Console /home/justinwieb/.forge-secrets/ |
Credentials (DO NOT back up to git or Drive; covered separately below) |
~/.config/rclone/rclone.conf |
Console /home/justinwieb/.config/ |
Drive auth; losing it requires OAuth re-auth |
forge/data/ (runtime JSONL, context DB, gdrive-cache) |
/home/justinwieb/forge/data/ |
Hevy, quota, telegram chat history, search DB |
forge/logs/ |
/home/justinwieb/forge/logs/ |
Operational logs (lower priority, but useful for incident review) |
| n8n SQLite DB | CT 106 /opt/n8n/ (inside container) |
All n8n workflows and credentials |
| n8n Docker config | CT 106 /opt/n8n/docker-compose.yml |
Workflow engine config |
3. Backup Destinations: What Goes Where¶
Rule: Never Put Secrets in Git or Drive¶
~/.forge-secrets/ stays out of all backup paths that touch git, GitHub, or shared drives. The correct backup for secrets is an encrypted local copy to /mnt/storage/forge-secrets-encrypted/ using gpg --symmetric, plus manual export to NordPass for critical credentials.
Google Drive (via rclone gdrive:)¶
Use for: human-readable assets and docs that Justin might need to recover from any device.
| What | Source | Drive Destination |
|---|---|---|
Forge memory/ snapshot |
/home/justinwieb/forge/memory/ |
gdrive:Forge-Backups/memory/YYYY-MM-DD/ |
| Claude memory layer | /home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/ |
gdrive:Forge-Backups/claude-memory/YYYY-MM-DD/ |
| Forge critical configs | CLAUDE.md, FORGE-DOCTRINE.md, LESSONS.md |
gdrive:Forge-Backups/root-docs/YYYY-MM-DD/ |
Drive retention: keep last 30 daily snapshots, then one per week for 90 days, then one per month indefinitely. Implement with a sweep that deletes daily snapshots older than 30 days and weekly ones older than 90 days.
Finn Media Drive (/mnt/storage/ via NFS from Console)¶
Use for: full working-tree tarballs and n8n database. Fast LAN transfer, large capacity.
| What | Source | Finn Destination |
|---|---|---|
| Full forge tarball | /home/justinwieb/forge/ |
/mnt/storage/forge-backups/daily/forge-YYYY-MM-DD.tar.gz |
| Claude home dir | /home/justinwieb/.claude/ |
/mnt/storage/forge-backups/daily/claude-YYYY-MM-DD.tar.gz |
| n8n DB + config | CT 106 /opt/n8n/ (rsync via SSH) |
/mnt/storage/forge-backups/n8n/n8n-YYYY-MM-DD.tar.gz |
| rclone config | /home/justinwieb/.config/rclone/ |
/mnt/storage/forge-backups/config/rclone-YYYY-MM-DD.tar.gz |
| Secrets (encrypted) | /home/justinwieb/.forge-secrets/ |
/mnt/storage/forge-backups/secrets-encrypted/secrets-YYYY-MM-DD.tar.gz.gpg |
Finn retention: keep last 14 daily tarballs, 8 weekly tarballs, 12 monthly tarballs. Implemented by the proposed forge_backup.sh script below.
Both (Drive AND Finn)¶
The Claude memory layer (~/.claude/projects/-home-justinwieb-forge/memory/) is small (596 KB) and high-value (all learned preferences, feedback corrections, reference files). Write it to both destinations nightly.
4. Script: forge_backup.sh (SHIPPED 2026-04-30)¶
Location: /home/justinwieb/forge/scripts/forge_backup.sh
Uses rclone directly for Drive (not forge_gdrive_write.py) for directory-level efficiency. Cron wired at 06:30 UTC. GPG passphrase created at ~/.forge-secrets/.backup-passphrase (passphrase must be saved to NordPass before relying on secrets backup). Smoke tested 2026-04-30: forge tarball 124 MB to Finn, Drive memory sync 130 files OK.
n8n volume path confirmed: /var/lib/docker/volumes/n8n_n8n_data/_data on host n8n.
#!/usr/bin/env bash
# forge_backup.sh — nightly forge backup to Finn media drive + Google Drive
# Cron: 01:30 CT daily (06:30 UTC)
# Log: /home/justinwieb/forge/logs/backup.log
set -euo pipefail
FORGE_DIR="/home/justinwieb/forge"
CLAUDE_MEM="/home/justinwieb/.claude/projects/-home-justinwieb-forge/memory"
RCLONE_CONF="/home/justinwieb/.config/rclone"
SECRETS_DIR="/home/justinwieb/.forge-secrets"
BACKUP_BASE="/mnt/storage/forge-backups"
GDRIVE_BASE="Forge-Backups"
DATE=$(date +%Y-%m-%d)
LOG="$FORGE_DIR/logs/backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
log "=== forge backup start $DATE ==="
# --- Finn: full forge tarball ---
mkdir -p "$BACKUP_BASE/daily"
FORGE_TAR="$BACKUP_BASE/daily/forge-$DATE.tar.gz"
tar --exclude="$FORGE_DIR/logs" \
--exclude="$FORGE_DIR/data/search" \
--exclude="$FORGE_DIR/data/gdrive-cache" \
--exclude="$FORGE_DIR/scripts/__pycache__" \
-czf "$FORGE_TAR" -C /home/justinwieb forge
log "forge tarball: $FORGE_TAR ($(du -sh "$FORGE_TAR" | cut -f1))"
# --- Finn: claude memory tarball ---
CLAUDE_TAR="$BACKUP_BASE/daily/claude-$DATE.tar.gz"
tar -czf "$CLAUDE_TAR" -C /home/justinwieb .claude
log "claude tarball: $CLAUDE_TAR"
# --- Finn: rclone config ---
mkdir -p "$BACKUP_BASE/config"
RCLONE_TAR="$BACKUP_BASE/config/rclone-$DATE.tar.gz"
tar -czf "$RCLONE_TAR" -C /home/justinwieb ".config/rclone"
log "rclone config tarball: $RCLONE_TAR"
# --- Finn: n8n DB (stop container, copy, restart) ---
mkdir -p "$BACKUP_BASE/n8n"
N8N_TAR="$BACKUP_BASE/n8n/n8n-$DATE.tar.gz"
ssh n8n "docker compose -f /opt/n8n/docker-compose.yml stop && \
tar -czf /tmp/n8n-backup.tar.gz -C /opt/n8n . && \
docker compose -f /opt/n8n/docker-compose.yml start" && \
scp n8n:/tmp/n8n-backup.tar.gz "$N8N_TAR" && \
ssh n8n "rm /tmp/n8n-backup.tar.gz"
log "n8n tarball: $N8N_TAR"
# --- Finn: secrets encrypted ---
mkdir -p "$BACKUP_BASE/secrets-encrypted"
SECRETS_TAR="$BACKUP_BASE/secrets-encrypted/secrets-$DATE.tar.gz.gpg"
tar -cz -C /home/justinwieb .forge-secrets | \
gpg --batch --yes --passphrase-file "$SECRETS_DIR/.backup-passphrase" \
--symmetric --output "$SECRETS_TAR"
log "secrets encrypted: $SECRETS_TAR"
# --- Drive: memory snapshot (uses forge_gdrive_write.py) ---
GDRIVE_MEM_DEST="$GDRIVE_BASE/memory/$DATE"
GDRIVE_WRITE="$FORGE_DIR/scripts/forge_gdrive_write.py"
for f in "$FORGE_DIR/memory/general/"*.md; do
fname=$(basename "$f")
python3 "$GDRIVE_WRITE" "$f" "$GDRIVE_MEM_DEST/$fname" --mkdir-parents 2>>"$LOG" || true
done
log "Drive memory snapshot: $GDRIVE_MEM_DEST"
# --- Drive: claude memory snapshot ---
GDRIVE_CLAUDE_DEST="$GDRIVE_BASE/claude-memory/$DATE"
for f in "$CLAUDE_MEM/"*.md; do
fname=$(basename "$f")
python3 "$GDRIVE_WRITE" "$f" "$GDRIVE_CLAUDE_DEST/$fname" --mkdir-parents 2>>"$LOG" || true
done
log "Drive claude-memory snapshot: $GDRIVE_CLAUDE_DEST"
# --- Drive: root docs ---
GDRIVE_DOCS_DEST="$GDRIVE_BASE/root-docs/$DATE"
for doc in CLAUDE.md FORGE-DOCTRINE.md LESSONS.md README.md; do
python3 "$GDRIVE_WRITE" "$FORGE_DIR/$doc" "$GDRIVE_DOCS_DEST/$doc" --mkdir-parents 2>>"$LOG" || true
done
log "Drive root docs: $GDRIVE_DOCS_DEST"
# --- Finn retention sweep ---
# Keep 14 daily, 8 weekly (Sundays), 12 monthly (1st of month)
for dir in daily n8n; do
find "$BACKUP_BASE/$dir" -name "*.tar.gz" -mtime +14 | while read -r f; do
bname=$(basename "$f")
fdate=$(echo "$bname" | grep -oP '\d{4}-\d{2}-\d{2}')
dow=$(date -d "$fdate" +%u 2>/dev/null || echo "0")
dom=$(date -d "$fdate" +%d 2>/dev/null || echo "0")
age=$(( ( $(date +%s) - $(date -d "$fdate" +%s 2>/dev/null || echo 0) ) / 86400 ))
# Keep Sundays for 8 weeks (56 days)
if [[ "$dow" == "7" && "$age" -lt 57 ]]; then continue; fi
# Keep 1st of month for 12 months (365 days)
if [[ "$dom" == "01" && "$age" -lt 366 ]]; then continue; fi
log "pruning $f (age ${age}d)"
rm -f "$f"
done
done
log "=== forge backup done ==="
5. Cron Entries¶
Add these to the justinwieb crontab. Uses UTC. 06:30 UTC = 01:30 CT (CDT), well after auto-dream (04:00 UTC) and quota aggregator (04:30 UTC) finish.
# forge-backup begin
# Full forge backup: Finn media drive tarballs + Drive memory snapshots. 01:30 CT = 06:30 UTC.
30 6 * * * /home/justinwieb/forge/scripts/forge_backup.sh >> /home/justinwieb/forge/logs/backup.log 2>&1
# forge-backup end
No separate weekly or monthly cron is needed; the retention sweep inside forge_backup.sh handles the promotion logic.
6. Nightly Sequence (Consolidated View)¶
After adding the backup job the nightly schedule on Console looks like this:
| UTC | CT (CDT) | Job |
|---|---|---|
| 03:00 | 22:00 | Eval harness (forge_eval_harness.py) |
| 03:30 | 22:30 | Search index rebuild (forge_search_index.py) |
| 04:00 | 23:00 | Auto-dream memory consolidation (forge_memory_auto_dream.py) |
| 04:15 | 23:15 | Retention sweep (forge_cleanup_retention.sh) |
| 04:30 | 23:30 | Quota aggregator (forge_quota_tracker.py) |
| 06:30 | 01:30 | Forge backup (forge_backup.sh) -- new |
The backup runs last, after all mutation jobs, so the snapshot captures a settled state.
7. n8n Backup Note¶
The n8n SQLite database lives at CT 106 (forge-n8n, 192.168.86.82). Per reference_n8n_state.md: never docker cp the DB while the container runs (WAL corruption risk). The proposed forge_backup.sh stops the container, tars /opt/n8n/, restarts, then copies the tar over SSH. This produces a consistent snapshot at the cost of ~30 seconds of n8n downtime at 01:30 CT, which is acceptable. If that window becomes a problem, the alternative is sqlite3 /opt/n8n/data/database.sqlite ".backup /tmp/n8n.db" (WAL-safe online backup) before stopping.
8. Secrets Backup¶
The .backup-passphrase file referenced above must be created manually:
# one-time setup on Console
openssl rand -base64 32 > ~/.forge-secrets/.backup-passphrase
chmod 600 ~/.forge-secrets/.backup-passphrase
# write the passphrase into NordPass manually before relying on this
The encrypted tarball at /mnt/storage/forge-backups/secrets-encrypted/ is useless without the passphrase, which must be stored in NordPass (the canonical credential store per doctrine Section security rules).
9. Console VM Disk (Worst-Case Scenario)¶
All of the above backs up the Forge working tree, but the Console VM disk image itself (CT/VM 103 on Finn) is not snapshotted via Proxmox Backup Server (PBS) by default. Adding a PBS backup job for VM 103 at the Proxmox level gives a full VM restore point independent of any file-level backup. This is out of scope for this document but is the recommended next step for full disaster recovery coverage.
10. What to Skip¶
Do not back up:
- forge/logs/ (large, ephemeral, logrotate already handles rotation)
- forge/data/search/ (sqlite-vec index, fully rebuilt by forge_search_index.py from source files)
- forge/data/gdrive-cache/ (fully rebuilt by forge_gdrive_index_extend.py)
- forge/scripts/__pycache__/ (bytecode, auto-regenerated)
- forge/assets/asset-drop/ (raw binary drops, not committed, treated as ephemeral staging)
[Claude Code, 2026-04-30]