Backup Script
Datei: /root/.nextcloud-backup.env
Ändere bitte alle Einträge mit Deinen Werten!
# --- Restic repository & credentials ---
RESTIC_REPOSITORY="/mnt/synology-backup/nextcloud"
RESTIC_PASSWORD_FILE="/root/.restic-password"
# --- Backup target mount (must be mounted + writable) ---
BACKUP_MOUNT="/mnt/synology-backup"
# --- Nextcloud paths on the Docker host (absolute base path) ---
# Example layout:
# /home/dockeruser/docker/nextcloud/
# ├─ config/
# ├─ nextcloud/ (volume bind-mount)
# │ ├─ data/
# │ └─ config/ (optional, depending on your setup)
# └─ docker-compose.yml
NC_BASE="/home/dockeruser/docker/nextcloud"
# Space-separated list of config directories relative to NC_BASE.
# Keep both if you're unsure; the script will include only existing directories.
NC_CONFIG_PATHS="config nextcloud/config"
# Data directory relative to NC_BASE
NC_DATA_PATH="nextcloud/data"
# docker-compose file relative to NC_BASE
NC_COMPOSE_FILE="docker-compose.yml"
# --- Docker container names ---
NC_APP_CONTAINER="nextcloud-app"
NC_DB_CONTAINER="nextcloud-db"
# --- Database credentials inside the DB container ---
PGUSER="nextcloud"
PGDB="nextcloud"
# --- Retention policy ---
KEEP_DAILY="7"
KEEP_WEEKLY="4"
KEEP_MONTHLY="6"
# --- Integrity check (daily subset; keep small for speed) ---
READ_SUBSET="5%"
# --- Pushover ---
PUSHOVER_TOKEN="PUT_YOUR_TOKEN_HERE"
PUSHOVER_USER="PUT_YOUR_USER_KEY_HERE"
# Optional: leave empty to notify all devices
PUSHOVER_DEVICE=""
sudo chown root:root /root/.nextcloud-backup.env
sudo chmod 600 /root/.nextcloud-backup.env
Datei: /root/nextcloud-backup.sh
#!/bin/bash
set -euo pipefail
# =========================
# Nextcloud Restic Backup
# =========================
# ---- Load config/secrets ----
ENV_FILE="/root/.nextcloud-backup.env"
if [[ -f "$ENV_FILE" ]]; then
source "$ENV_FILE"
else
echo "ERROR: Env file not found: $ENV_FILE"
exit 1
fi
# ---- Required env vars ----
: "${RESTIC_REPOSITORY:?RESTIC_REPOSITORY not set}"
: "${RESTIC_PASSWORD_FILE:?RESTIC_PASSWORD_FILE not set}"
: "${BACKUP_MOUNT:?BACKUP_MOUNT not set}"
: "${NC_BASE:?NC_BASE not set}"
: "${NC_CONFIG_PATHS:?NC_CONFIG_PATHS not set}"
: "${NC_DATA_PATH:?NC_DATA_PATH not set}"
: "${NC_COMPOSE_FILE:?NC_COMPOSE_FILE not set}"
: "${NC_APP_CONTAINER:?NC_APP_CONTAINER not set}"
: "${NC_DB_CONTAINER:?NC_DB_CONTAINER not set}"
: "${PGUSER:?PGUSER not set}"
: "${PGDB:?PGDB not set}"
: "${KEEP_DAILY:?KEEP_DAILY not set}"
: "${KEEP_WEEKLY:?KEEP_WEEKLY not set}"
: "${KEEP_MONTHLY:?KEEP_MONTHLY not set}"
: "${READ_SUBSET:?READ_SUBSET not set}"
: "${PUSHOVER_TOKEN:?PUSHOVER_TOKEN not set}"
: "${PUSHOVER_USER:?PUSHOVER_USER not set}"
# PUSHOVER_DEVICE is optional
export RESTIC_REPOSITORY
export RESTIC_PASSWORD_FILE
# ---- Logging ----
LOGFILE="/var/log/nextcloud-backup.log"
CSVLOG="/var/log/nextcloud-backup.csv"
HOSTNAME="$(hostname -s)"
exec >> "$LOGFILE" 2>&1
log() { echo "[$(date '+%F %T')] $*"; }
csv_log() {
local STATUS="$1" # OK / FAIL
local DURATION="$2" # seconds
local SNAPSHOT="${3:-}"
local TS
TS="$(date --iso-8601=seconds)"
if [[ ! -f "$CSVLOG" ]]; then
echo "timestamp,host,status,duration_seconds,snapshot_id" > "$CSVLOG"
chmod 600 "$CSVLOG" || true
fi
echo "${TS},${HOSTNAME},${STATUS},${DURATION},${SNAPSHOT}" >> "$CSVLOG"
}
# ---- Pushover ----
pushover() {
local TITLE="$1"
local MESSAGE="$2"
local PRIORITY="${3:-0}"
local DEVICE_ARG=()
if [[ -n "${PUSHOVER_DEVICE:-}" ]]; then
DEVICE_ARG=(-F "device=$PUSHOVER_DEVICE")
fi
curl -s \
-F "token=$PUSHOVER_TOKEN" \
-F "user=$PUSHOVER_USER" \
"${DEVICE_ARG[@]}" \
-F "title=$TITLE" \
-F "priority=$PRIORITY" \
-F "message=$MESSAGE" \
https://api.pushover.net/1/messages.json >/dev/null || true
}
# ---- Wait for backup target (mounted + writable) ----
wait_for_backup_target() {
local TARGET_MOUNT="$BACKUP_MOUNT"
local MAX_WAIT=900 # 15 minutes
local INTERVAL=15 # seconds
local waited=0
log "Checking backup target: ${TARGET_MOUNT}"
while true; do
if mountpoint -q "$TARGET_MOUNT"; then
if timeout 5 sh -c "echo test > '${TARGET_MOUNT}/.nc_backup_writetest'"; then
rm -f "${TARGET_MOUNT}/.nc_backup_writetest" >/dev/null 2>&1 || true
log "Backup target OK (mounted + writable)"
return 0
fi
log "Backup target mounted but not writable yet."
else
log "Backup target not mounted yet."
fi
if (( waited >= MAX_WAIT )); then
log "ERROR: Backup target not ready after ${MAX_WAIT}s"
pushover "Nextcloud Backup FAILED" "Backup target (${TARGET_MOUNT}) not available/writable after ${MAX_WAIT}s. Aborting." 1
exit 1
fi
sleep "$INTERVAL"
waited=$((waited + INTERVAL))
done
}
# ---- Maintenance cleanup ----
cleanup() {
docker exec -u www-data "$NC_APP_CONTAINER" php occ maintenance:mode --off >/dev/null 2>&1 || true
}
START_TS=$(date +%s)
SNAPSHOT_ID=""
on_error() {
local exit_code=$?
local now dur
now=$(date +%s)
dur=$((now-START_TS))
log "ERROR: Backup failed (exit=${exit_code}) after ${dur}s"
csv_log "FAIL" "$dur" "${SNAPSHOT_ID:-}"
pushover "Nextcloud Backup FAILED" "Backup failed after ${dur}s (exit=${exit_code}). Check ${LOGFILE}" 1
cleanup
exit "$exit_code"
}
trap on_error ERR
trap cleanup EXIT
log "========== START BACKUP =========="
# Wait for NAS/NFS target before doing anything
wait_for_backup_target
log "Enabling maintenance mode"
docker exec -u www-data "$NC_APP_CONTAINER" php occ maintenance:mode --on
# ---- Build backup path list from env ----
BACKUP_PATHS=()
# Config directories (only include existing ones)
for cfg in $NC_CONFIG_PATHS; do
FULL_CFG="$NC_BASE/$cfg"
if [[ -d "$FULL_CFG" ]]; then
BACKUP_PATHS+=("$FULL_CFG")
fi
done
# Compose file
if [[ -f "$NC_BASE/$NC_COMPOSE_FILE" ]]; then
BACKUP_PATHS+=("$NC_BASE/$NC_COMPOSE_FILE")
else
log "WARNING: Compose file not found: $NC_BASE/$NC_COMPOSE_FILE"
fi
# Data directory
if [[ -d "$NC_BASE/$NC_DATA_PATH" ]]; then
BACKUP_PATHS+=("$NC_BASE/$NC_DATA_PATH")
else
log "WARNING: Data directory not found: $NC_BASE/$NC_DATA_PATH"
fi
if [[ ${#BACKUP_PATHS[@]} -eq 0 ]]; then
log "ERROR: No valid backup paths found. Check NC_BASE / NC_CONFIG_PATHS / NC_DATA_PATH / NC_COMPOSE_FILE."
exit 1
fi
log "Restic backup paths: ${BACKUP_PATHS[*]}"
log "Running restic backup (files)"
restic backup --tag nextcloud --tag daily "${BACKUP_PATHS[@]}"
log "Running restic backup (database dump via stdin -> db.sql)"
docker exec -t "$NC_DB_CONTAINER" pg_dump -U "$PGUSER" -d "$PGDB" \
| restic backup --stdin --stdin-filename "db.sql" --tag nextcloud --tag daily
log "Disabling maintenance mode"
docker exec -u www-data "$NC_APP_CONTAINER" php occ maintenance:mode --off
# Snapshot ID (best-effort)
SNAPSHOT_ID="$(restic snapshots --tag nextcloud --latest 1 --json 2>/dev/null \
| grep -m1 '"short_id"' \
| sed -E 's/.*"short_id": ?"([^"]+)".*/\1/' || true)"
log "Retention: forget/prune (daily=${KEEP_DAILY}, weekly=${KEEP_WEEKLY}, monthly=${KEEP_MONTHLY})"
restic forget \
--tag nextcloud \
--keep-daily "$KEEP_DAILY" \
--keep-weekly "$KEEP_WEEKLY" \
--keep-monthly "$KEEP_MONTHLY" \
--prune
log "Integrity check (read-data-subset=${READ_SUBSET})"
restic check --read-data-subset="$READ_SUBSET" >/dev/null
END_TS=$(date +%s)
DUR=$((END_TS-START_TS))
log "Backup finished in ${DUR}s (snapshot=${SNAPSHOT_ID:-unknown})"
log "========== END BACKUP =========="
csv_log "OK" "$DUR" "${SNAPSHOT_ID:-}"
pushover "Nextcloud Backup OK" "Backup successful in ${DUR}s (snapshot=${SNAPSHOT_ID:-unknown})."
sudo chown root:root /root/nextcloud-backup.sh
sudo chmod 700 /root/nextcloud-backup.sh
Datei: /root/nextcloud-restore-test.sh
#!/bin/bash
set -euo pipefail
# =========================
# Monthly Restic Restore Test
# =========================
ENV_FILE="/root/.nextcloud-backup.env"
if [[ -f "$ENV_FILE" ]]; then
source "$ENV_FILE"
else
echo "ERROR: Env file not found: $ENV_FILE"
exit 1
fi
: "${RESTIC_REPOSITORY:?RESTIC_REPOSITORY not set}"
: "${RESTIC_PASSWORD_FILE:?RESTIC_PASSWORD_FILE not set}"
: "${PUSHOVER_TOKEN:?PUSHOVER_TOKEN not set}"
: "${PUSHOVER_USER:?PUSHOVER_USER not set}"
export RESTIC_REPOSITORY
export RESTIC_PASSWORD_FILE
LOGFILE="/var/log/nextcloud-restore-test.log"
exec >> "$LOGFILE" 2>&1
log() { echo "[$(date '+%F %T')] $*"; }
pushover() {
local TITLE="$1"
local MESSAGE="$2"
local PRIORITY="${3:-0}"
local DEVICE_ARG=()
if [[ -n "${PUSHOVER_DEVICE:-}" ]]; then
DEVICE_ARG=(-F "device=$PUSHOVER_DEVICE")
fi
curl -s \
-F "token=$PUSHOVER_TOKEN" \
-F "user=$PUSHOVER_USER" \
"${DEVICE_ARG[@]}" \
-F "title=$TITLE" \
-F "priority=$PRIORITY" \
-F "message=$MESSAGE" \
https://api.pushover.net/1/messages.json >/dev/null || true
}
# Helper: pick first real path from `restic find` output
pick_find_path() {
# Prefer lines that start with "/" (real paths)
local p
p="$(restic find latest "$1" 2>/dev/null | grep '^/' | head -n 1 || true)"
if [[ -n "${p:-}" ]]; then
echo "$p"
return 0
fi
# Fallback: ignore informational lines, take first remaining line
p="$(restic find latest "$1" 2>/dev/null \
| grep -v '^Found matching entries' \
| grep -v '^repository ' \
| head -n 1 || true)"
echo "$p"
}
TARGET="/tmp/restore-test-monthly"
rm -rf "$TARGET"
mkdir -p "$TARGET"
log "========== MONTHLY RESTORE TEST =========="
log "Repo: $RESTIC_REPOSITORY"
SNAPSHOT_ID="$(restic snapshots --latest 1 --json 2>/dev/null \
| grep -m1 '"short_id"' \
| sed -E 's/.*"short_id": ?"([^"]+)".*/\1/' || true)"
if [[ -z "${SNAPSHOT_ID:-}" ]]; then
log "ERROR: No snapshots found"
pushover "Nextcloud Restore-Test FAILED" "No restic snapshots found." 1
exit 1
fi
log "Latest snapshot: $SNAPSHOT_ID"
CFG_PATH="$(pick_find_path "config.php")"
DB_PATH="$(pick_find_path "db.sql")"
if [[ -z "${CFG_PATH:-}" ]]; then
log "ERROR: config.php not found in latest snapshot"
pushover "Nextcloud Restore-Test FAILED" "config.php not found in latest snapshot." 1
exit 1
fi
if [[ -z "${DB_PATH:-}" ]]; then
log "ERROR: db.sql not found in latest snapshot"
pushover "Nextcloud Restore-Test FAILED" "db.sql not found in latest snapshot." 1
exit 1
fi
# Use relative paths for restore (more compatible across restic versions)
CFG_REL="${CFG_PATH#/}"
DB_REL="${DB_PATH#/}"
log "Restoring config: $CFG_REL"
restic restore latest --target "$TARGET" --include "$CFG_REL"
log "Restoring database: $DB_REL"
restic restore latest --target "$TARGET" --include "$DB_REL"
RESTORED_CFG="$TARGET/$CFG_REL"
RESTORED_DB="$TARGET/$DB_REL"
log "Checking restored files"
log " CFG: $RESTORED_CFG"
log " DB : $RESTORED_DB"
if [[ ! -s "$RESTORED_CFG" ]]; then
log "ERROR: Restored config.php missing/empty"
ls -lah "$TARGET" || true
pushover "Nextcloud Restore-Test FAILED" "Restore of config.php failed or file is empty." 1
exit 1
fi
if [[ ! -s "$RESTORED_DB" ]]; then
log "ERROR: Restored db.sql missing/empty"
pushover "Nextcloud Restore-Test FAILED" "Restore of db.sql failed or file is empty." 1
exit 1
fi
# Real read test
head -n 3 "$RESTORED_CFG" >/dev/null
head -n 3 "$RESTORED_DB" >/dev/null
log "Running quick integrity check (read-data-subset=1%)"
restic check --read-data-subset=1% >/dev/null
rm -rf "$TARGET"
log "Restore test successful (snapshot=$SNAPSHOT_ID)"
log "========== RESTORE TEST OK =========="
pushover "Nextcloud Restore-Test OK" "Restore test successful. Snapshot: $SNAPSHOT_ID"
sudo chown root:root /root/nextcloud-restore-test.sh
sudo chmod 700 /root/nextcloud-restore-test.sh
Cron einrichten
Wichtig ist, das Du als root eingeloggt sein musst.
crontab -e
Füge folgende Zeilen an das Ende ein.
Das Backup erfolgt jeden Tag um 3:30 Uhr und monatlich wird ein Restore-Test um 30:30 durchgeführt.
30 3 * * * /root/nextcloud-backup.sh
30 20 1 * * /root/nextcloud-restore-test.sh