quadrante — where hyperfocus reigns
Quadrante

Utilitários

From .restic.sh to .rustic.sh: Adopting Native rustic Profiles and Fixing Silent Traps

How I finally retired the legacy ~/.restic.sh script in favor of ~/.rustic.sh using native TOML profiles, config inheritance, a single backup command per repository, silent exclusion fixes with as-path, and a dual disaster-recovery guide for both rustic and restic.

Giuliano Cardoso · · 18 min read

In the previous post, My new backup routine with rustic, I talked about swapping out the engine of my backup routine — which originally started in My backup routine with restic — moving from restic to the Rust-based rustic.

There was just one thing still nagging at me every time I opened a terminal or checked crontab -e: I had replaced the engine, but kept the entire legacy restic bodywork intact. The script still lived at ~/.restic.sh, fired off four separate backup commands in a row with dozens of repeated CLI flags, and carried over exclusion patterns untouched on autopilot.

Today I finally paid down that technical debt. I migrated ~/.restic.sh to ~/.rustic.sh, adopted rustic's native TOML configuration profiles, and uncovered a few silent traps along the way that could have wasted cloud storage or caused nasty surprises during a real restore.

Why move from repeated CLI flags to native profiles?

When you merely swap restic for rustic inside an old shell script, you leave most of the ergonomics and efficiency gains of rustic's configuration files and profiles on the table.

Here is why the upgrade was worth doing:

  1. No more legacy name confusion in cron: Keeping a rustic routine named ~/.restic.sh only made sense during the initial transition. Renaming it and updating crontab -e to ~/.rustic.sh keeps the setup honest.
  2. A single base profile with inheritance (use-profiles): Instead of duplicating the password file path, retention rules (keep-daily, keep-weekly, keep-monthly, keep-tags, prune), exclusions, and source lists inside Bash loops for every repository, shared settings live in /etc/rustic/base.toml. Both cloud.toml (plus rclone retry and timeout options) and local.toml simply inherit from base and declare their repository location.
  3. Shared visibility for both root and regular users: Because scheduled backups run via sudo, placing the profiles under /etc/rustic/ with 644 permissions guarantees that cron runs and ad-hoc manual commands read the exact same definitions.
  4. Four backups become one per repository: By defining [[backup.snapshots]] blocks in TOML, a single sudo rustic -P cloud backup opens the remote repository index once, starts the Rclone transport once, and creates all four snapshots sequentially — cutting overhead and connection churn.
  5. Much shorter everyday CLI commands: No more typing long --repo or --password-file flags in the terminal. With rustic.toml pointing to local, running sudo rustic snapshots queries the local NAS repository directly, and sudo rustic -P cloud snapshots checks the cloud copy.

Technical gains and traps uncovered along the way

Rewriting the script forced me to test every piece against temporary repositories and inspect older snapshots. Four findings made the whole effort pay off immediately.

1. The silent !./var/tmp/* glob bug and the cleanliness of as-path = "/"

In the old script, backing up the root filesystem via a read-only Btrfs snapshot meant cd /.snap_root followed by rustic backup . with exclusions like --glob "!./tmp/*" and --glob "!./var/tmp/*".

When I inspected an earlier system snapshot with rustic cat tree, I got a surprise: files inside /var/tmp — including service scratch directories and even temporary Docker export files — were actually being backed up. That leading ./ prefix simply did not match the way I assumed in rustic.

After checking rustic's file exclusion documentation and testing in a scratch repository, the native fix turned out much cleaner:

  • Point sources = ["/.snap_root"] directly;
  • Set as-path = "/" so snapshots store / as their canonical path instead of the temporary /.snap_root mount prefix;
  • Anchor exclusion globs explicitly to the source directory: "!/.snap_root/tmp/*", "!/.snap_root/var/tmp/*", "!/.snap_root/timeshift".

In practice, empty /tmp and /var/tmp directories are still preserved for clean restores, temporary scratch files stay out, and new snapshots no longer require moving files out of /.snap_root/* after restoring.

2. Profile globs in [[backup.snapshots]] replace rather than append

This subtlety is documented in the official config/full.toml reference, and it is easy to miss: when a [[backup.snapshots]] block omits globs or exclude-if-present, it inherits them from [backup]. However, once a snapshot block defines its own globs = [...], it replaces [backup].globs instead of appending to it.

That is why personal-data and sync-folder explicitly repeat "!**/.cache" alongside exclude-if-present = ["CACHEDIR.TAG", ".nobackup"] inside their snapshot blocks. Inspecting the merged config via rustic show-config verified this before any live run touched production backups.

3. Execution hygiene, trap EXIT, and honest notifications

If a backup is interrupted mid-flight, a leftover read-only Btrfs snapshot at /.snap_root or a stale /var/tmp/docker_bkp_temp directory can linger on disk. To harden the workflow:

  • I added trap cleanup_temps EXIT, guaranteeing automatic cleanup on exit — even on failures or validation runs;
  • I also run cleanup_temps right before creating a fresh /.snap_root snapshot;
  • I replaced shell redirections paired with sudo (sudo cmd > file, which break outside a root shell) with | sudo tee ... > /dev/null and mapfile -t for Docker container and image IDs;
  • I added an ERRORS counter so the ntfy success ping only fires when ERRORS -eq 0 — eliminating false "Backup Completed" notifications if an earlier stage failed;
  • I added three maintenance flags: -n | --dry-run, --only-profiles (regenerates /etc/rustic/*.toml without running backups), and --validate (exercises Btrfs snapshot creation, Docker metadata capture, profile loading for local and cloud, --dry-run backup and forget, and automatic cleanup via trap EXIT).

Note for anyone exploring further: rustic also supports native hooks inside TOML profiles (hooks.backup-before, hooks.backup-after), though I chose to keep Btrfs snapshotting and Docker exports in the Bash wrapper so both repositories (cloud and local) share a single preparation and cleanup cycle while keeping validation modes and ntfy error reporting straightforward.

4. The restore syntax trap between rustic and restic (plus Btrfs subvolumes)

One of the best things about rustic is repository-format compatibility with the restic design specification. In an emergency, you can recover your machine using either rustic or restic, whichever is easier to grab on a Live USB.

There was, however, a dangerous trap lurking in the old disaster-recovery notes at the bottom of my script: a quick search-and-replace from earlier had produced rustic restore latest --tag ... --target ..., which is invalid syntax. The flags differ between the two tools:

  • In rustic (restore): tag filtering uses --filter-tags "<tag>", and the destination path is positional (rustic restore latest --filter-tags "root-system" /mnt/@).
  • In restic (restore): tag filtering uses --tag "<tag>" with --target <dest> (restic restore latest --tag "root-system" --target /mnt/@).

Furthermore, on Manjaro using the default Btrfs subvolume layout (@ for / and @home for /home), recreating @ from a Live USB requires mounting subvol=@home onto /mnt/@/home before restoring personal data — otherwise /home files land inside @ instead of @home. Both complete recovery paths are now documented side-by-side in the script itself.

How the profiles are structured under /etc/rustic

The script automatically generates and keeps four TOML files synced in /etc/rustic/ (shown below with anonymized paths, remotes, topics, and host details):

# /etc/rustic/base.toml
[global]
log-level = "info"
progress-interval = "1min"
group-by = "host,label,paths"

[repository]
password-file = "/secure/path/.password.txt"

[backup]
exclude-if-present = ["CACHEDIR.TAG", ".nobackup"]
globs = ["!**/.cache"]

[[backup.snapshots]]
name = "root-system"
sources = ["/.snap_root"]
as-path = "/"
tags = ["root-system"]
one-file-system = true
globs = [
    "!/.snap_root/tmp/*",
    "!/.snap_root/var/tmp/*",
    "!/.snap_root/timeshift",
]

[[backup.snapshots]]
name = "sync-folder"
sources = ["/home/user/Sync"]
tags = ["sync-folder"]
exclude-if-present = ["CACHEDIR.TAG", ".nobackup"]
globs = ["!**/.cache"]

[[backup.snapshots]]
name = "personal-data"
sources = ["/home", "/data"]
tags = ["personal-data"]
exclude-if-present = ["CACHEDIR.TAG", ".nobackup"]
globs = [
    "!/data/backup",
    "!**/.cache",
]

[[backup.snapshots]]
name = "docker-data"
sources = ["/var/lib/docker/volumes", "/var/tmp/docker_bkp_temp"]
tags = ["docker-data"]

[forget]
keep-daily = 7
keep-weekly = 2
keep-monthly = 1
keep-tags = ["keep"]
prune = true

And the repository-specific profiles stay clean and concise:

# /etc/rustic/cloud.toml
[global]
use-profiles = ["base"]

[repository]
repository = "rclone:my_remote:backup/desktop"

[repository.options]
retry = "default"
timeout = "10min"
# /etc/rustic/local.toml
[global]
use-profiles = ["base"]

[repository]
repository = "/mnt/nas/backup"
# /etc/rustic/rustic.toml
[global]
use-profiles = ["local"]

The full ~/.rustic.sh script and crontab -e

Here is the complete updated script (kept alongside an identical synced copy, with real paths, remote names, notification endpoints, and secrets anonymized for publication):

#!/usr/bin/env bash

# ==============================================================================
# FULL BACKUP SCRIPT (SYSTEM + DATA + DOCKER) - RUSTIC (PROFILES) & BTRFS
# ==============================================================================

set -u

LOG_FILE="/home/user/rustic_backup_completo.log"
PASSWORD_FILE="/secure/path/.password.txt"
SYNC_DIR="/home/user/Sync"
NTFY_URL="https://ntfy.example.work/my-secret-topic"
DOCKER_BKP_DIR="/var/tmp/docker_bkp_temp"
RUSTIC_CONFIG_DIR="/etc/rustic"

REPO_CLOUD="rclone:my_remote:backup/desktop"
REPO_LOCAL="/mnt/nas/backup"

DRY_RUN=0
ONLY_PROFILES=0
VALIDATE=0
ERRORS=0

for arg in "$@"; do
    case "$arg" in
        -n|--dry-run)
            DRY_RUN=1
            ;;
        --only-profiles)
            ONLY_PROFILES=1
            ;;
        --validate)
            VALIDATE=1
            DRY_RUN=1
            ;;
        *)
            echo "Usage: $0 [-n|--dry-run] [--only-profiles] [--validate]" >&2
            exit 2
            ;;
    esac
done

# Redirect stdout and stderr to the timestamped log file during normal runs
if [ "$ONLY_PROFILES" -eq 0 ] && [ "$VALIDATE" -eq 0 ]; then
    exec > >(awk -v logfile="$LOG_FILE" '{ print strftime("[%Y-%m-%d %H:%M:%S]"), $0 >> logfile; close(logfile) }') 2>&1
fi

# Send error alerts via ntfy (Priority: default)
notify_error() {
    local message="$1"
    ERRORS=$((ERRORS + 1))
    echo "[ALERT] $message"
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "[DRY-RUN] ntfy notification suppressed."
        return 0
    fi
    curl -s -o /dev/null \
        -H "Title: Backup Failure (Desktop)" \
        -H "Tags: rotating_light,skull" \
        -H "Priority: default" \
        -d "$message" \
        "$NTFY_URL" || true
}

# Notify success only when no stage failed (Priority: min)
notify_success() {
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "[DRY-RUN] ntfy success notification suppressed."
        return 0
    fi
    curl -s -o /dev/null \
        -H "Title: Backup Completed (Desktop)" \
        -H "Tags: white_check_mark,floppy_disk" \
        -H "Priority: min" \
        -d "Backup and verification routine completed with zero interruptions." \
        "$NTFY_URL" || true
}

# Remove the Btrfs snapshot and temporary Docker export dir on exit or before start
cleanup_temps() {
    cd / || return 0
    if sudo btrfs subvolume show /.snap_root >/dev/null 2>&1; then
        sudo btrfs subvolume delete /.snap_root >/dev/null 2>&1 || true
    fi
    if [ -d "$DOCKER_BKP_DIR" ]; then
        sudo rm -rf "$DOCKER_BKP_DIR" || true
    fi
}

# Generate/update native TOML profiles in /etc/rustic (readable by root and user)
ensure_rustic_profiles() {
    sudo mkdir -p "$RUSTIC_CONFIG_DIR"
    sudo chmod 755 "$RUSTIC_CONFIG_DIR"

    sudo tee "$RUSTIC_CONFIG_DIR/base.toml" > /dev/null <<EOF
# Base profile shared across all repositories (inherited via use-profiles)
[global]
log-level = "info"
progress-interval = "1min"
group-by = "host,label,paths"

[repository]
password-file = "$PASSWORD_FILE"

[backup]
exclude-if-present = ["CACHEDIR.TAG", ".nobackup"]
globs = ["!**/.cache"]

[[backup.snapshots]]
name = "root-system"
sources = ["/.snap_root"]
as-path = "/"
tags = ["root-system"]
one-file-system = true
globs = [
    "!/.snap_root/tmp/*",
    "!/.snap_root/var/tmp/*",
    "!/.snap_root/timeshift",
]

[[backup.snapshots]]
name = "sync-folder"
sources = ["$SYNC_DIR"]
tags = ["sync-folder"]
exclude-if-present = ["CACHEDIR.TAG", ".nobackup"]
globs = ["!**/.cache"]

[[backup.snapshots]]
name = "personal-data"
sources = ["/home", "/data"]
tags = ["personal-data"]
exclude-if-present = ["CACHEDIR.TAG", ".nobackup"]
globs = [
    "!/data/backup",
    "!**/.cache",
]

[[backup.snapshots]]
name = "docker-data"
sources = ["/var/lib/docker/volumes", "$DOCKER_BKP_DIR"]
tags = ["docker-data"]

[forget]
keep-daily = 7
keep-weekly = 2
keep-monthly = 1
keep-tags = ["keep"]
prune = true
EOF

    sudo tee "$RUSTIC_CONFIG_DIR/cloud.toml" > /dev/null <<EOF
# Remote cloud repository profile (via rclone)
[global]
use-profiles = ["base"]

[repository]
repository = "$REPO_CLOUD"

[repository.options]
retry = "default"
timeout = "10min"
EOF

    sudo tee "$RUSTIC_CONFIG_DIR/local.toml" > /dev/null <<EOF
# Local / NAS repository profile ($REPO_LOCAL)
[global]
use-profiles = ["base"]

[repository]
repository = "$REPO_LOCAL"
EOF

    sudo tee "$RUSTIC_CONFIG_DIR/rustic.toml" > /dev/null <<'EOF'
# Default profile when -P is omitted on the command line
[global]
use-profiles = ["local"]
EOF

    sudo chmod 644 \
        "$RUSTIC_CONFIG_DIR/base.toml" \
        "$RUSTIC_CONFIG_DIR/cloud.toml" \
        "$RUSTIC_CONFIG_DIR/local.toml" \
        "$RUSTIC_CONFIG_DIR/rustic.toml"
}

ensure_rustic_profiles

if [ "$ONLY_PROFILES" -eq 1 ]; then
    echo "[INFO] rustic profiles updated in $RUSTIC_CONFIG_DIR."
    exit 0
fi

trap cleanup_temps EXIT

echo "=== Starting FULL backup routine (Native Profiles) ==="

# 1. Clean any leftover artifacts and freeze the root filesystem (Read-Only Btrfs Snapshot)
cleanup_temps
echo "[INFO] Creating root filesystem (/) snapshot..."
sudo btrfs subvolume snapshot -r / /.snap_root || notify_error "Failed to create root Btrfs snapshot."

# 2. Prepare Docker backup (Images and Metadata)
echo "[INFO] Collecting Docker metadata and images..."
sudo mkdir -p "$DOCKER_BKP_DIR"

mapfile -t CONTAINER_IDS < <(sudo docker ps -aq)
if [ "${#CONTAINER_IDS[@]}" -gt 0 ]; then
    sudo docker ps -a | sudo tee "$DOCKER_BKP_DIR/containers_list.txt" > /dev/null
    sudo docker inspect "${CONTAINER_IDS[@]}" 2>/dev/null | sudo tee "$DOCKER_BKP_DIR/containers_inspect.json" > /dev/null || true
else
    echo "No containers found." | sudo tee "$DOCKER_BKP_DIR/containers_list.txt" > /dev/null
fi

if [ "$VALIDATE" -eq 1 ]; then
    echo "[VALIDATION] Full docker_images.tar export skipped in --validate mode."
    echo "Dry-run validation." | sudo tee "$DOCKER_BKP_DIR/docker_images_validation.txt" > /dev/null
else
    echo "[INFO] Exporting Docker images to .tar archive..."
    mapfile -t IMAGE_IDS < <(sudo docker images -q)
    if [ "${#IMAGE_IDS[@]}" -gt 0 ]; then
        sudo docker save "${IMAGE_IDS[@]}" -o "$DOCKER_BKP_DIR/docker_images.tar" 2>/dev/null || notify_error "Failed to export Docker images."
    else
        echo "No images found." | sudo tee "$DOCKER_BKP_DIR/docker_images.txt" > /dev/null
    fi
fi

# Fast end-to-end validation mode (Btrfs + Docker + Profiles + trap)
if [ "$VALIDATE" -eq 1 ]; then
    echo "[VALIDATION] Verifying local and cloud profile loading..."
    sudo rustic -P local show-config > /dev/null
    sudo rustic -P cloud show-config > /dev/null
    echo "[VALIDATION] Testing backup --dry-run (sync-folder) and forget --dry-run on local profile..."
    sudo rustic -P local --dry-run backup --name sync-folder
    sudo rustic -P local --dry-run forget
    echo "=== Validation succeeded (cleanup handled by trap EXIT) ==="
    exit 0
fi

# 3. Repository Profiles (/etc/rustic/<profile>.toml)
PROFILES=(
    "cloud"
    "local"
)

RUSTIC_ARGS=()
if [ "$DRY_RUN" -eq 1 ]; then
    RUSTIC_ARGS+=(--dry-run)
fi

# 4. Run for each profile (a single backup command creates all [[backup.snapshots]])
for PROFILE in "${PROFILES[@]}"; do
    echo "--> Processing profile: $PROFILE"

    # 4.1 Unified backup (root-system, sync-folder, personal-data, and docker-data)
    echo "[INFO] Running backup of all snapshots for profile $PROFILE..."
    sudo rustic -P "$PROFILE" "${RUSTIC_ARGS[@]}" backup \
        || notify_error "Backup error on profile: $PROFILE"

    # 4.2 Retention policy and pruning (prune = true is set in [forget] of base.toml)
    echo "[INFO] Applying retention policy and prune on profile $PROFILE..."
    sudo rustic -P "$PROFILE" "${RUSTIC_ARGS[@]}" forget \
        || notify_error "Retention/prune (forget) error on profile: $PROFILE"

    # 4.3 Repository integrity check
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "[DRY-RUN] Heavy integrity check skipped for profile: $PROFILE"
    else
        echo "[INFO] Checking repository integrity ($PROFILE)..."
        sudo rustic -P "$PROFILE" check \
            || notify_error "WARNING: INTEGRITY failure detected on profile: $PROFILE"
    fi

    echo "--> Finished: $PROFILE"
    echo "----------------------------------------"
done

# 5. Final Cleanup (Btrfs and Docker)
echo "[INFO] Removing temporary Btrfs snapshot and temporary Docker export..."
cleanup_temps

if [ "$ERRORS" -eq 0 ]; then
    echo "=== Backup completed successfully ==="
    notify_success
else
    echo "=== Backup finished with $ERRORS failure(s) ==="
    exit 1
fi

# ==============================================================================
# DISASTER RECOVERY MANUAL (BARE METAL RESTORE)
# ==============================================================================
# rustic's repository format is 100% compatible with restic.
# In an emergency you can restore using EITHER tool (rustic OR restic),
# depending on what is easier to obtain in your Live USB environment.
#
# SYNTAX WARNING:
#   - rustic restore uses `--filter-tags "<tag>"` and a POSITIONAL destination (no `--target`).
#   - restic restore uses `--tag "<tag>"` and `--target <destination>`.
#
# 1. BOOT FROM A LIVE USB (e.g. Manjaro Live USB).
#
# 2. MOUNT THE MAIN BTRFS PARTITION (Replace sdXN with your root partition, e.g. nvme0n1p2):
#    sudo mount /dev/sdXN /mnt
#
# 3. DELETE THE BROKEN SUBVOLUME AND CREATE A CLEAN ONE:
#    sudo btrfs subvolume delete /mnt/@
#    sudo btrfs subvolume create /mnt/@
#    (If you also need to restore @home from scratch, recreate /mnt/@home and mount it at /mnt/@/home:
#     sudo mkdir -p /mnt/@/home && sudo mount -o subvol=@home /dev/sdXN /mnt/@/home)
#
# ------------------------------------------------------------------------------
# OPTION A: RESTORING WITH RUSTIC
# ------------------------------------------------------------------------------
# 4A. INSTALL RUSTIC ON THE LIVE USB:
#     sudo pacman -Sy rustic
#     # Or via prebuilt static binary (handy if the package is unavailable):
#     curl -L https://github.com/rustic-rs/rustic/releases/latest/download/rustic-x86_64-unknown-linux-musl.tar.gz \
#       | tar -xz && sudo mv rustic /usr/local/bin/
#
# 5A. SET CREDENTIALS AND RESTORE SNAPSHOTS WITH RUSTIC:
#     export RUSTIC_PASSWORD="your_password_here"
#     export RUSTIC_REPOSITORY="rclone:my_remote:backup/desktop"  # or /path/to/local/repo
#     # Operating system (root):
#     sudo -E rustic restore latest --filter-tags "root-system" /mnt/@
#     # Personal data (/home and /data) and Sync folder:
#     sudo -E rustic restore latest --filter-tags "personal-data" /mnt/@
#     sudo -E rustic restore latest --filter-tags "sync-folder" /mnt/@
#     # (Tip: if the system is already running with /etc/rustic/ profiles, just run:
#     #  sudo rustic -P local restore latest --filter-tags "sync-folder" /)
#
# ------------------------------------------------------------------------------
# OPTION B: RESTORING WITH RESTIC
# ------------------------------------------------------------------------------
# 4B. INSTALL RESTIC ON THE LIVE USB (if not already present):
#     sudo pacman -Sy restic
#
# 5B. SET CREDENTIALS AND RESTORE SNAPSHOTS WITH RESTIC:
#     export RESTIC_PASSWORD="your_password_here"
#     export RESTIC_REPOSITORY="rclone:my_remote:backup/desktop"  # or /path/to/local/repo
#     # Operating system (root):
#     sudo -E restic restore latest --tag "root-system" --target /mnt/@
#     # Personal data (/home and /data) and Sync folder:
#     sudo -E restic restore latest --tag "personal-data" --target /mnt/@
#     sudo -E restic restore latest --tag "sync-folder" --target /mnt/@
#
# ------------------------------------------------------------------------------
# 6. NOTE ON RESTORED FILE PATHS:
#    - New snapshots (created with `as-path = "/"` in rustic profiles) restore
#      /etc, /usr, /var, etc. directly into /mnt/@/. No extra moves needed!
#    - Only if restoring an OLD snapshot (prior to migration, storing the
#      /.snap_root prefix), flatten paths with:
#      sudo mv /mnt/@/.snap_root/* /mnt/@/
#      sudo mv /mnt/@/.snap_root/.* /mnt/@/ 2>/dev/null || true
#      sudo rmdir /mnt/@/.snap_root
#
# 7. UNMOUNT TEMPORARY SUBVOLUMES AND REINSTALL THE BOOTLOADER (GRUB):
#    sudo umount -R /mnt
#    Manjaro includes the native `manjaro-chroot` utility:
#    sudo manjaro-chroot -a
#    (Select the restored installation). Inside the chroot, run:
#    grub-install /dev/sdX   # Replace sdX with the disk (e.g. /dev/nvme0n1), NO partition number
#    update-grub
#    exit
#
# 8. DOCKER RESTORE (if rebuilding separately after booting into the system):
#    - Using rustic:
#      sudo -E rustic restore latest --filter-tags "docker-data" /
#      # (or with a profile: sudo rustic -P local restore latest --filter-tags "docker-data" /)
#    - Or using restic:
#      sudo -E restic restore latest --tag "docker-data" --target /
#    - This restores volumes to /var/lib/docker/volumes and metadata/images to
#      /var/tmp/docker_bkp_temp/.
#    - Reload Docker images:
#      sudo docker load -i /var/tmp/docker_bkp_temp/docker_images.tar
#    - Recreate containers using your original docker-compose.yml files or by
#      inspecting /var/tmp/docker_bkp_temp/containers_list.txt and containers_inspect.json.
#    - Clean up the temporary directory when done:
#      sudo rm -rf /var/tmp/docker_bkp_temp
#
# 9. REBOOT AND VERIFY:
#    reboot
# ==============================================================================

And in crontab -e, updating the entry was just one line:

0 3 * * * sudo /home/user/.rustic.sh >/dev/null 2>&1

Validating it in the real world: commands and observed results

After passing bash -n ~/.rustic.sh && shellcheck ~/.rustic.sh with zero warnings, live checks confirmed how much cleaner daily operations became:

# 1. Generate/refresh only the TOML profiles in /etc/rustic/
~/.rustic.sh --only-profiles

# 2. Inspect the resolved merged config (base + target profile)
sudo rustic -P local show-config
sudo rustic -P cloud show-config

# 3. Query snapshots without typing --repo or --password-file
sudo rustic snapshots          # reads /etc/rustic/rustic.toml -> local (117 snapshots)
sudo rustic -P cloud snapshots # queries the remote cloud repo (108 snapshots)

# 4. Run a single targeted snapshot from the profile (e.g., Sync folder)
sudo rustic -P local backup --name sync-folder

In that targeted incremental run, rustic processed 756 files (1.3 GiB) in one focused pass, picked up 1 new file and 1 changed file (the synced copy of the updated script itself), uploaded 59.3 MiB compressed (from 79.2 MiB raw) to the local repository, and saved snapshot 26450f62 without touching the other three targets. Running sudo rustic -P local forget --dry-run and sudo rustic -P local check right after confirmed retention policy evaluation and repository integrity.

Finally, whenever I change something in the script and want to verify the entire pipeline before the 3 AM cron job fires — Btrfs snapshotting, Docker metadata collection, profile loading for both repositories, backup --dry-run, forget --dry-run, and clean teardown via trap EXIT — one command covers everything:

~/.rustic.sh --validate

Now the script's filename actually matches what is under the hood, configuration lives cleanly in native profiles, exclusions do what they promise, and if disaster ever strikes, both rustic and trusty restic have verified restore steps waiting right inside the file.

Sources and references

Comments (0)

No comments yet.

Leave a comment

SEE ALSO