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.
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