• Home
  • Guides
    • All
    • Linux
    • Programming
    • Tools
    • WordPress
    My Backup Setup for Linux PCs

    My Backup Setup for Linux PCs

    Detecting Hidden WordPress Malware Disguised as Images

    Detecting Hidden WordPress Malware Disguised as Images

    Server-Side Image Conversion with Apache

    Server-Side Image Conversion with Apache

    Fastest Way to Extract a Massive .tar.gz File on Linux

    Fastest Way to Extract a Massive .tar.gz File on Linux

    Monitor SSL Expiration with Python

    Monitor SSL Expiration with Python

    Building a Simple WordPress Post List Tool with PHP

    Building a Simple WordPress Post List Tool with PHP

    Monitoring Web Page Changes with Python

    Monitoring Web Page Changes with Python

    My SSH Setup: How I Manage Multiple Servers

    My SSH Setup: How I Manage Multiple Servers

    Building a Network Tracker Auditor for Privacy with Python

    Building a Network Tracker Auditor for Privacy with Python

  • Blog
    • All
    • Artificial Intelligence
    • Developer Life
    • Privacy
    • Reviews
    • Security
    • Tutorials
    Imposter Syndrome as a Self-Taught Developer

    Imposter Syndrome as a Self-Taught Developer

    Why Stable Websites Outperform Flashy Redesigns

    Why Stable Websites Outperform Flashy Redesigns

    AdGuard Ad Blocker Review

    AdGuard Ad Blocker Review

    Surfshark VPN Review

    Surfshark VPN Review

    Nmap Unleash the Power of Cybersecurity Scanning

    Nmap: Unleash the Power of Cybersecurity Scanning

    Floorp Browser Review

    Floorp Browser Review

    Understanding Man-in-the-Middle Attacks

    Understanding Man-in-the-Middle Attacks

    Privacy-Focused Analytics

    Privacy-Focused Analytics: Balancing Insights and Integrity

    Safeguarding Your Facebook Account

    Safeguarding Your Facebook Account: Understanding the Differences Between Hacking and Cloning

  • Apps
    • Bible App
    • Bible Verse Screensaver
    • Blue AI Chatbot
    • Early Spring Predictor
    • FIGlet Generator
    • Password Generator
    • StegX
    • The Matrix
    • WeatherX
    • Website Risk Level Tool
  • About
    • About JMooreWV
    • Live Cyber Attack Stats
  • Contact
    • General Contact
    • Website Administration & Cybersecurity
No Result
View All Result
  • Home
  • Guides
    • All
    • Linux
    • Programming
    • Tools
    • WordPress
    My Backup Setup for Linux PCs

    My Backup Setup for Linux PCs

    Detecting Hidden WordPress Malware Disguised as Images

    Detecting Hidden WordPress Malware Disguised as Images

    Server-Side Image Conversion with Apache

    Server-Side Image Conversion with Apache

    Fastest Way to Extract a Massive .tar.gz File on Linux

    Fastest Way to Extract a Massive .tar.gz File on Linux

    Monitor SSL Expiration with Python

    Monitor SSL Expiration with Python

    Building a Simple WordPress Post List Tool with PHP

    Building a Simple WordPress Post List Tool with PHP

    Monitoring Web Page Changes with Python

    Monitoring Web Page Changes with Python

    My SSH Setup: How I Manage Multiple Servers

    My SSH Setup: How I Manage Multiple Servers

    Building a Network Tracker Auditor for Privacy with Python

    Building a Network Tracker Auditor for Privacy with Python

  • Blog
    • All
    • Artificial Intelligence
    • Developer Life
    • Privacy
    • Reviews
    • Security
    • Tutorials
    Imposter Syndrome as a Self-Taught Developer

    Imposter Syndrome as a Self-Taught Developer

    Why Stable Websites Outperform Flashy Redesigns

    Why Stable Websites Outperform Flashy Redesigns

    AdGuard Ad Blocker Review

    AdGuard Ad Blocker Review

    Surfshark VPN Review

    Surfshark VPN Review

    Nmap Unleash the Power of Cybersecurity Scanning

    Nmap: Unleash the Power of Cybersecurity Scanning

    Floorp Browser Review

    Floorp Browser Review

    Understanding Man-in-the-Middle Attacks

    Understanding Man-in-the-Middle Attacks

    Privacy-Focused Analytics

    Privacy-Focused Analytics: Balancing Insights and Integrity

    Safeguarding Your Facebook Account

    Safeguarding Your Facebook Account: Understanding the Differences Between Hacking and Cloning

  • Apps
    • Bible App
    • Bible Verse Screensaver
    • Blue AI Chatbot
    • Early Spring Predictor
    • FIGlet Generator
    • Password Generator
    • StegX
    • The Matrix
    • WeatherX
    • Website Risk Level Tool
  • About
    • About JMooreWV
    • Live Cyber Attack Stats
  • Contact
    • General Contact
    • Website Administration & Cybersecurity
No Result
View All Result
Home Guides Programming Bash

My Backup Setup for Linux PCs

Jonathan Moore by Jonathan Moore
45 minutes ago
Reading Time: 19 mins read
A A
My Backup Setup for Linux PCs
FacebookTwitter

March 31st is World Backup Day. It is meant to be a reminder, but I do not rely on reminders to protect data. I rely on a system that runs on its own and proves it worked.

If your hard drive died right now, how long would it take you to be back up and running? That question matters more than whether a backup job exists. A backup that runs is not the same as a backup you can restore quickly. I built this setup around that idea because I have seen too many systems fail when they were needed most.

This is the backup system I use on Linux PCs. It works the same way on servers, but I keep it focused on full system protection instead of website specific backups. Everything here is script driven, predictable, and easy to verify.

What This Setup Is Designed To Do

I wanted something that does more than copy files. It needed to protect the entire system in a way that makes recovery straightforward. That means preserving permissions, handling system paths correctly, and avoiding unnecessary data.

This setup creates full backups and snapshot backups. A full backup captures everything at a point in time. Snapshots reuse unchanged files using hard links, which keeps storage usage under control. You get multiple restore points without duplicating the same data over and over.

It also includes verification and rotation. Backups are checked after they run, and older ones are removed automatically. I do not want to manage cleanup manually or wonder whether the last run actually worked.

How The Backup System Is Structured

I keep this setup simple on purpose. When something breaks, I do not want to think through a complicated system just to figure out where things are or what ran last. Everything is organized in a way that makes it easy to find the latest backup, step back through history, and restore without guessing.

At the center of it is a single backup root directory. Inside that, everything is grouped by hostname so I can use the same structure across multiple machines if I want. Under that, there are separate directories for full backups, snapshots, logs, and verification output. This keeps things organized without spreading data across unrelated paths.

The full backups and snapshots are separated for a reason. Full backups act as a stable baseline, while snapshots build on top of that and provide additional restore points. By keeping them in different directories, I can manage retention and cleanup more predictably. It also makes it obvious which backups are complete standalone copies and which ones are part of the snapshot chain.

The structure ends up looking something like this:

/backups/
└── hostname/
    ├── full/
    │   ├── 2026-03-31_01-30-00/
    │   │   └── filesystem/
    ├── snapshots/
    │   ├── 2026-04-01_01-00-00/
    │   │   └── filesystem/
    │   ├── 2026-04-02_01-00-00/
    │   │   └── filesystem/
    ├── logs/
    ├── verify/
    ├── latest/
    │   └── filesystem
    └── current-full
        └── filesystem

The latest symlink is what I rely on most during normal operation. It always points to the most recent backup, whether that is a full backup or a snapshot. That makes it easy to reference the last run without having to sort directories or remember timestamps. The script also uses this link when building new snapshots.

The current-full symlink serves a different purpose. It always points to the most recent full backup, which gives the system a stable reference point. If I ever need to rebuild from a full backup instead of a snapshot, I know exactly where to look without searching through directories.

Logs and verification data are kept alongside the backups instead of being scattered across the system. That makes it easier to audit what happened during each run. If something fails or looks off, I can go straight to the backup directory and see both the data and the logs that created it.

This structure also makes cleanup straightforward. Snapshots can be rotated independently without touching full backups, and full backups can be limited to a small number without affecting recent restore points. Everything has a clear place and a clear purpose, which keeps the system predictable over time.

The goal here is not to build something complex. It is to build something that I can understand quickly when I need it. When a system is down, I do not want to explore directories or trace logic. I want to know exactly where the backups are, which one I need, and how to restore it without hesitation.

The Main Backup Script

This is the core of everything. I do not split this across multiple scripts or try to make it overly modular. I want one script that I can run manually or from cron and know exactly what it is going to do every time. When something fails, I also want one place to look instead of chasing logic across multiple files.

Before running anything, I always look at the top of the script and adjust the variables. This is where the behavior is controlled. Things like the backup location, retention limits, exclusions file, and remote sync settings are all defined here. That makes it easy to adapt this to a Linux PC, a laptop, or even a small server without rewriting logic.

The script also assumes that /backups is a real storage location and not part of the system disk. That is intentional. Backing up to the same drive you are trying to protect does not help much when the hardware fails. Even on a Linux PC, I prefer pointing this to a secondary drive or mounted storage.

#!/bin/bash

set -Eeuo pipefail

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
HOSTNAME_SHORT="$( hostname -s )"

BACKUP_ROOT="/backups"
BACKUP_NAME="$HOSTNAME_SHORT"
SOURCE_PATH="/"
EXCLUDES_FILE="$SCRIPT_DIR/backup-excludes.txt"

BACKUP_MODE="${1:-auto}"                     # auto | full | snapshot
MIN_FREE_GB=20
SNAPSHOT_RETENTION=7
FULL_RETENTION=1

ENABLE_REMOTE_SYNC=false
REMOTE_USER="backupuser"
REMOTE_HOST="backup.example.com"
REMOTE_PATH="/srv/remote-backups"

VERIFY_CRITICAL_PATHS=(
    "/etc"
    "/home"
    "/root"
)

VERIFY_HASH_FILES=(
    "/etc/fstab"
    "/etc/passwd"
    "/etc/group"
    "/etc/hosts"
)

BACKUP_BASE="$BACKUP_ROOT/$BACKUP_NAME"
FULL_DIR="$BACKUP_BASE/full"
SNAPSHOT_DIR="$BACKUP_BASE/snapshots"
LOG_DIR="$BACKUP_BASE/logs"
VERIFY_DIR="$BACKUP_BASE/verify"
LOCK_FILE="$BACKUP_BASE/backup.lock"
LATEST_LINK="$BACKUP_BASE/latest"
CURRENT_FULL_LINK="$BACKUP_BASE/current-full"

TIMESTAMP="$( date '+%Y-%m-%d_%H-%M-%S' )"
LOG_FILE="$LOG_DIR/backup-$TIMESTAMP.log"

mkdir -p "$FULL_DIR" "$SNAPSHOT_DIR" "$LOG_DIR" "$VERIFY_DIR"

exec 9>"$LOCK_FILE"
if ! flock -n 9; then
    echo "Another backup job is already running."
    exit 1
fi

log() {
    local message="$1"
    echo "[$( date '+%Y-%m-%d %H:%M:%S' )] $message" | tee -a "$LOG_FILE"
}

die() {
    local message="$1"
    log "ERROR: $message"
    exit 1
}

cleanup_incomplete_backup() {
    if [[ -n "${TARGET_BACKUP_DIR:-}" && -d "${TARGET_BACKUP_DIR:-}" ]]; then
        log "Cleaning up incomplete backup directory: $TARGET_BACKUP_DIR"
        rm -rf "$TARGET_BACKUP_DIR"
    fi
}

on_error() {
    local exit_code=$?
    log "Backup failed with exit code $exit_code"
    cleanup_incomplete_backup
    exit "$exit_code"
}

trap on_error ERR

require_root() {
    if [[ "$EUID" -ne 0 ]]; then
        die "This script must be run as root."
    fi
}

check_dependencies() {
    local deps=( rsync df du find sha256sum flock tee readlink awk sort head rm mkdir hostname date )
    local dep
    for dep in "${deps[@]}"; do
        command -v "$dep" >/dev/null 2>&1 || die "Required command not found: $dep"
    done
}

check_excludes_file() {
    [[ -f "$EXCLUDES_FILE" ]] || die "Exclusions file not found: $EXCLUDES_FILE"
}

check_disk_space() {
    local available_kb required_kb
    available_kb="$( df -Pk "$BACKUP_ROOT" | awk 'NR==2 { print $4 }' )"
    required_kb="$(( MIN_FREE_GB * 1024 * 1024 ))"

    log "Available free space: $(( available_kb / 1024 / 1024 )) GB"
    log "Minimum required free space: $MIN_FREE_GB GB"

    if (( available_kb < required_kb )); then
        die "Not enough free disk space on $BACKUP_ROOT"
    fi
}

determine_mode() {
    if [[ ! -L "$CURRENT_FULL_LINK" || ! -d "$( readlink -f "$CURRENT_FULL_LINK" )" ]]; then
        echo "full"
        return
    fi

    case "$BACKUP_MODE" in
        full)
            echo "full"
            ;;
        snapshot)
            echo "snapshot"
            ;;
        auto)
            echo "snapshot"
            ;;
        *)
            die "Invalid backup mode: $BACKUP_MODE"
            ;;
    esac
}

create_backup_dirs() {
    local mode="$1"

    if [[ "$mode" == "full" ]]; then
        TARGET_BACKUP_DIR="$FULL_DIR/$TIMESTAMP"
    else
        TARGET_BACKUP_DIR="$SNAPSHOT_DIR/$TIMESTAMP"
    fi

    TARGET_FILESYSTEM_DIR="$TARGET_BACKUP_DIR/filesystem"
    mkdir -p "$TARGET_FILESYSTEM_DIR"
}

run_rsync_backup() {
    local mode="$1"
    local previous_backup=""
    local rsync_args=()

    if [[ "$mode" == "snapshot" ]]; then
        if [[ ! -L "$LATEST_LINK" || ! -d "$( readlink -f "$LATEST_LINK" )" ]]; then
            die "Cannot create snapshot because no previous backup exists."
        fi

        previous_backup="$( readlink -f "$LATEST_LINK" )"
        rsync_args+=( "--link-dest=$previous_backup/filesystem" )
        log "Using link-dest from previous backup: $previous_backup"
    fi

    log "Starting $mode backup to $TARGET_BACKUP_DIR"

    rsync -aAXH \
        --numeric-ids \
        --delete \
        --delete-excluded \
        --exclude-from="$EXCLUDES_FILE" \
        "${rsync_args[@]}" \
        "$SOURCE_PATH" "$TARGET_FILESYSTEM_DIR/"

    log "rsync completed successfully"
}

write_metadata() {
    local mode="$1"
    local metadata_file="$TARGET_BACKUP_DIR/backup-meta.txt"

    cat > "$metadata_file" <<EOF
timestamp=$TIMESTAMP
hostname=$HOSTNAME_SHORT
mode=$mode
source=$SOURCE_PATH
backup_dir=$TARGET_BACKUP_DIR
created_at=$( date '+%Y-%m-%d %H:%M:%S' )
EOF

    log "Metadata written to $metadata_file"
}

verify_critical_paths() {
    local missing=0
    local path
    local relative_path

    for path in "${VERIFY_CRITICAL_PATHS[@]}"; do
        if [[ -e "$path" ]]; then
            relative_path="${path#/}"
            if [[ ! -e "$TARGET_FILESYSTEM_DIR/$relative_path" ]]; then
                log "Verification failed. Missing path in backup: $path"
                missing=1
            else
                log "Verified path exists in backup: $path"
            fi
        fi
    done

    (( missing == 0 )) || die "Critical path verification failed"
}

verify_hash_files() {
    local verify_file="$VERIFY_DIR/verify-$TIMESTAMP.txt"
    : > "$verify_file"

    local file
    local source_hash
    local backup_hash
    local relative_path

    for file in "${VERIFY_HASH_FILES[@]}"; do
        if [[ -f "$file" ]]; then
            relative_path="${file#/}"

            if [[ ! -f "$TARGET_FILESYSTEM_DIR/$relative_path" ]]; then
                die "Verification failed. Expected file missing in backup: $file"
            fi

            source_hash="$( sha256sum "$file" | awk '{ print $1 }' )"
            backup_hash="$( sha256sum "$TARGET_FILESYSTEM_DIR/$relative_path" | awk '{ print $1 }' )"

            echo "$file | source=$source_hash | backup=$backup_hash" >> "$verify_file"

            if [[ "$source_hash" != "$backup_hash" ]]; then
                die "Verification failed. Hash mismatch for $file"
            fi

            log "Hash verified: $file"
        fi
    done

    log "Hash verification results written to $verify_file"
}

verify_backup_size() {
    local backup_size_kb
    backup_size_kb="$( du -sk "$TARGET_FILESYSTEM_DIR" | awk '{ print $1 }' )"

    log "Backup size: $(( backup_size_kb / 1024 / 1024 )) GB"

    if (( backup_size_kb < 1024 )); then
        die "Backup size looks too small. Refusing to treat this as successful."
    fi
}

update_symlinks() {
    local mode="$1"

    ln -sfn "$TARGET_BACKUP_DIR" "$LATEST_LINK"

    if [[ "$mode" == "full" ]]; then
        ln -sfn "$TARGET_BACKUP_DIR" "$CURRENT_FULL_LINK"
    fi

    log "Updated latest symlink: $LATEST_LINK -> $TARGET_BACKUP_DIR"

    if [[ "$mode" == "full" ]]; then
        log "Updated current full symlink: $CURRENT_FULL_LINK -> $TARGET_BACKUP_DIR"
    fi
}

rotate_snapshots() {
    log "Rotating snapshots. Keeping last $SNAPSHOT_RETENTION"

    mapfile -t old_snapshots < <( find "$SNAPSHOT_DIR" -mindepth 1 -maxdepth 1 -type d | sort | head -n -"$SNAPSHOT_RETENTION" || true )

    local dir
    for dir in "${old_snapshots[@]}"; do
        [[ -n "$dir" ]] || continue
        log "Removing old snapshot: $dir"
        rm -rf "$dir"
    done
}

rotate_full_backups() {
    log "Rotating full backups. Keeping last $FULL_RETENTION"

    mapfile -t old_fulls < <( find "$FULL_DIR" -mindepth 1 -maxdepth 1 -type d | sort | head -n -"$FULL_RETENTION" || true )

    local dir
    for dir in "${old_fulls[@]}"; do
        [[ -n "$dir" ]] || continue
        log "Removing old full backup: $dir"
        rm -rf "$dir"
    done
}

run_remote_sync() {
    if [[ "$ENABLE_REMOTE_SYNC" != true ]]; then
        log "Remote sync disabled"
        return
    fi

    log "Starting remote sync to $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/$BACKUP_NAME/"

    rsync -aAXH --delete \
        "$BACKUP_BASE/" \
        "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/$BACKUP_NAME/"

    log "Remote sync completed successfully"
}

main() {
    require_root
    check_dependencies
    check_excludes_file
    check_disk_space

    local mode
    mode="$( determine_mode )"

    log "Backup job started"
    log "Selected backup mode: $mode"

    create_backup_dirs "$mode"
    run_rsync_backup "$mode"
    write_metadata "$mode"
    verify_critical_paths
    verify_hash_files
    verify_backup_size
    update_symlinks "$mode"
    rotate_snapshots
    rotate_full_backups
    run_remote_sync

    log "Backup completed successfully"
}

main "$@"

The first thing I pay attention to in this script is the safety layer. It uses set -Eeuo pipefail, a lock file, and an error trap to make sure failures do not go unnoticed. If something breaks halfway through, it cleans up the incomplete backup instead of leaving a directory that looks valid but is missing data.

The next important part is how it decides between a full backup and a snapshot. The script checks for an existing full backup using a symlink. If one does not exist, it creates a full backup automatically. After that, it switches to snapshot mode unless I explicitly force a full backup. That keeps things efficient without needing extra logic in cron.

The rsync command is where the real work happens. I use -aAXH along with --numeric-ids so permissions, ownership, ACLs, and hard links are preserved correctly. The --link-dest option is what allows snapshots to reuse unchanged files, which is how I keep multiple restore points without wasting disk space.

I also rely on those symlinks when the script runs. They give it a stable reference for both the most recent backup and the current full backup without extra lookup logic.

Rotation is handled automatically at the end. The script keeps the last 7 snapshots and removes older ones. That keeps storage under control without requiring manual cleanup. If I want to keep more history, I only need to change one number at the top of the script.

The Exclusions File

The exclusions file is what keeps this backup from turning into a mess. When I back up a Linux PC, I do not want to blindly copy every path under / just because it exists. Some parts of the filesystem are temporary, some are recreated at boot, and some would only add noise or create problems during a restore. This file is what tells rsync to skip those paths and stay focused on data that actually matters.

I treat this file as part of the backup logic, not as an afterthought. A lot of backup problems come from copying things that were never meant to be restored as regular files. That includes virtual filesystem paths, runtime data, swap files, caches, and the backup destination itself. Leaving those in can waste space, slow the job down, and make the restore result less reliable.

What gets excluded depends on the kind of Linux system I am protecting. A desktop PC, a laptop, and a server may all need slightly different rules. Even so, the core exclusions stay mostly the same because Linux always has the same categories of paths that should be skipped. I would rather start with a safe base list and adjust it than learn the hard way that I copied something useless or harmful.

/dev/*
/proc/*
/sys/*
/run/*
/tmp/*
/mnt/*
/media/*
/lost+found
/swapfile
/var/tmp/*
/var/cache/*
/var/run/*
/var/lock/*
/home/*/.cache/*
/home/*/.local/share/Trash/*
/root/.cache/*
/backups/*

The first group of exclusions covers system generated and virtual filesystem paths. Directories like /dev, /proc, /sys, and /run are not normal stored data in the same way as /etc or /home. They are created and managed by the kernel or the running system, so copying them into a backup does not help me recover a machine properly. In some cases, restoring them like ordinary files can create confusion or lead to a broken system state.

The next group covers temporary storage and disposable data. Paths like /tmp, /var/tmp, caches, and trash folders are full of files that come and go all the time. I do not want those inflating the size of my backup or causing me to waste time restoring things the system or applications will rebuild on their own. On a Linux PC, browser caches and desktop trash folders can grow larger than people expect, so skipping them can make a real difference.

I also exclude mounted media paths like /mnt and /media because I do not want the script wandering into external drives, mounted shares, or removable media by accident. If I attach a USB drive or mount a network share, that does not mean I want it folded into the system backup automatically. The same logic applies to /backups/*, because backing up the backup destination into itself is an easy way to create a recursive disaster.

This file is also one of the easiest places to customize the setup for a specific machine. If I have a directory full of VM images, large downloads, Steam libraries, or other data I do not need in my system backup, I can add those paths here. That lets me keep the backup focused on the operating system, user data, and configuration that would actually matter if the internal drive failed.

I would not treat this file as something to set once and never review again. As a Linux PC changes over time, the exclusions may need to change with it. New applications, larger caches, or different storage habits can all affect what belongs in the backup. I like keeping the file simple and readable so I can look at it later and still understand exactly why each path is there.

How Snapshot Backups Work

Snapshot backups are what make this entire setup practical to run every day. A full backup of a Linux system can take time and disk space, especially once user data starts growing. If I copied everything from scratch every night, I would either run out of storage or stop running backups as often as I should. That is where snapshots change things.

This setup uses rsync with the --link-dest option. Instead of copying every file again, it compares the current system to the previous backup. If a file has not changed, it creates a hard link to the existing copy instead of writing a new one. If a file has changed, it copies only that file into the new snapshot. That means each snapshot looks like a full backup, but it only consumes space for what actually changed.

From the outside, every snapshot directory looks complete. I can browse it, search it, or restore from it exactly the same way I would with a full backup. There is no special format, no archive extraction, and no extra tooling required. That is one of the reasons I prefer this approach over compressed backup files or proprietary formats. When something breaks, I want to work with a normal filesystem, not decode a backup format under pressure.

The hard link behavior is what makes this efficient. Multiple snapshots can reference the same physical file on disk as long as that file has not changed. Once a file changes, the new version is written separately, and future snapshots will reference that version instead. This gives me a clean history of changes without duplicating unchanged data across every backup.

One thing I pay attention to is how deletes are handled. The script uses --delete, which means if a file is removed from the system, it will not appear in new snapshots. That is what I want for an accurate point in time view. At the same time, older snapshots still contain that file, which gives me a way to recover something that was deleted days ago without digging through logs or guessing when it disappeared.

This is also where rotation becomes important. Keeping snapshots forever would eventually consume all available space, even with hard linking. By limiting the system to the last 7 snapshots, I keep a rolling window of restore points without letting storage usage grow out of control. If I need a longer history, I can increase that number, but I prefer keeping it tight and predictable.

The most important part is how this affects recovery. When I restore from a snapshot, I am not restoring a diff or applying changes. I am copying a usable filesystem back into place. I only need to know which point in time I want to go back to.

I can take any snapshot, restore it to another directory or drive, and verify that everything looks correct. That gives me confidence that the backup system is actually working, not just running. Testing a restore from a snapshot is no different than testing a restore from a full backup, which is exactly how it should be.

Verifying That The Backup Is Actually Usable

I do not consider a backup successful just because the script finished running. I have seen too many cases where a backup completed, logs looked fine, and the data was still incomplete or unusable. That is why verification is built into this setup instead of being something I might check later.

The first thing I verify is that the structure of the backup looks correct. There are a few directories that should always exist, like /etc, /home, and /root. If any of those are missing from the backup, something went wrong and I want the script to fail immediately. It is better to catch that the moment it happens than to assume everything is fine and find out during a restore.

The script also checks that the backup size is reasonable. A backup that suddenly drops to a very small size is usually a sign that something failed silently. That could be a permission issue, a mount problem, or even an empty source path. I would rather have the script stop and treat that as a failure than accept a backup that clearly does not contain everything it should.

For critical files, I go a step further and verify hashes. Files like /etc/passwd, /etc/group, and /etc/fstab are small but important. The script calculates a hash of the original file and compares it to the hash of the version in the backup. If they do not match, something is wrong, and the backup is not trusted. This gives me a quick way to confirm that key parts of the system were copied correctly.

All of this verification happens automatically as part of the backup run. I am not relying on memory or manual checks after the fact. If something fails, the script exits with an error and logs the reason. That makes it obvious that something needs attention instead of quietly continuing as if nothing happened.

I also like that this approach produces verification output I can review later. The hash checks are written to a file, and the logs show what passed and what failed. If I ever need to audit what happened during a backup, I have enough information to understand it without guessing.

It is not enough to copy files from one place to another. I want proof that the result is usable. Without verification, a backup is just a hope that everything worked. With it, I have something I can trust when I need it.

Restoring A Full Backup

A backup system only proves itself when something breaks and I need to bring a machine back quickly. I want a process I already understand and trust.

This script is designed to restore a full backup back to a target location. That target might be a newly installed system, a mounted replacement drive, or even a temporary recovery directory. The key idea is that I am copying a known good filesystem back into place without guessing or improvising.

Before I run anything, I always make sure I am restoring to the correct location. If I am working on a fresh drive, I mount it somewhere like /mnt/recovery and double check that it is empty or contains only what I expect. The script uses --delete, which means anything in the target that does not exist in the backup will be removed. That is exactly what I want during a full restore, but it also means I need to be certain about the target path.

#!/bin/bash

set -Eeuo pipefail

if [[ "$EUID" -ne 0 ]]; then
    echo "This script must be run as root."
    exit 1
fi

if [[ $# -lt 2 || $# -gt 3 ]]; then
    echo "Usage: $0 /path/to/full/backup /restore/target [--yes-restore]"
    exit 1
fi

BACKUP_DIR="$1"
RESTORE_TARGET="$2"
CONFIRM="${3:-}"

if [[ "$CONFIRM" != "--yes-restore" ]]; then
    echo "Refusing to continue without --yes-restore"
    exit 1
fi

if [[ ! -d "$BACKUP_DIR/filesystem" ]]; then
    echo "Backup filesystem directory not found: $BACKUP_DIR/filesystem"
    exit 1
fi

if [[ ! -d "$RESTORE_TARGET" ]]; then
    echo "Restore target does not exist: $RESTORE_TARGET"
    exit 1
fi

echo "About to restore FULL backup"
echo "Source: $BACKUP_DIR/filesystem/"
echo "Target: $RESTORE_TARGET/"
echo

rsync -aAXH \
    --numeric-ids \
    --delete \
    "$BACKUP_DIR/filesystem/" "$RESTORE_TARGET/"

echo
echo "Full backup restore completed successfully."

The confirmation flag is there on purpose. When I am restoring a system, I do not want the script to assume anything. Requiring --yes-restore forces me to stop and confirm what I am about to overwrite. It is a small safeguard, but it prevents the kind of mistake that is hard to undo.

When I actually run the restore, I keep it simple. I point the script at the backup directory and the mounted target, then let rsync handle the rest. Because the backup was created with permissions, ownership, and links preserved, the restore puts everything back exactly the way it was. There is no extra step to reconstruct the filesystem or interpret the data.

One thing this script does not try to handle is the bootloader. That part depends on the system, whether it uses BIOS or UEFI, and how the disk is partitioned. After restoring the filesystem, I handle bootloader installation separately using the tools appropriate for that system. Keeping that out of the script avoids making assumptions that could break the recovery process.

I also test this process outside of an emergency. I will restore a backup to a spare directory or another drive just to confirm that everything behaves the way I expect. That way, when something actually fails, I am not learning the process under pressure. I already know how long it takes and what the result should look like.

Because the backups are stored as a normal filesystem and not compressed archives, the restore is straightforward. I am not extracting, converting, or rebuilding anything. I am just copying a known good state back into place, which is exactly what I want when time matters.

Restoring From A Snapshot

Most failures are not complete system losses. They are accidental deletes, bad updates, or configuration changes that need to be rolled back. In those cases, I do not need to go all the way back to the last full backup. I just need a clean state from a specific point in time.

That is where snapshots become useful. Each snapshot represents the system exactly as it looked when that backup ran. Even though snapshots are built using hard links to save space, they behave like a complete filesystem when I restore from them. I do not need to think about how they were created. I only need to choose the point in time I want.

Before running the restore, I make sure the target location is correct and that I am not about to overwrite something I care about. Since the script uses --delete, anything in the target that is not in the snapshot will be removed.

#!/bin/bash

set -Eeuo pipefail

if [[ "$EUID" -ne 0 ]]; then
    echo "This script must be run as root."
    exit 1
fi

if [[ $# -lt 2 || $# -gt 3 ]]; then
    echo "Usage: $0 /path/to/snapshot /restore/target [--yes-restore]"
    exit 1
fi

SNAPSHOT_DIR="$1"
RESTORE_TARGET="$2"
CONFIRM="${3:-}"

if [[ "$CONFIRM" != "--yes-restore" ]]; then
    echo "Refusing to continue without --yes-restore"
    exit 1
fi

if [[ ! -d "$SNAPSHOT_DIR/filesystem" ]]; then
    echo "Snapshot filesystem directory not found: $SNAPSHOT_DIR/filesystem"
    exit 1
fi

if [[ ! -d "$RESTORE_TARGET" ]]; then
    echo "Restore target does not exist: $RESTORE_TARGET"
    exit 1
fi

echo "About to restore SNAPSHOT backup"
echo "Source: $SNAPSHOT_DIR/filesystem/"
echo "Target: $RESTORE_TARGET/"
echo

rsync -aAXH \
    --numeric-ids \
    --delete \
    "$SNAPSHOT_DIR/filesystem/" "$RESTORE_TARGET/"

echo
echo "Snapshot restore completed successfully."

The process itself is no different from restoring a full backup. I point the script at the snapshot I want and the location I want to restore to, then let rsync do the work. There is no special handling required, which is one of the biggest advantages of this setup.

Where snapshots really help is with smaller recoveries. If I accidentally delete a directory or overwrite a configuration file, I can restore just that part instead of the entire system. I can mount or browse the snapshot, find the exact file or directory I need, and copy it back. That is faster and less disruptive than doing a full restore.

They also make it easier to recover from mistakes that are not immediately obvious. If I notice a problem a few days later, I can step back through recent snapshots and find the last known good version. That gives me a simple way to recover without trying to recreate changes manually.

I also test this process outside of emergencies. I restore snapshots into a separate directory and verify that everything looks correct. That gives me confidence that the snapshots are usable and not just taking up space.

Snapshots give me flexibility without adding complexity. I get multiple recovery points, simple restore behavior, and the ability to recover individual files or entire systems without changing the process.

Automating It With Cron

This is the part that makes the entire setup reliable. I do not want to remember to run backups, and I do not want to rely on good intentions. If it is not automated, it eventually stops happening. Cron solves that by making sure the script runs whether I think about it or not.

I keep the schedule simple. The script runs every night in automatic mode, and once a week I force a full backup. That gives me a steady stream of snapshots while still refreshing the full backup regularly. It is a balance between efficiency and safety that works well for a Linux PC.

0 1 * * * /usr/local/sbin/linux-backup.sh auto
30 1 * * 0 /usr/local/sbin/linux-backup.sh full

The first line runs the backup every night at 1:00 AM. In most cases, that will create a snapshot because a full backup already exists. The second line runs at 1:30 AM on Sundays and forces a full backup. Spacing them apart avoids overlap and keeps the system from trying to run two backup jobs at the same time.

I also think about when the system is actually being used. On a Linux PC, running backups during heavy use can slow things down or interfere with work. Early morning hours are usually quiet, which makes them a good time for disk intensive operations like this. If the machine is not always on at night, I adjust the schedule to match when it is most likely to be running.

The script itself is designed to work cleanly with cron. It uses a lock file to prevent overlapping runs, so even if something gets delayed or triggered twice, it will not start a second backup on top of the first one. That removes one of the more common failure points in automated jobs.

Logs are written automatically for every run, which gives me a way to check what happened without digging through system logs. If I ever need to troubleshoot a problem or confirm that a backup ran, I can look at the log directory and see exactly what happened. That is especially useful if the system is running unattended.

I do not rely on cron blindly either. Every so often I check that backups are still being created and that the timestamps look correct. It only takes a few seconds to confirm that everything is working, and it is worth doing. Automation handles the routine work, but a quick check now and then makes sure nothing has silently broken.

Once this is in place, backups happen every day, snapshots rotate automatically, and full backups are refreshed on schedule. That is what I want from a setup like this. It should work in the background without needing constant attention, but still be easy to verify when I want to check it.

Final Thoughts

World Backup Day is a useful reminder, but reminders are not the same thing as a recovery plan. What matters is having a backup system you can understand, verify, and restore without hesitation.

For me, that is the real goal of this setup. It is not about using the most advanced tools or building the most complicated workflow. It is about making recovery predictable when something eventually goes wrong.

If you take one thing from this, focus on whether you can restore quickly and confidently. That is what makes a backup worth having.

Tags: AutomationBackupLinuxrsyncSecurity
ShareTweetSharePinShareShareScan
ADVERTISEMENT
Jonathan Moore

Jonathan Moore

I am a Software Architect and Senior Software Engineer with 30+ years of experience building applications for Linux and Windows systems. I focus on system architecture, custom web platforms, server infrastructure, and security-focused tools, with an emphasis on performance and reliability. Over the years, I have built everything from WordPress plugins and automation systems to full platforms, ad serving systems, monitoring tools, and API-driven applications. I prefer working close to the system, solving real problems, and building tools that are meant to be used.

Related Articles

Detecting Hidden WordPress Malware Disguised as Images

Detecting Hidden WordPress Malware Disguised as Images

With the recent discovery of malicious files like Stained_Heart_Red-600x500.png being found on servers across the web, it pushed me to...

Server-Side Image Conversion with Apache

Server-Side Image Conversion with Apache

I stopped relying on third party image services a while ago. They work, but they add cost, latency, and another...

Fastest Way to Extract a Massive .tar.gz File on Linux

Fastest Way to Extract a Massive .tar.gz File on Linux

When I am dealing with a 40GB or 50GB website backup, I do not just run tar -xzf file.tar.gz and...

Recommended Services

Latest Articles

My Backup Setup for Linux PCs

My Backup Setup for Linux PCs

March 31st is World Backup Day. It is meant to be a reminder, but I do not rely on reminders...

Read moreDetails

Detecting Hidden WordPress Malware Disguised as Images

Detecting Hidden WordPress Malware Disguised as Images

With the recent discovery of malicious files like Stained_Heart_Red-600x500.png being found on servers across the web, it pushed me to...

Read moreDetails

Server-Side Image Conversion with Apache

Server-Side Image Conversion with Apache

I stopped relying on third party image services a while ago. They work, but they add cost, latency, and another...

Read moreDetails

Imposter Syndrome as a Self-Taught Developer

Imposter Syndrome as a Self-Taught Developer

I started writing code over 35 years ago. Everything I learned came from figuring things out on my own, long...

Read moreDetails
  • Privacy Policy
  • Terms of Service

© 2025 JMooreWV. All rights reserved.

No Result
View All Result
  • Home
  • Guides
    • Linux
    • Programming
      • JavaScript
      • PHP
      • Python
    • Tools
    • WordPress
  • Blog
    • Artificial Intelligence
    • Tutorials
    • Privacy
    • Security
  • Apps
    • Bible App
    • Bible Verse Screensaver
    • Blue AI Chatbot
    • Early Spring Predictor
    • FIGlet Generator
    • Password Generator
    • StegX
    • The Matrix
    • WeatherX
    • Website Risk Level Tool
  • About
    • About JMooreWV
    • Live Cyber Attack Stats
  • Contact
    • General Contact
    • Website Administration & Cybersecurity