Skip to content

Backup Script

Skrypt restic-backup.sh

Skrypt restic-backup.sh odpowiada za wykonanie kompletnego backupu danych serwera z użyciem Restic.

Zakres działania skryptu:

  • wykonanie dumpów baz danych (PostgreSQL, MariaDB)
  • walidacja poprawności dumpów
  • wykonanie backupu Restic:
  • danych aplikacji Dockera
  • sekretów
  • dumpów baz danych
  • zastosowanie polityki retencji w repozytorium Restic
  • lokalna retencja katalogów uruchomień

Skrypt działa jako użytkowni root i przeznaczony jest do uruchamiania ręcznego i automatycznego przez usługę systemd.


Wymagany skrypt wspólny

Wymagane skrypty

Aby skrypt backup się wykonał wymagane jest, aby w tym samym folderze znajdowały się skrypty dumpów baz danych backup-db-postgres.sh i backup-db-mariadb.sh oraz skrypt backup-common.sh, który zawiera funkcje współdzielone przez wszystkie skrypty backup.

backup-common.sh backup-db (PostgreSQL + MariaDB)


Tworzenie skryptu

Tworzymy plik:

micro /srv/config/scripts/backup-restic.sh

W pliku umieszczamy:

backup-restic.sh
#!/usr/bin/env bash
# /srv/config/scripts/backup-restic.sh
set -euo pipefail
IFS=$'\n\t'
umask 077

# 1. Load Common Library (robust path)
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
LIB_FILE="${SCRIPT_DIR}/backup-common.sh"

if [[ ! -f "${LIB_FILE}" ]]; then
  echo "CRITICAL: Library ${LIB_FILE} not found." >&2
  exit 1
fi

# shellcheck disable=SC1090
source "${LIB_FILE}"

die() { critical "$*"; }

# 2. Configuration
BACKUP_NAME="restic"

# Restic config / secrets
DB_ENV_FILE="/srv/backups/restic/db-backup.env"
RESTIC_REPOSITORY="sftp:storagebox:restic-repo"
RESTIC_PASSWORD_FILE="/srv/backups/restic/restic-password.txt"
RESTIC_EXCLUDES_FILE="/srv/backups/restic/restic-excludes.txt"
RESTIC_CACHE_DIR="/srv/backups/restic/cache"

export RESTIC_REPOSITORY
export RESTIC_PASSWORD_FILE
export RESTIC_CACHE_DIR

# What to back up
DATA_DIR="/srv/docker/data"
SECRETS_DIR="/srv/docker/secrets"
DUMPS_DIR="/srv/backups/db-dumps"

# DB dump scripts (same dir as this script)
PG_DUMP_SCRIPT="${SCRIPT_DIR}/backup-db-postgres.sh"
MDB_DUMP_SCRIPT="${SCRIPT_DIR}/backup-db-mariadb.sh"

# Identity / tags
RESTIC_HOSTNAME="$(hostname -s)"
RESTIC_TAG="prod-data"

# Retention policy (restic repo)
KEEP_DAILY="7"
KEEP_WEEKLY="4"
KEEP_MONTHLY="6"

# Retention policy (local run dirs)
RUNS_BASE_DIR="/srv/backups/restic/runs"
RUN_RETENTION_DAYS="30"

# Run dir
TS="$(date +%Y%m%d_%H%M)"
RUN_DIR="${RUNS_BASE_DIR}/${TS}"

START_EPOCH="$(date +%s)"

# 3. Standard Checks & Setup (from Common)
acquire_lock "backup-${BACKUP_NAME}"
check_root

# Ensure base dirs exist
mkdir -p "${RUNS_BASE_DIR}"
setup_backup_env "${RUN_DIR}"

# 4. Preflight
header "Starting Restic Backup: ${TS}"

command -v restic >/dev/null 2>&1 || die "restic not found in PATH"
command -v docker  >/dev/null 2>&1 || die "docker not found in PATH"

# Hard checks for secret/config files (root:root 600, no symlink)
check_config_perms "${DB_ENV_FILE}"
check_config_perms "${RESTIC_PASSWORD_FILE}"
check_config_perms "${RESTIC_EXCLUDES_FILE}"

# Scripts must be executable
[[ -x "${PG_DUMP_SCRIPT}"  ]] || die "Not executable: ${PG_DUMP_SCRIPT}"
[[ -x "${MDB_DUMP_SCRIPT}" ]] || die "Not executable: ${MDB_DUMP_SCRIPT}"

# Directories must exist
[[ -d "${DATA_DIR}"    ]] || die "Missing directory: ${DATA_DIR}"
[[ -d "${SECRETS_DIR}" ]] || die "Missing directory: ${SECRETS_DIR}"
[[ -d "${DUMPS_DIR}"   ]] || die "Missing directory: ${DUMPS_DIR}"

# Cache dir
mkdir -p "${RESTIC_CACHE_DIR}"
chmod 0700 "${RESTIC_CACHE_DIR}" || true

# Guardrails for destructive operations scoping
[[ -n "${RESTIC_HOSTNAME}" ]] || die "RESTIC_HOSTNAME is empty (safety stop)"
[[ -n "${RESTIC_TAG}"      ]] || die "RESTIC_TAG is empty (safety stop)"

# Run metadata (no secrets)
{
  echo "ts=${TS}"
  echo "hostname=${RESTIC_HOSTNAME}"
  echo "tag=${RESTIC_TAG}"
  echo "repository=${RESTIC_REPOSITORY}"
  echo "data_dir=${DATA_DIR}"
  echo "secrets_dir=${SECRETS_DIR}"
  echo "dumps_dir=${DUMPS_DIR}"
  echo "keep_daily=${KEEP_DAILY}"
  echo "keep_weekly=${KEEP_WEEKLY}"
  echo "keep_monthly=${KEEP_MONTHLY}"
  echo "run_retention_days=${RUN_RETENTION_DAYS}"
} > "${RUN_DIR}/env-summary.txt"

# 5. Repo reachable?
log "[0/5] Checking restic repo..."
restic cat config >/dev/null 2>&1 || die "Cannot access restic repo (RESTIC_REPOSITORY)"
restic unlock >/dev/null 2>&1 || true

# 6. Helper: run dump script + validate SUCCESS/FAILED
run_dump_and_check() {
  local name="$1"
  local script="$2"
  local logfile="$3"

  log "==> Running ${name} dump: ${script}"

  set +e
  # Capture to run logfile AND stdout (journald)
  local out rc
  out="$(bash "${script}" 2>&1 | tee -a "${logfile}")"
  rc=${PIPESTATUS[0]}
  set -e

  # Extract FINAL_OUT_DIR emitted by dump script
  local dir
  dir="$(awk -F= '/^FINAL_OUT_DIR=/{print $2}' <<<"${out}" | tail -n1)"

  if [[ -z "${dir}" ]]; then
    [[ $rc -eq 0 ]] || die "${name} dump failed (rc=${rc}) and did not report FINAL_OUT_DIR"
    die "${name} dump did not report FINAL_OUT_DIR"
  fi

  # Validate markers
  if [[ -f "${dir}/.FAILED" ]]; then
    die "${name} dump marked FAILED: ${dir}"
  fi
  [[ -f "${dir}/.SUCCESS" ]] || die "${name} dump missing .SUCCESS marker: ${dir}"

  # Freshness check: .SUCCESS mtime must be >= start time of this run
  local ok_mtime
  ok_mtime="$(stat -c %Y "${dir}/.SUCCESS" 2>/dev/null || echo 0)"
  [[ "${ok_mtime}" -ge "${START_EPOCH}" ]] || die "${name} dump .SUCCESS not fresh (dir=${dir})"

  # If script returned non-zero but still produced SUCCESS (shouldn't happen) => fail hard
  [[ $rc -eq 0 ]] || die "${name} dump exited non-zero (rc=${rc}) despite .SUCCESS: ${dir}"

  log "OK: ${name} dump success: ${dir}"
}

# 7. DB dumps
log "[1/5] Dumping PostgreSQL..."
run_dump_and_check "PostgreSQL" "${PG_DUMP_SCRIPT}" "${RUN_DIR}/dump-postgres.log"

log "[2/5] Dumping MariaDB..."
run_dump_and_check "MariaDB" "${MDB_DUMP_SCRIPT}" "${RUN_DIR}/dump-mariadb.log"

# 8. Restic backup
log "[3/5] Running restic backup..."
RESTIC_LOG="${RUN_DIR}/restic.log"

ARGS=(
  backup
  "${DATA_DIR}"
  "${SECRETS_DIR}"
  "${DUMPS_DIR}"
  --tag "${RESTIC_TAG}"
  --host "${RESTIC_HOSTNAME}"
  --one-file-system
  --exclude-caches
  --exclude-file "${RESTIC_EXCLUDES_FILE}"
)

# Log to file + stdout
restic "${ARGS[@]}" 2>&1 | tee -a "${RESTIC_LOG}"

# Optional: record last snapshot (best-effort for convenience)
{
  echo ""
  echo "---- restic snapshots (last 1) ----"
  restic snapshots --host "${RESTIC_HOSTNAME}" --tag "${RESTIC_TAG}" --last 1 || true
} 2>&1 | tee -a "${RUN_DIR}/snapshot.txt" >/dev/null

# 9. Retention + prune (scoped)
log "[4/5] Applying retention policy and pruning..."
restic forget \
  --host "${RESTIC_HOSTNAME}" \
  --tag "${RESTIC_TAG}" \
  --keep-daily "${KEEP_DAILY}" \
  --keep-weekly "${KEEP_WEEKLY}" \
  --keep-monthly "${KEEP_MONTHLY}" \
  --prune 2>&1 | tee -a "${RESTIC_LOG}"

# 10. Local retention for run dirs (fail on error)
log "[5/5] Applying local retention for run directories..."
perform_retention "${RUNS_BASE_DIR}" "${RUN_RETENTION_DAYS}"

# Success marker (trap will NOT write .FAILED on rc=0)
: > "${RUN_DIR}/.SUCCESS"
log "Completed successfully."
echo "FINAL_OUT_DIR=${RUN_DIR}"

Nadawanie uprawnień

Uprawnienia 0750 pozwalają uruchamiać skrypt tylko rootowi i zaufanej grupie (np. sudo), a jednocześnie blokują dostęp dla pozostałych użytkowników.

sudo chown root:root /srv/config/scripts/backup-restic.sh
sudo chmod 0750 /srv/config/scripts/backup-restic.sh