feat(backups): añadir stack de backups cifrados con Kopia + Cloudflare R2

- init-r2-repository.sh: inicializa repositorio Kopia en R2
- run-backup.sh: ejecuta backup de rutas definidas en backup-paths.txt
- pre-backup-dumps.sh: vuelca bases de datos (Postgres, MariaDB) antes del backup
- kopia-common.sh: funciones compartidas de autenticación y configuración
- restore-smoke-test.sh: test de restauración para verificar integridad
- backup-paths.txt / excludes.txt: rutas y exclusiones del backup
- systemd/: unidades kopia-server, kopia-docker-backup (service + timer)
- traefik-kopia.yml: exposición del servidor Kopia vía Traefik
- env.example: plantilla de variables (credenciales R2, contraseña repo)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduardo David Paredes Vara
2026-05-25 05:48:38 +00:00
parent 6ba696ff2b
commit 616b20f285
14 changed files with 614 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/kopia-common.sh"
require_cmd docker
ensure_runtime_dirs
DUMP_DIR="$BACKUP_STAGING_DIR/dumps"
RUN_DIR="$DUMP_DIR/latest"
mkdir -p "$RUN_DIR"
find "$RUN_DIR" -type f -name '*.dump' -delete
find "$RUN_DIR" -type f -name '*.sql.gz' -delete
container_running() {
[[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" == "true" ]]
}
container_env() {
local container="$1"
local key="$2"
docker exec "$container" sh -lc "printenv $key 2>/dev/null || true" | tr -d '\r'
}
dump_postgres() {
local container="$1"
local fallback_user="$2"
local fallback_db="$3"
if ! container_running "$container"; then
log "Skip PostgreSQL dump for $container: container not running"
return 0
fi
local user db out
user="$(container_env "$container" POSTGRES_USER)"
db="$(container_env "$container" POSTGRES_DB)"
user="${user:-$fallback_user}"
db="${db:-$fallback_db}"
out="$RUN_DIR/${container}.dump"
log "Dumping PostgreSQL $container/$db"
docker exec "$container" pg_dump -U "$user" -d "$db" -Fc > "$out"
}
dump_mariadb() {
local container="$1"
if ! container_running "$container"; then
log "Skip MariaDB dump for $container: container not running"
return 0
fi
local out
out="$RUN_DIR/${container}.sql.gz"
log "Dumping MariaDB $container"
docker exec "$container" sh -lc '
set -eu
db="${MYSQL_DATABASE:-nextcloud}"
user="${MYSQL_USER:-nextcloud}"
pass="${MYSQL_PASSWORD:-}"
if command -v mariadb-dump >/dev/null 2>&1; then
MYSQL_PWD="$pass" mariadb-dump --single-transaction --quick -u "$user" "$db"
else
MYSQL_PWD="$pass" mysqldump --single-transaction --quick -u "$user" "$db"
fi
' | gzip -c > "$out"
}
dump_postgres "paperless-db" "paperless" "paperless"
dump_postgres "gitea-postgres" "gitea" "gitea"
dump_postgres "n8n-pg" "n8n" "n8n"
dump_postgres "vikunja-db" "vikunja" "vikunja"
dump_postgres "memos-db" "memos" "memos"
dump_postgres "ths-authentik-postgres" "authentik" "authentik"
dump_postgres "scheduler-db" "scheduler" "scheduler"
dump_postgres "guacamole-postgres" "guacamole" "guacamole"
dump_postgres "kasm_db" "kasmapp" "kasm"
dump_postgres "thscom-db" "postgres" "postgres"
dump_postgres "spintools-db" "postgres" "postgres"
dump_postgres "spintools-authentik-postgres" "authentik" "authentik"
dump_mariadb "nextcloud-db"
log "Database dumps written to $RUN_DIR"