Automated Config Backup with Secret Redaction
The worst time to think about backups is after you've lost your configs. My homelab runs 10+ Docker Compose services, and losing even one docker-compose.yml or config.ini means hours of reconstruction from memory. So I built a fully automated backup pipeline that runs every night — and it's the single most valuable thing in my lab.
The Pipeline: Sync → Redact → Scan → Commit → Push
Every day at 03:25 UTC, a cron job triggers backup-and-push.sh, which orchestrates five stages:
1. Sync — Allowlist-based config capture
Instead of backing up everything (and drowning in noise), I maintain an explicit allowlist in inventory/include-paths.txt. Only files listed there get synced into configs/:
traefik/docker-compose.yml pihole/docker-compose.yml plex/config/Library/Application Support/Plex Media Server/Preferences.xml homepage/config/services.yaml arrstack/docker-compose.yml ...
The sync-configs.sh script reads each line, checks if the source file exists under $HOME, and copies it into the repo preserving directory structure. Runtime artifacts — logs, databases, media, temp files — are intentionally excluded.
2. Redact — Automatic secret removal
This is where it gets interesting. The redact-secrets.sh script uses sed to replace sensitive values with REDACTED placeholders. It handles:
- Generic patterns:
password,api_key,token,secret,client_secret - Service-specific patterns:
PlexOnlineToken,PlexOnlineUsername, Tailscale keys (tskey-api-*), Portainer tokens (ptr_*) - WireGuard private keys in the Arr stack's Gluetun config
The result: every config file is safe to commit to a public repo. No secrets ever touch Git history.
3. Scan — Safety gate before commit
Even with automated redaction, mistakes happen. The scan-secrets.sh script acts as a second line of defense, running inverse pattern matching:
grep -RInE '(password|api_?key|token|secret)[[:space:]]*[:=][[:space:]]*[^#[:space:]]+' configs/
If any match is found, the script exits with code 1 — and the entire pipeline stops. No commit, no push. This gate has saved me more than once from pushing a freshly added service config that the redaction patterns hadn't covered yet.
4. Commit — Only when changes exist
The script checks git status --porcelain and only commits if there are actual changes. Commit messages are standardized: backup: sync homelab configs.
5. Pull + Rebase + Push
Before pushing, the script does a git pull --rebase to maintain linear history. If there are conflicts (rare, since I'm the only contributor), it fails safely and waits for manual intervention.
Weekly Snapshot Tags
Every Sunday at 03:40 UTC, weekly-tag.sh runs the full daily backup first, then creates a dated tag:
git tag backup-2026-05-09 git push origin backup-2026-05-09
These tags serve as restore points. If I want to roll back to a known-good state from two weeks ago, I checkout that tag and run the restore workflow.
Monitoring Backup Health
I monitor backup health with a simple script that checks the age of backup-cron.log. If no backup has run in 30+ hours, it alerts. Logs are structured so I can grep for errors:
tail -50 ~/homelab-backup/backup-cron.log grep -i "error\|failed" ~/homelab-backup/backup-cron.log
Cron Setup
# Daily backup at 03:25 UTC 25 3 * * * cd /home/anas/homelab-backup && ./scripts/backup-and-push.sh >> backup-cron.log 2>&1 # Weekly snapshot tag at 03:40 UTC (Sunday) 40 3 * * 0 cd /home/anas/homelab-backup && ./scripts/weekly-tag.sh >> backup-weekly.log 2>&1
SSH keys are configured for passwordless Git push from cron. The remote is set to SSH (git@github.com:AnasSemesmieh/homelab.git) to avoid credential prompts.
What I Learned
- Allowlists beat blocklists. Explicitly listing what to back up is safer than trying to exclude everything you don't want.
- Two layers of secret protection. Automated redaction catches the expected; scanning catches the unexpected.
- Cron failures are silent. Always redirect stderr to a log file and monitor it.
- Weekly tags are cheap insurance. Git tags cost nothing but give you named restore points forever.
See the full scripts and configs in the homelab repo, or continue to the next post: Docker Compose All The Things.