#!/usr/bin/env bash # Backup completo do VPS (nível host, fora dos sidecars de cada stack). # # Cobre o que os sidecars de `lronetto-wedding` (postgres-backup-local + # media-backup) NÃO cobrem sozinhos: Redis, dados do Gitea (repos + config), # .env de todos os consumidores da infra base (wedding, gitea, finance, # claudeweb) e certs do Caddy. Pensado pra rodar via cron no host, fora de # qualquer container. # # Nota: o dump do Postgres é `pg_dumpall` (passo 1) — cobre TODOS os bancos da # instância de uma vez (wedding, gitea, finance, claudeweb), não só o da # wedding. Não precisa de um passo por app. # # Uso: # ./scripts/vps-backup.sh # # Cron sugerido (diário às 3h, log em backups/vps/cron.log): # 0 3 * * * cd /opt/lronetto-main && ./scripts/vps-backup.sh >> backups/vps/cron.log 2>&1 # # Env vars (todas opcionais, com default): # BACKUP_DIR destino local dos tarballs (default: ./backups/vps) # BACKUP_RETENTION_DAYS dias de retenção local (default: 14) # BACKUP_REMOTE remote do rclone pra cópia offsite, ex.: r2:meu-bucket/vps-backup # (vazio = não envia offsite) # GITEA_DIR caminho do repo lronetto-gitea (default: ../lronetto-gitea) # WEDDING_DIR caminho do repo lronetto-wedding (default: ../lronetto-wedding) # FINANCE_DIR caminho do repo lronetto-finance (default: ../lronetto-finance) # CLAUDEWEB_DIR caminho do repo lronetto-claudeweb (default: ../lronetto-claudeweb) # MINIO_ROOT_USER / MINIO_ROOT_PASSWORD só necessários se quiser o mirror # direto do MinIO por aqui (ver passo 4) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MAIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" BACKUP_DIR="${BACKUP_DIR:-$MAIN_DIR/backups/vps}" BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}" BACKUP_REMOTE="${BACKUP_REMOTE:-}" GITEA_DIR="${GITEA_DIR:-$MAIN_DIR/../lronetto-gitea}" WEDDING_DIR="${WEDDING_DIR:-$MAIN_DIR/../lronetto-wedding}" FINANCE_DIR="${FINANCE_DIR:-$MAIN_DIR/../lronetto-finance}" CLAUDEWEB_DIR="${CLAUDEWEB_DIR:-$MAIN_DIR/../lronetto-claudeweb}" TS="$(date +%Y%m%d-%H%M%S)" WORK_DIR="$(mktemp -d)" STAGE="$WORK_DIR/vps-backup-$TS" mkdir -p "$STAGE" log() { echo "[vps-backup] $(date '+%Y-%m-%d %H:%M:%S') $*"; } cleanup() { rm -rf "$WORK_DIR"; } trap cleanup EXIT container_up() { docker ps --format '{{.Names}}' | grep -qx "$1" } # 1. Postgres: dump lógico de TODOS os bancos + roles (pg_dumpall) if container_up postgres; then log "Dump do Postgres (pg_dumpall)..." docker exec -i postgres pg_dumpall -U postgres | gzip > "$STAGE/postgres-all.sql.gz" else log "AVISO: container 'postgres' não está rodando — pulando dump, backup incompleto" fi # 2. Redis: snapshot RDB (BGSAVE + copia o dump.rdb gerado) if container_up redis; then log "Snapshot do Redis (BGSAVE)..." docker exec redis redis-cli BGSAVE >/dev/null for _ in $(seq 1 30); do sleep 1 docker exec redis redis-cli INFO persistence 2>/dev/null | grep -q 'rdb_bgsave_in_progress:0' && break done docker cp redis:/data/dump.rdb "$STAGE/redis-dump.rdb" 2>/dev/null \ || log "AVISO: não consegui copiar dump.rdb do redis" else log "AVISO: container 'redis' não está rodando — pulando snapshot" fi # 3. Gitea: repos + config + dados (tudo que o docker-compose do gitea monta em bind mount) if [ -d "$GITEA_DIR" ]; then if [ -d "$GITEA_DIR/gitea/data" ]; then log "Compactando dados do Gitea ($GITEA_DIR/gitea/data)..." tar -czf "$STAGE/gitea-data.tar.gz" -C "$GITEA_DIR" gitea/data else log "AVISO: $GITEA_DIR/gitea/data não encontrado (confira o bind mount no docker-compose.yml do gitea)" fi else log "AVISO: $GITEA_DIR não encontrado — pulando backup do Gitea (ajuste GITEA_DIR= se o caminho for outro)" fi # 4. MinIO: mirror incremental do bucket de mídia via mc (opcional — a stack wedding já # tem o sidecar media-backup fazendo isso continuamente; isso aqui é um extra/fallback # se você rodar esse script num host que não tem o sidecar, ou quiser um snapshot avulso) if command -v mc >/dev/null 2>&1 && container_up minio; then log "Mirror do MinIO (bucket ${WEDDING_BUCKET:-wedding-media})..." mc alias set vps-backup-src "http://localhost:9000" \ "${MINIO_ROOT_USER:?defina MINIO_ROOT_USER pra fazer o mirror do MinIO, ou remova o binário mc do PATH pra pular essa etapa}" \ "${MINIO_ROOT_PASSWORD:?defina MINIO_ROOT_PASSWORD pra fazer o mirror do MinIO}" >/dev/null mkdir -p "$STAGE/minio-media" mc mirror --quiet "vps-backup-src/${WEDDING_BUCKET:-wedding-media}" "$STAGE/minio-media" \ || log "AVISO: mc mirror falhou" mc alias remove vps-backup-src >/dev/null 2>&1 || true else log "mc indisponível (ou minio parado) — pulando mirror direto; confie no sidecar media-backup da stack wedding" fi # 5. .env de todos os consumidores da infra base (segredos!) — o tarball final # fica com permissão 600. Cobre os repos "core" (wedding/gitea) e os demais # consumidores do platform layer (finance/claudeweb), se estiverem clonados. log "Copiando .env de todos os repos..." mkdir -p "$STAGE/envs" [ -f "$MAIN_DIR/.env" ] && cp "$MAIN_DIR/.env" "$STAGE/envs/lronetto-main.env" [ -f "$GITEA_DIR/.env" ] && cp "$GITEA_DIR/.env" "$STAGE/envs/lronetto-gitea.env" [ -f "$WEDDING_DIR/.env" ] && cp "$WEDDING_DIR/.env" "$STAGE/envs/lronetto-wedding.env" [ -f "$FINANCE_DIR/.env" ] && cp "$FINANCE_DIR/.env" "$STAGE/envs/lronetto-finance.env" [ -f "$CLAUDEWEB_DIR/.env" ] && cp "$CLAUDEWEB_DIR/.env" "$STAGE/envs/lronetto-claudeweb.env" # 6. Certs do Caddy (evita reemissão no Let's Encrypt depois de um restore) if [ -d "$MAIN_DIR/caddy/data" ]; then log "Compactando certs do Caddy..." tar -czf "$STAGE/caddy-data.tar.gz" -C "$MAIN_DIR/caddy" data fi # 7. Empacota tudo num único tarball final, permissão restrita (tem segredo dentro) mkdir -p "$BACKUP_DIR" FINAL="$BACKUP_DIR/vps-backup-$TS.tar.gz" log "Gerando tarball final: $FINAL" tar -czf "$FINAL" -C "$WORK_DIR" "vps-backup-$TS" chmod 600 "$FINAL" # 8. Envia offsite via rclone, se configurado (BACKUP_REMOTE + rclone instalado) if [ -n "$BACKUP_REMOTE" ]; then if command -v rclone >/dev/null 2>&1; then log "Enviando offsite pra $BACKUP_REMOTE..." rclone copy "$FINAL" "$BACKUP_REMOTE" || log "AVISO: rclone copy falhou" else log "AVISO: BACKUP_REMOTE definido mas rclone não está instalado — pulando envio offsite" fi fi # 9. Rotação: apaga tarballs locais mais velhos que a retenção configurada log "Rotacionando backups locais (> $BACKUP_RETENTION_DAYS dias)..." find "$BACKUP_DIR" -name 'vps-backup-*.tar.gz' -mtime "+$BACKUP_RETENTION_DAYS" -print -delete log "Backup concluído: $FINAL ($(du -h "$FINAL" | cut -f1))"