Automated Config Backup with Secret Redaction

By Anas Semesmieh · April 28, 2026 · Homelab · Automation

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 full pipeline is open source: homelab/scripts/

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:

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

See the full scripts and configs in the homelab repo, or continue to the next post: Docker Compose All The Things.