From 9c0a3c19f626192e3c8556a8eb628b4068a95ee7 Mon Sep 17 00:00:00 2001 From: Daniel Dada Date: Wed, 25 Feb 2026 01:47:35 +0300 Subject: [PATCH] added backup script --- .gitignore | 3 +- README.md | 84 +++++++++++++++++++++++++++++++ scripts/restic-backup.env.example | 12 +++++ scripts/restic-backup.sh | 57 +++++++++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 scripts/restic-backup.env.example create mode 100755 scripts/restic-backup.sh diff --git a/.gitignore b/.gitignore index a9440a8..05ebaf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /data -/config/media \ No newline at end of file +/config/media +scripts/restic-backup.env \ No newline at end of file diff --git a/README.md b/README.md index 4b46b3b..646e87d 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,87 @@ docker compose -f stacks/media/compose.yaml down | **Storage** | | | | Gitea | 3000, 222 | storage | | Copyparty | 3923 | storage | + +## Backup + +Backups use [Restic](https://restic.net/). The script backs up `/srv/homelab` (including `data/`), `/etc/caddy/Caddyfile`, and `/etc/unbound/unbound.conf`. It **stops all Docker stacks** before backup for consistent data, then starts them again—run it manually when you can afford a few minutes of downtime. + +### One-time setup + +1. Install restic (e.g. `pacman -S restic` or from [restic.net](https://restic.net/)). + +2. Copy the env example and set repository and password: + + ```bash + cp scripts/restic-backup.env.example scripts/restic-backup.env + # Edit scripts/restic-backup.env: set RESTIC_REPOSITORY and RESTIC_PASSWORD + ``` + + Examples for `RESTIC_REPOSITORY`: + - Local: `RESTIC_REPOSITORY=/backup/restic` + - SFTP: `RESTIC_REPOSITORY=sftp:user@backup-host:/restic` + - S3: `RESTIC_REPOSITORY=s3:s3.amazonaws.com/bucket-name` + +3. Initialize the repo (once): + + ```bash + export RESTIC_PASSWORD='your-password' + restic -r /backup/restic init # use your actual repo path + ``` + +### Create a backup + +From the repo root, run: + +```bash +sudo ./scripts/restic-backup.sh +``` + +Stacks are stopped, then the backup runs, then they are started again. A failed backup still triggers the start so the homelab comes back up. + +### Inspect backups + +List snapshots (IDs and timestamps): + +```bash +source scripts/restic-backup.env +sudo restic snapshots -r $RESTIC_REPOSITORY +``` + +List files in a snapshot (e.g. latest or by ID): + +```bash +restic ls latest -r $RESTIC_REPOSITORY +restic ls -r $RESTIC_REPOSITORY +``` + +Browse a path inside a snapshot: + +```bash +restic ls latest -r $RESTIC_REPOSITORY /srv/homelab/config +``` + +### See diffs between backups + +Compare two snapshots (added, changed, removed files and content diff): + +```bash +restic diff -r $RESTIC_REPOSITORY +``` + +### Restore + +Restore the latest snapshot into a directory (does not overwrite the repo; use a separate target dir): + +```bash +restic restore latest --target /tmp/restore -r $RESTIC_REPOSITORY +``` + +Restore a specific snapshot or path: + +```bash +restic restore --target /tmp/restore -r $RESTIC_REPOSITORY +restic restore latest --path /etc/caddy --target /tmp/restore -r $RESTIC_REPOSITORY +``` + +After restore, fix ownership on `data/` if needed (see [Monitoring permissions](#2-monitoring-permissions)). diff --git a/scripts/restic-backup.env.example b/scripts/restic-backup.env.example new file mode 100644 index 0000000..9e824ed --- /dev/null +++ b/scripts/restic-backup.env.example @@ -0,0 +1,12 @@ +# Copy to restic-backup.env and set values. +# restic-backup.env is not committed (add to .gitignore if needed). + +# Repository: local path, sftp, rclone, or S3-compatible. +# Examples: +# RESTIC_REPOSITORY=/backup/restic +# RESTIC_REPOSITORY=sftp:user@backup-host:/restic +# RESTIC_REPOSITORY=s3:s3.amazonaws.com/bucket-name +export RESTIC_REPOSITORY="" + +# Repository password (used for encryption). +export RESTIC_PASSWORD="" diff --git a/scripts/restic-backup.sh b/scripts/restic-backup.sh new file mode 100755 index 0000000..1d40381 --- /dev/null +++ b/scripts/restic-backup.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/restic-backup.env" +HOMELAB_ROOT="/srv/homelab" +STACKS_STOPPED=false + +start_stacks() { + if [[ "$STACKS_STOPPED" != true ]]; then + return 0 + fi + echo "Starting Docker stacks..." + cd "$HOMELAB_ROOT" + docker compose -f stacks/media/compose.yaml up -d + docker compose -f stacks/monitoring/compose.yaml up -d + docker compose -f stacks/storage/compose.yaml up -d + echo "Stacks started." +} +trap start_stacks EXIT + +if [[ -f "$ENV_FILE" ]]; then + set -a + # shellcheck source=/dev/null + source "$ENV_FILE" + set +a +fi + +if [[ -z "${RESTIC_REPOSITORY:-}" ]]; then + echo "Error: RESTIC_REPOSITORY is not set. Create ${ENV_FILE} or export it." >&2 + echo "Example: RESTIC_REPOSITORY=sftp:user@host:/backups/restic" >&2 + exit 1 +fi + +if [[ -z "${RESTIC_PASSWORD:-}" ]]; then + echo "Error: RESTIC_PASSWORD is not set. Create ${ENV_FILE} or export it." >&2 + exit 1 +fi + +CADDYFILE="/etc/caddy/Caddyfile" +UNBOUND_CONF="/etc/unbound/unbound.conf" + +echo "Stopping Docker stacks..." +cd "$HOMELAB_ROOT" +docker compose -f stacks/media/compose.yaml down +docker compose -f stacks/monitoring/compose.yaml down +docker compose -f stacks/storage/compose.yaml down +STACKS_STOPPED=true + +echo "Starting restic backup..." +restic backup \ + "$HOMELAB_ROOT" \ + "$CADDYFILE" \ + "$UNBOUND_CONF" + +echo "Backup finished. Snapshot list:" +restic snapshots --latest 5