147 lines
6.7 KiB
Bash
147 lines
6.7 KiB
Bash
|
|
#!/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))"
|