How to Create a Bash File Backup Tool

How to Create a Bash File Backup Tool

How to Create a Bash File Backup Tool

I’ve lost data exactly twice in my life, and both times it happened because I “meant to set up backups eventually.” The third time didn’t happen, because I finally sat down and built a proper Bash backup tool that runs on its own schedule without me having to remember anything. In this article, I’ll walk through exactly how I built it — from a bare-bones version to something with rotation, logging, and remote copies built in.

This tool pairs directly with the Bash File Restore Tool covered elsewhere in this series, so I’d recommend reading both.

What a Good Backup Script Actually Needs

Before writing code, I mapped out the requirements:

  1. Compress the target folder into a timestamped archive
  2. Keep a rolling window of backups instead of accumulating forever
  3. Optionally copy the backup somewhere off-machine
  4. Log every run so I can audit what happened
  5. Fail loudly if something goes wrong, rather than silently

Prerequisites

sudo apt install tar gzip rsync

Step 1: The Core Backup Script

#!/usr/bin/env bash
set -euo pipefail

SOURCE="${1:-}"
BACKUP_DIR="${2:-$HOME/backups}"

if [[ -z "$SOURCE" || ! -d "$SOURCE" ]]; then
    echo "Usage: $0 <source-directory> [backup-directory]"
    exit 1
fi

mkdir -p "$BACKUP_DIR"

TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
ARCHIVE_NAME="backup-${TIMESTAMP}.tar.gz"
ARCHIVE_PATH="${BACKUP_DIR}/${ARCHIVE_NAME}"

tar -czf "$ARCHIVE_PATH" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")"

echo "Backup created: $ARCHIVE_PATH"

How This Works

Step 2: Verifying the Backup Succeeded

A backup that silently fails is worse than no backup at all, because it gives you false confidence. Let’s verify:

if tar -tzf "$ARCHIVE_PATH" &>/dev/null; then
    SIZE=$(du -h "$ARCHIVE_PATH" | cut -f1)
    echo "Verified OK. Size: $SIZE"
else
    echo "ERROR: Backup archive appears corrupted!" >&2
    exit 1
fi

tar -tzf (list contents) doubles as a lightweight integrity check here — if the archive is truncated or corrupted, this command will fail with a non-zero exit code, which we catch with the if statement.

Step 3: Adding Rotation (Keeping Only the Last N Backups)

Without rotation, your backup folder grows forever. Let’s keep only the most recent 7:

KEEP=7

BACKUP_COUNT=$(find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | wc -l)

if (( BACKUP_COUNT > KEEP )); then
    find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | sort | head -n "-${KEEP}" | while IFS= read -r old_backup; do
        echo "Removing old backup: $(basename "$old_backup")"
        rm -f "$old_backup"
    done
fi

Breaking This Down

Step 4: Logging Every Run

LOG_DIR="$HOME/.local/share/backup-tool/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/backup.log"

{
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup of $SOURCE"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Archive: $ARCHIVE_PATH"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Size: $SIZE"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Status: SUCCESS"
    echo "---"
} >> "$LOG_FILE"

The { ... } >> "$LOG_FILE" syntax groups multiple commands and redirects all of their combined output to a single file with one >> (append) redirect, which is more efficient and readable than repeating the redirect on every line.

Step 5: Optional Off-Site Copy via rsync

REMOTE="${3:-}"

if [[ -n "$REMOTE" ]]; then
    echo "Copying backup to remote: $REMOTE"
    rsync -av "$ARCHIVE_PATH" "$REMOTE"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copied to $REMOTE" >> "$LOG_FILE"
fi

This makes the third argument optional — if supplied (e.g. user@host:/backups/), the freshly created archive gets copied off-machine immediately, following the 3-2-1 backup principle (three copies, two different media, one off-site).

Full Combined Script

#!/usr/bin/env bash
set -euo pipefail

SOURCE="${1:-}"
BACKUP_DIR="${2:-$HOME/backups}"
REMOTE="${3:-}"
KEEP=7

if [[ -z "$SOURCE" || ! -d "$SOURCE" ]]; then
    echo "Usage: $0 <source-dir> [backup-dir] [remote-destination]"
    exit 1
fi

mkdir -p "$BACKUP_DIR"
LOG_DIR="$HOME/.local/share/backup-tool/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/backup.log"

TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
ARCHIVE_PATH="${BACKUP_DIR}/backup-${TIMESTAMP}.tar.gz"

tar -czf "$ARCHIVE_PATH" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")"

if tar -tzf "$ARCHIVE_PATH" &>/dev/null; then
    SIZE=$(du -h "$ARCHIVE_PATH" | cut -f1)
    echo "Backup verified OK. Size: $SIZE"
else
    echo "ERROR: Backup archive appears corrupted!" >&2
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Status: FAILED - corrupted archive" >> "$LOG_FILE"
    exit 1
fi

BACKUP_COUNT=$(find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | wc -l)
if (( BACKUP_COUNT > KEEP )); then
    find "$BACKUP_DIR" -maxdepth 1 -iname "backup-*.tar.gz" | sort | head -n "-${KEEP}" | while IFS= read -r old; do
        rm -f "$old"
    done
fi

if [[ -n "$REMOTE" ]]; then
    rsync -av "$ARCHIVE_PATH" "$REMOTE"
fi

{
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup of $SOURCE -> $ARCHIVE_PATH ($SIZE) - SUCCESS"
} >> "$LOG_FILE"

echo "Backup complete: $ARCHIVE_PATH"

Real-World Use Cases

Automating with Cron

0 2 * * * /usr/local/bin/backup.sh /home/user/documents /home/user/backups user@offsite:/backups/documents >> /var/log/backup-cron.log 2>&1

Runs every night at 2 AM, backs up the documents folder, rotates old backups, and copies the fresh archive off-site.

Security Considerations

Optimization Tips

Troubleshooting

Common Mistakes to Avoid

Frequently Asked Questions

How often should I run backups? Depends on how much data you can afford to lose. Nightly is a reasonable default for most personal and small business use; critical databases may warrant hourly incremental backups.

Should backups be encrypted? Yes, especially for anything containing personal, financial, or credential data, and especially before copying to remote or cloud storage you don’t fully control.

What’s the difference between this and just using a cloud backup service? This script gives you full control and transparency over exactly what’s happening, works entirely offline if needed, and costs nothing beyond your own storage — but it also means you’re responsible for monitoring it, unlike a managed service.

Summary

We built a Bash backup tool that compresses a target directory into a timestamped, verified archive, automatically rotates old backups to control disk usage, logs every run for auditability, and optionally pushes a copy off-site via rsync. The recurring theme across this entire series holds true here too: verification and logging aren’t optional extras — they’re what separates a backup script you can actually trust from one that just gives you false confidence.

References

Exit mobile version