From lehnert-skills
Use when user wants to back up a Linux server, design a backup strategy, write a backup script, set up automated backups, configure restic or borgbackup, restore from a backup, test backup integrity, back up databases, Docker volumes, or asks about the 3-2-1 backup rule or disaster recovery.
How this skill is triggered — by the user, by Claude, or both
Slash command
/lehnert-skills:linux-backup-restoreThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Designs and generates a complete backup solution tailored to what the user needs to protect. Presents tool options, handles the full lifecycle: backup creation, encryption, remote transfer, retention, integrity testing, and restore procedures. Writes all scripts and configs to disk.
Designs and generates a complete backup solution tailored to what the user needs to protect. Presents tool options, handles the full lifecycle: backup creation, encryption, remote transfer, retention, integrity testing, and restore procedures. Writes all scripts and configs to disk.
Language: Respond in the user's language. All scripts and configs in English.
Map the user's request to a mode. If unclear, ask ONE question.
| User says | Mode |
|---|---|
| "back up my server", "back up my VPS" | Full server backup strategy |
| "back up my database", "PostgreSQL backup", "MySQL backup" | Database backup |
| "back up Docker volumes" | Docker volume backup |
| "back up my files / directory" | Directory backup |
| "set up automated backups" | Scheduled backup with cron/systemd |
| "restore from backup", "I lost my data" | Restore procedure |
| "test my backups", "verify integrity" | Backup testing |
| "off-site backup", "remote backup", "S3", "cloud" | Remote / cloud backup |
If the scope is vague, ask:
"What do you want to back up? (e.g. specific directories, databases, Docker volumes, or the whole server)"
When the user hasn't specified a tool, present options based on the use case:
Here are the best backup tools for your use case. Which fits your needs?
1. Restic (Recommended) Modern, fast, encrypted-by-default backup with deduplication. Supports local, SSH, S3, Backblaze B2, and many other backends. Easy restore and snapshot management. Best for: most use cases, especially with remote/cloud storage.
2. BorgBackup Excellent deduplication and compression. Encrypted. Slightly faster than restic for large datasets on the same machine. Requires borg on both ends for remote. Best for: large datasets, same-machine or SSH remote backup.
3. rsync Simple, fast, widely available. No deduplication or encryption built-in. Best for simple directory mirroring. Best for: simple local or SSH mirror, no versioning needed.
4. tar + gpg Basic but universal. Compressed archive with optional GPG encryption. No incremental, no deduplication. Best for: one-off archives, maximum compatibility.
Present: pg_dump (PostgreSQL), mysqldump / xtrabackup (MySQL/MariaDB), mongodump (MongoDB), plus restic wrapping the dump file.
Present: docker run --volumes-from + tar, or restic with Docker volume path, or pre-stop dump approach.
Initialize repository:
# Local
restic init --repo /mnt/backups/myserver
# S3 (AWS or compatible like Backblaze B2, MinIO)
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
restic init --repo s3:https://s3.amazonaws.com/bucket-name/myserver
# SFTP / SSH
restic init --repo sftp:user@backuphost:/backups/myserver
# S3-compatible (MinIO, Wasabi, Linode Object Storage, Backblaze S3-compatible)
# Set custom endpoint via environment variable:
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
# Wasabi (us-east-1):
restic init --repo s3:https://s3.us-east-1.wasabisys.com/bucket-name/myserver
# MinIO (self-hosted):
restic init --repo s3:http://minio.example.com:9000/bucket-name/myserver
# Linode (Newark):
restic init --repo s3:https://us-east-1.linodeobjects.com/bucket-name/myserver
# Backblaze B2
export B2_ACCOUNT_ID=your_id
export B2_ACCOUNT_KEY=your_key
restic init --repo b2:bucket-name:myserver
Backup script (backup-restic.sh):
#!/usr/bin/env bash
set -euo pipefail
# ── Config ──────────────────────────────────────────────────
BACKUP_REPO="${BACKUP_REPO:-/mnt/backups/myserver}"
BACKUP_PASSWORD="${RESTIC_PASSWORD:?RESTIC_PASSWORD not set}"
SOURCES=("/etc" "/home" "/var/www" "/opt")
EXCLUDES=("*.log" "*.tmp" "/proc" "/sys" "/dev" "/run" "/tmp")
RETENTION_DAILY=7
RETENTION_WEEKLY=4
RETENTION_MONTHLY=6
LOG_FILE="/var/log/restic-backup.log"
export RESTIC_REPOSITORY="$BACKUP_REPO"
export RESTIC_PASSWORD="$BACKUP_PASSWORD"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
# ── Build exclude args ───────────────────────────────────────
EXCLUDE_ARGS=()
for excl in "${EXCLUDES[@]}"; do
EXCLUDE_ARGS+=(--exclude "$excl")
done
# ── Run backup ───────────────────────────────────────────────
log "Starting backup to $BACKUP_REPO"
restic backup "${SOURCES[@]}" "${EXCLUDE_ARGS[@]}" \
--tag "$(hostname)" \
--tag "auto" \
2>&1 | tee -a "$LOG_FILE"
# ── Apply retention policy ───────────────────────────────────
log "Applying retention policy"
restic forget \
--keep-daily "$RETENTION_DAILY" \
--keep-weekly "$RETENTION_WEEKLY" \
--keep-monthly "$RETENTION_MONTHLY" \
--prune \
2>&1 | tee -a "$LOG_FILE"
# ── Verify last snapshot ─────────────────────────────────────
log "Verifying latest snapshot"
restic check 2>&1 | tee -a "$LOG_FILE"
log "Backup complete"
Restore commands:
# List snapshots
restic snapshots
# Restore latest snapshot to original paths
restic restore latest --target /
# Restore specific snapshot to a temp directory
restic restore abc1234 --target /tmp/restore
# Restore only specific paths
restic restore latest --target /tmp/restore --path /etc/nginx
# Mount snapshot as filesystem (browse before restoring)
restic mount /mnt/restic-mount
ls /mnt/restic-mount/snapshots/latest/
Initialize repository:
# Local with encryption
borg init --encryption=repokey /mnt/backups/myserver
# Remote via SSH
borg init --encryption=repokey user@backuphost:/backups/myserver
Backup script (backup-borg.sh):
#!/usr/bin/env bash
set -euo pipefail
BORG_REPO="${BORG_REPO:-/mnt/backups/myserver}"
export BORG_PASSPHRASE="${BORG_PASSPHRASE:?BORG_PASSPHRASE not set}"
SOURCES="/etc /home /var/www /opt"
LOG_FILE="/var/log/borg-backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
log "Starting borg backup"
borg create \
--verbose \
--filter AME \
--list \
--stats \
--show-rc \
--compression lz4 \
--exclude-caches \
--exclude '/home/*/.cache/*' \
--exclude '/var/tmp/*' \
"${BORG_REPO}::{hostname}-{now:%Y-%m-%dT%H:%M:%S}" \
$SOURCES \
2>&1 | tee -a "$LOG_FILE"
log "Pruning old backups"
borg prune \
--list \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
"${BORG_REPO}" \
2>&1 | tee -a "$LOG_FILE"
log "Checking repository integrity"
borg check "${BORG_REPO}" 2>&1 | tee -a "$LOG_FILE"
log "Backup complete"
PostgreSQL:
#!/usr/bin/env bash
set -euo pipefail
DB_NAME="${POSTGRES_DB:?}"
DB_USER="${POSTGRES_USER:?}"
BACKUP_DIR="/var/backups/postgresql"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/pg-backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
mkdir -p "$BACKUP_DIR"
log "Dumping $DB_NAME"
# -Fc = custom format with built-in zlib compression — do NOT pipe through gzip
# (pg_restore cannot read double-compressed files)
pg_dump -U "$DB_USER" -Fc "$DB_NAME" > "${BACKUP_DIR}/${DB_NAME}_${DATE}.dump"
# Verify the dump is readable and non-empty
pg_restore --list "${BACKUP_DIR}/${DB_NAME}_${DATE}.dump" > /dev/null \
|| { log "ERROR: Dump is corrupted or unreadable"; exit 1; }
DUMP_KB=$(du -k "${BACKUP_DIR}/${DB_NAME}_${DATE}.dump" | cut -f1)
if [[ $DUMP_KB -lt 10 ]]; then
log "ERROR: Dump suspiciously small (${DUMP_KB}KB) — possible empty database or write failure"
exit 1
fi
log "Dump verified OK (${DUMP_KB}KB)"
# Retain last 14 dumps
find "$BACKUP_DIR" -name "${DB_NAME}_*.dump" -mtime +14 -delete
log "PostgreSQL backup complete: ${DB_NAME}_${DATE}.dump"
Restore PostgreSQL:
# Drop and recreate the database
dropdb -U "$DB_USER" "$DB_NAME"
createdb -U "$DB_USER" "$DB_NAME"
pg_restore -U "$DB_USER" -d "$DB_NAME" backup.dump
MySQL / MariaDB:
mysqldump \
--single-transaction \
--routines \
--triggers \
--all-databases \
-u root -p"${MYSQL_ROOT_PASSWORD}" \
| gzip > "/var/backups/mysql/all_${DATE}.sql.gz"
# Restore
gunzip < backup.sql.gz | mysql -u root -p"${MYSQL_ROOT_PASSWORD}"
MongoDB:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/mongodb"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/mongo-backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
mkdir -p "$BACKUP_DIR"
log "Dumping all MongoDB databases"
mongodump \
--host "${MONGO_HOST:-localhost}" \
--port "${MONGO_PORT:-27017}" \
--username "${MONGO_USER:?}" \
--password "${MONGO_PASSWORD:?}" \
--authenticationDatabase admin \
--out "${BACKUP_DIR}/dump_${DATE}"
log "Compressing dump"
tar czf "${BACKUP_DIR}/mongo_${DATE}.tar.gz" -C "${BACKUP_DIR}" "dump_${DATE}"
rm -rf "${BACKUP_DIR}/dump_${DATE}"
# Retain last 14 dumps
find "$BACKUP_DIR" -name "mongo_*.tar.gz" -mtime +14 -delete
log "MongoDB backup complete: mongo_${DATE}.tar.gz"
# Restore MongoDB
tar xzf mongo_YYYYMMDD.tar.gz
mongorestore \
--host "${MONGO_HOST:-localhost}" \
--username "${MONGO_USER}" \
--password "${MONGO_PASSWORD}" \
--authenticationDatabase admin \
--drop \
dump_YYYYMMDD/
#!/usr/bin/env bash
# Back up all named Docker volumes
set -euo pipefail
BACKUP_DIR="/var/backups/docker-volumes"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/docker-volume-backup.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
mkdir -p "$BACKUP_DIR"
# Get all named volumes
VOLUMES=$(docker volume ls --format '{{.Name}}')
for VOL in $VOLUMES; do
log "Backing up volume: $VOL"
docker run --rm \
-v "${VOL}:/data:ro" \
-v "${BACKUP_DIR}:/backup" \
alpine \
tar czf "/backup/${VOL}_${DATE}.tar.gz" -C /data . \
&& log "✓ $VOL backed up" \
|| log "✗ $VOL FAILED"
done
# Retain last 7 days
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +7 -delete
log "Docker volume backup complete"
Restore a Docker volume:
# Stop the container first
docker compose down
# Restore
docker run --rm \
-v "${VOLUME_NAME}:/data" \
-v "$(pwd):/backup:ro" \
alpine \
sh -c "cd /data && tar xzf /backup/${VOLUME_NAME}_YYYYMMDD.tar.gz"
# Restart
docker compose up -d
Always include a test-restore script alongside every backup setup:
#!/usr/bin/env bash
# test-restore.sh — Run monthly to verify backups are actually restorable
set -euo pipefail
RESTORE_DIR="/tmp/backup-test-$(date +%Y%m%d)"
LOG_FILE="/var/log/backup-test.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
log "=== BACKUP RESTORE TEST ==="
mkdir -p "$RESTORE_DIR"
# Restic test restore
log "Testing restic restore..."
restic restore latest --target "$RESTORE_DIR" --path /etc/passwd
if [[ -f "${RESTORE_DIR}/etc/passwd" ]]; then
log "✓ Restic restore: PASSED"
else
log "✗ Restic restore: FAILED — /etc/passwd not found in restore"
exit 1
fi
# Verify a critical file exists and is readable
LINES=$(wc -l < "${RESTORE_DIR}/etc/passwd")
if [[ "$LINES" -gt 5 ]]; then
log "✓ File content check: PASSED ($LINES lines in passwd)"
else
log "✗ File content check: FAILED (only $LINES lines)"
exit 1
fi
rm -rf "$RESTORE_DIR"
log "=== TEST COMPLETE: All checks passed ==="
Always include guidance on storing encryption credentials safely. A lost password = permanently unrecoverable backup.
| Tool | Key variable | Storage guidance |
|---|---|---|
| restic | RESTIC_PASSWORD | Store in /root/.restic-env (chmod 400) or a password manager; print and store offline |
| BorgBackup | BORG_PASSPHRASE | Same — store in /root/.borg-env (chmod 400); write the passphrase down and store in a safe |
/root/.restic-env pattern (source this before running backup commands):
export RESTIC_REPOSITORY="/mnt/backups/myserver"
export RESTIC_PASSWORD="your-strong-passphrase-here"
# For S3:
# export AWS_ACCESS_KEY_ID="..."
# export AWS_SECRET_ACCESS_KEY="..."
chmod 400 /root/.restic-env
# source it: . /root/.restic-env
Warn the user explicitly: If you lose RESTIC_PASSWORD or BORG_PASSPHRASE, all backups are permanently inaccessible — even if the files exist. Test the password in a restore drill before relying on it.
Always explain and enforce this when generating a backup strategy:
| Rule | Meaning | Implementation |
|---|---|---|
| 3 copies of data | Original + 2 backups | Local backup + remote backup |
| 2 different storage media | Not just 2 copies on same disk | Local disk + cloud or remote SSH |
| 1 off-site | At least one backup physically separate | S3 / Backblaze B2 / remote VPS |
Write all scripts to ./backup/ in the current working directory:
backup/
backup.sh ← main backup script
test-restore.sh ← restore test script (auto-generated — run monthly to verify backups work)
.env.example ← required env vars (repo path, password, etc.)
README.md ← what each script does, restore steps
Then print ONLY:
✅ Backup solution created in ./backup/
▶ First-time setup:
cp .env.example .env && nano .env
source .env
# Initialize the backup repository (run once)
[init command for chosen tool]
▶ Run first backup:
chmod +x backup/backup.sh
sudo ./backup/backup.sh
▶ Automate (choose one):
# Cron — daily at 2 AM
echo "0 2 * * * root /path/to/backup/backup.sh" >> /etc/cron.d/backup
# systemd timer — see /linux-cron-manager for a full timer unit
▶ Test your backup (run monthly!):
chmod +x backup/test-restore.sh
./backup/test-restore.sh
💡 Follow the 3-2-1 rule: local + remote/cloud copy + test regularly.
⚠️ Store your encryption password in a password manager AND write it down offline.
If lost, ALL backups are permanently unrecoverable — even if the files exist.
Run the test-restore.sh NOW before trusting these backups with real data.
💡 Next: /linux-cron-manager to create a systemd timer for automated backups.
💡 Next: /linux-monitoring-setup to alert when backups fail or disk fills up.
npx claudepluginhub chfle/lehnert-claude-skills --plugin linux-vuln-scannerImplements backup strategies for databases, filesystems, and cloud resources using tar, rsync, pg_dump, AWS S3. Automates scheduling, retention, encryption, verification, and disaster recovery.
Generates backup scripts for PostgreSQL, MySQL, MongoDB, and SQLite with scheduling, compression, encryption, retention policies, and restore procedures.
Generates Odoo backup scripts (PostgreSQL dump + filestore archive), cron automation, S3 upload, and step-by-step restore procedures for production recovery.