lronetto-main/scripts/vps-backup.sh

147 lines
6.7 KiB
Bash
Raw Normal View History

2026-07-08 19:25:10 -03:00
#!/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))"