A developer’s backup strategy needs to cover more than documents. Code history lives in git but local work-in-progress, environment configs, credentials managers, and databases need separate protection. This guide builds a 3-2-1 strategy: 3 copies, 2 different media, 1 offsite.
Table of Contents
- What Needs Backing Up
- Dotfiles: Git as Backup
- macOS: Time Machine + rsync Offsite
- Linux: Restic to S3/B2
- Local Database Backups
- Backup Verification (Critical)
- SSH Keys: Special Handling
- Cloud Sync Is Not a Backup
- Secrets and Environment Files
- Windows and WSL2 Considerations
- Testing Your Full Recovery Scenario
- Related Reading
What Needs Backing Up
Map your risk before picking tools:
Priority 1 — Irreplaceable
~/.ssh/ SSH keys (2FA recovery codes too)
~/.gnupg/ GPG keys
Password manager vault (Bitwarden/1Password export)
~/Documents/ Non-synced docs
Work-in-progress code Uncommitted local branches
Priority 2 — Recoverable but slow
~/.config/ App configs
~/.dotfiles/ Shell configs, editor settings
~/projects/ Committed code (in git, but local clones)
Local databases Dev DBs, SQLite files
Priority 3 — Re-creatable
node_modules/, .venv/ Dependencies (exclude from backup)
build/, dist/, .cache/ Build artifacts (exclude)
~/Downloads/ Temporary files
Dotfiles: Git as Backup
# Initialize dotfiles as a bare git repo
git init --bare $HOME/.dotfiles
# Alias for managing dotfiles
echo "alias dotfiles='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'" >> ~/.zshrc
source ~/.zshrc
# Hide untracked files (don't show everything in $HOME)
dotfiles config --local status.showUntrackedFiles no
# Add files
dotfiles add ~/.zshrc ~/.gitconfig ~/.vimrc ~/.tmux.conf
dotfiles add ~/.config/nvim/init.lua
dotfiles add ~/.config/alacritty/alacritty.toml
dotfiles commit -m "Add dotfiles"
dotfiles remote add origin git@github.com:yourname/dotfiles.git
dotfiles push -u origin main
# Restore on new machine
git clone --bare git@github.com:yourname/dotfiles.git $HOME/.dotfiles
alias dotfiles='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
dotfiles checkout
dotfiles config --local status.showUntrackedFiles no
macOS: Time Machine + rsync Offsite
# Time Machine: local backup to external drive
# System Settings > Time Machine > Add Backup Disk
# Verify Time Machine is running
tmutil status
# BackupPhase = Copying
# DateOfLatestBackup = 2026-03-22-030000
# Exclude large folders from Time Machine
tmutil addexclusion ~/projects/node_modules
tmutil addexclusion ~/.npm
tmutil addexclusion ~/.cache
tmutil addexclusion ~/Library/Caches
# List exclusions
tmutil isexcluded ~/projects/node_modules
#!/bin/bash
# scripts/offsite-backup.sh
# Runs daily via launchd, syncs to Hetzner storage box
REMOTE_HOST="your-storagebox.your-storagebox.de"
REMOTE_USER="your-username"
REMOTE_PATH="/backup/$(hostname)"
LOG="/var/log/offsite-backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG"; }
log "Starting offsite backup"
rsync -avz \
--delete \
--exclude='.DS_Store' \
--exclude='node_modules/' \
--exclude='.venv/' \
--exclude='__pycache__/' \
--exclude='.cache/' \
--exclude='*.pyc' \
--backup \
--backup-dir="$REMOTE_PATH/deleted/$(date +%Y%m%d)" \
-e "ssh -i ~/.ssh/backup_key -p 23" \
~/Documents/ ~/projects/ ~/.ssh/ ~/.config/ \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/files/" \
>> "$LOG" 2>&1
if [ $? -eq 0 ]; then
log "Offsite backup succeeded"
else
log "Offsite backup FAILED"
fi
<!-- ~/Library/LaunchAgents/com.yourname.backup.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.yourname.backup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/yourname/scripts/offsite-backup.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/com.yourname.backup.plist
Linux: Restic to S3/B2
Restic is the best open-source backup tool — deduplication, encryption, versioning.
# Install restic
brew install restic # macOS
sudo apt install restic # Ubuntu
# Initialize repository (Backblaze B2)
export B2_ACCOUNT_ID="your-account-id"
export B2_ACCOUNT_KEY="your-app-key"
export RESTIC_PASSWORD="your-strong-encryption-password"
restic -r b2:your-bucket-name:backups init
# Or use a local repo for testing
restic -r /Volumes/ExternalDrive/restic-backups init
#!/bin/bash
# scripts/restic-backup.sh
export B2_ACCOUNT_ID="${B2_ACCOUNT_ID}"
export B2_ACCOUNT_KEY="${B2_ACCOUNT_KEY}"
export RESTIC_PASSWORD="${RESTIC_PASSWORD}"
REPO="b2:your-bucket-name:backups"
# Backup
restic -r "$REPO" backup \
~/Documents \
~/projects \
~/.ssh \
~/.config \
--exclude-file ~/.restic-excludes \
--tag "daily" \
--verbose
# Forget old snapshots (keep 7 daily, 4 weekly, 12 monthly)
restic -r "$REPO" forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--prune
# Verify integrity
restic -r "$REPO" check
# ~/.restic-excludes
node_modules/
.venv/
__pycache__/
*.pyc
.cache/
.npm/
.gradle/
target/
build/
dist/
*.log
.DS_Store
Local Database Backups
#!/bin/bash
# scripts/backup-local-dbs.sh
# Back up all local development databases
BACKUP_DIR="$HOME/.db-backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# PostgreSQL
if command -v pg_dumpall &>/dev/null; then
pg_dumpall -U postgres | gzip > "$BACKUP_DIR/postgres_all_${DATE}.sql.gz"
echo "PostgreSQL backed up"
fi
# MySQL
if command -v mysqldump &>/dev/null; then
mysqldump --all-databases | gzip > "$BACKUP_DIR/mysql_all_${DATE}.sql.gz"
echo "MySQL backed up"
fi
# SQLite files in projects
find ~/projects -name "*.sqlite" -o -name "*.db" 2>/dev/null | while read f; do
rel="${f#$HOME/}"
dest="$BACKUP_DIR/sqlite/${DATE}/${rel}"
mkdir -p "$(dirname "$dest")"
cp "$f" "$dest"
done
# Remove backups older than 7 days
find "$BACKUP_DIR" -name "*.gz" -mtime +7 -delete
find "$BACKUP_DIR/sqlite" -mtime +7 -type f -delete
echo "DB backups complete in $BACKUP_DIR"
Backup Verification (Critical)
Backups you haven’t tested are worthless. Run monthly:
#!/bin/bash
# scripts/verify-backups.sh
echo "=== Backup Verification $(date) ==="
# Test restic restore
restic -r "b2:your-bucket-name:backups" \
restore latest \
--target /tmp/restore-test \
--include ~/.zshrc
if [ -f /tmp/restore-test/.zshrc ]; then
echo "PASS: Restic restore works"
rm -rf /tmp/restore-test
else
echo "FAIL: Restic restore failed"
fi
# Verify latest snapshot exists and is recent
LATEST=$(restic -r "b2:your-bucket-name:backups" snapshots --last --json | jq -r '.[0].time')
DAYS_OLD=$(( ($(date +%s) - $(date -d "$LATEST" +%s)) / 86400 ))
if [ "$DAYS_OLD" -lt 2 ]; then
echo "PASS: Latest backup is ${DAYS_OLD} days old"
else
echo "FAIL: Latest backup is ${DAYS_OLD} days old — too old!"
fi
SSH Keys: Special Handling
# Never store raw private keys in cloud sync
# Instead: export encrypted with GPG
gpg --symmetric --cipher-algo AES256 ~/.ssh/id_ed25519
# Creates: ~/.ssh/id_ed25519.gpg
# Store the encrypted version in your git dotfiles or cloud
# Restore:
gpg --decrypt ~/.ssh/id_ed25519.gpg > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
Cloud Sync Is Not a Backup
iCloud, Dropbox, and Google Drive sync deletions instantly. If you accidentally rm -rf ~/projects/critical-work, the deletion propagates to every device within seconds. These services are useful for active-file access across devices, but they are not backups.
The key distinction: sync replicates your current state; backup preserves historical states. Use both, and make sure they are independent.
For Dropbox and Google Drive, enable extended version history (Dropbox Plus gives 180 days; Google Drive keeps 30 days of versions). This helps with accidental overwrites but does not protect against ransomware or account compromise.
Secrets and Environment Files
.env files and credential JSON files are the most dangerous things to lose — and the most dangerous to back up carelessly. A structured approach:
# Audit what secrets you have locally
find ~ -name ".env" -o -name "*.env" -o -name "credentials.json" \
-o -name "service-account.json" 2>/dev/null | grep -v node_modules | grep -v .git
# For each critical secrets file, store an encrypted copy
# Use age (modern, simpler than GPG) for file encryption
brew install age
# Generate a key pair (store the private key in your password manager)
age-keygen -o ~/.age/key.txt
# Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aq
# Encrypt a secrets file
age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aq \
-o ~/.secrets-backup/myproject.env.age \
~/projects/myproject/.env
# Decrypt on restore
age -d -i ~/.age/key.txt \
~/.secrets-backup/myproject.env.age > ~/projects/myproject/.env
Add the .secrets-backup/ directory to your restic or rsync offsite backup. Keep the age private key in your password manager (1Password, Bitwarden) with a backup export.
Windows and WSL2 Considerations
Remote developers on Windows using WSL2 have a split filesystem. Back up both sides:
# WSL2 home directory (inside the Linux layer)
# Access from Windows PowerShell:
# \\wsl$\Ubuntu\home\yourname
# Back up WSL2 with export (creates a tarball)
wsl --export Ubuntu C:\Backups\ubuntu-wsl-$(Get-Date -Format "yyyyMMdd").tar
# Or use restic from inside WSL2 (same script as Linux)
# Install restic in WSL2:
sudo apt install restic
# Windows Documents folder is mounted at /mnt/c/Users/yourname/Documents
# Include it in your restic backup:
restic -r "$REPO" backup \
~/Documents \
/mnt/c/Users/yourname/Documents \
~/.ssh \
~/.config
For Windows-side tooling, WinSCP supports rsync-like sync to remote SSH targets. Robocopy handles local redundancy well:
# Mirror Documents to external drive (Windows)
robocopy C:\Users\yourname\Documents E:\Backup\Documents /MIR /R:3 /W:5 /LOG:C:\Logs\backup.log
Testing Your Full Recovery Scenario
Running the verification script monthly is good. Running a full recovery drill quarterly is better. Document the scenario:
Recovery Drill Checklist (run on a fresh machine or VM):
[ ] Restore dotfiles from git bare repo
[ ] Decrypt and restore SSH keys from backup
[ ] Restore .env files from encrypted backup
[ ] Clone critical repos (verify SSH keys work)
[ ] Restore dev database from latest backup
[ ] Verify app starts and connects to local DB
[ ] Confirm shell aliases, editor config, git config all present
[ ] Total time to full working environment: ______ minutes
The goal is to know, not guess, how long recovery takes. Teams that have done this drill typically discover that their database restore script has a bug, or that a critical .env file was never added to the backup scope. Find these gaps during drills, not during an actual incident.
Related Reading
- Best Backup Solutions for Remote Developer Machines
- How to Set Up MinIO for Team Object Storage
- Best Dotfiles Manager for Remote Developer Setup
- Podcast Guesting Strategy for Freelance Developers