Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions script/database-backup/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
DATABASE_HOST=10.0.10.80
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=masterkey
DATABASE_NAME=herbario_prod

# Google Drive (Dados reais que você gerou)
GDRIVE_TOKEN={"access_token":"SEU_ACCESS_TOKEN_AQUI","token_type":"Bearer","refresh_token":"SEU_REFRESH_TOKEN_AQUI","expiry":"2026-01-01T00:00:00Z"}
# ID extraído da URL da pasta do Google Drive destino
GDRIVE_FOLDER_ID=COLE_O_ID_DA_PASTA_DO_DRIVE_AQUI

# Configurações do serviço
RETENTION_DAILY=5
RETENTION_WEEKLY=4
CRON_SCHEDULE="0 2 * * *"
TZ=America/Sao_Paulo
1 change: 1 addition & 0 deletions script/database-backup/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
36 changes: 36 additions & 0 deletions script/database-backup/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
FROM debian:12-slim

RUN \
apt-get update && \
apt-get install -y \
cron \
tzdata \
gnupg \
curl \
ca-certificates \
rclone \
gzip && \
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg && \
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y \
postgresql-client-18 && \
apt-get remove -y \
curl \
ca-certificates \
gnupg && \
rm -rf /var/lib/apt/lists/*

ENV TZ=America/Sao_Paulo
ENV CRON_SCHEDULE="0 2 * * *"

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

COPY backup.sh entrypoint.sh /

RUN sed -i 's/\r$//' /backup.sh /entrypoint.sh && \
chmod +x /backup.sh /entrypoint.sh

ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]

CMD ["cron", "-f"]
90 changes: 90 additions & 0 deletions script/database-backup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# database-backup

Serviço standalone de backup automático do PostgreSQL para o Google Drive.

Arquitetura:

- Container Debian slim com `postgresql-client-18`, `rclone`, `cron` e `gzip`
- Deploy como app separado no CapRover
- Container stateless (arquivo temporário em `/tmp`)

Fluxo:

`cron -> backup.sh -> pg_dump -Fp -> gzip -> rclone copy -> retenção`

## Formato do backup

Arquivos seguem o padrão:

```text
{DATABASE_NAME}_{unix_timestamp}.sql.gz
```

Exemplo: `herbario_prod_1748469600.sql.gz`

## Variáveis de ambiente

Veja `.env.example` para o contrato completo.

- `DATABASE_HOST`
- `DATABASE_PORT`
- `DATABASE_USER`
- `DATABASE_PASSWORD`
- `DATABASE_NAME`
- `GDRIVE_TOKEN`
- `GDRIVE_FOLDER_ID`
- `RETENTION_DAILY` (default: `5`)
- `RETENTION_WEEKLY` (default: `4`)
- `CRON_SCHEDULE` (ex.: `0 2 * * *`)
- `TZ` (ex.: `America/Sao_Paulo`)

## Política de retenção

Após upload bem-sucedido:

- mantém os `RETENTION_DAILY` backups mais recentes
- entre os restantes, mantém até `RETENTION_WEEKLY` semanas ISO distintas
- exclui o restante com `rclone deletefile`

Com valores padrão: máximo de 9 arquivos.

## Teste local

No diretório `script/database-backup`:

```bash
docker compose build
docker compose run --rm database-backup /backup.sh
```

Para validar remoto configurado no container:

```bash
docker compose run --rm database-backup rclone lsf gdrive:
```

## Restore de backup

O dump é SQL plain comprimido (`.sql.gz`), então restore é via `psql`:

```bash
gunzip -c herbario_prod_1748469600.sql.gz | \
PGPASSWORD="SENHA" psql \
-h 10.0.10.80 \
-p 5432 \
-U postgres \
-d herbario_restore
```

Fluxo recomendado:

1. Baixar `.sql.gz` do Google Drive.
2. Criar banco temporário de restore.
3. Restaurar no banco temporário.
4. Validar integridade e tabelas.
5. Remover banco temporário.

## Segurança

- O `access_token` expira; o `refresh_token` mantém o acesso de longo prazo.
- Se houver exposição do token, revogue no Google e gere um novo.
85 changes: 85 additions & 0 deletions script/database-backup/backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/bin/bash

set -euo pipefail

TIMESTAMP=$(date +%s)
FILE_NAME="${DATABASE_NAME}_${TIMESTAMP}.sql.gz"
TMP_FILE="/tmp/${FILE_NAME}"

echo "Iniciando processo de backup: ${FILE_NAME}"

echo "Dumping e comprimindo database..."
PGPASSWORD="${DATABASE_PASSWORD}" pg_dump \
-h "${DATABASE_HOST}" \
-p "${DATABASE_PORT}" \
-U "${DATABASE_USER}" \
-d "${DATABASE_NAME}" \
-Fp | gzip > "${TMP_FILE}"

echo "Enviando para o Google Drive..."
rclone copy "${TMP_FILE}" gdrive:

echo "Limpando arquivo local..."
rm -f "${TMP_FILE}"

echo "Aplicando política de retenção..."
FILES=()
INDEX_FILE=$(mktemp)

while IFS= read -r file; do
file="${file%$'\r'}"

ts="${file##*_}"
ts="${ts%.sql.gz}"
ts="${ts%$'\r'}"

if [[ "$ts" =~ ^[0-9]{10,}$ ]]; then
printf '%s\t%s\n' "$ts" "$file" >> "$INDEX_FILE"
fi
done < <(rclone lsf gdrive: --files-only)

if [ -s "$INDEX_FILE" ]; then
mapfile -t FILES < <(sort -rn "$INDEX_FILE" | cut -f2-)
fi

echo " -> Arquivos elegiveis para retenção: ${#FILES[@]}"

rm -f "$INDEX_FILE"

if [ "${#FILES[@]}" -eq 0 ]; then
echo " -> Nenhum arquivo elegivel para retenção foi encontrado em gdrive:."
fi

DAILY_KEPT=0
WEEKLY_KEPT=0
WEEKLY_SEEN_WEEKS=""
MAX_DAILY=${RETENTION_DAILY:-5}
MAX_WEEKLY=${RETENTION_WEEKLY:-4}

for FILE in "${FILES[@]}"; do
FILE_TS="${FILE##*_}"
FILE_TS="${FILE_TS%.sql.gz}"

if [[ ! "$FILE_TS" =~ ^[0-9]{10,}$ ]]; then
continue
fi

ISO_WEEK=$(date -d "@$FILE_TS" +%G-W%V)

if [ "$DAILY_KEPT" -lt "$MAX_DAILY" ]; then
echo " -> [DIÁRIO] Mantendo backup recente: $FILE (Semana: $ISO_WEEK)"
DAILY_KEPT=$((DAILY_KEPT + 1))

else
if [[ ! "$WEEKLY_SEEN_WEEKS" =~ " $ISO_WEEK " ]] && [ "$WEEKLY_KEPT" -lt "$MAX_WEEKLY" ]; then
echo " -> [SEMANAL] Mantendo primeiro da semana: $FILE (Semana: $ISO_WEEK)"
WEEKLY_KEPT=$((WEEKLY_KEPT + 1))
WEEKLY_SEEN_WEEKS="$WEEKLY_SEEN_WEEKS $ISO_WEEK "
else
echo " -> Deletando backup antigo: $FILE"
rclone deletefile gdrive:"$FILE"
fi
fi
done

echo "Processo de backup concluído com sucesso: ${FILE_NAME}"
4 changes: 4 additions & 0 deletions script/database-backup/captain-definition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
22 changes: 22 additions & 0 deletions script/database-backup/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
services:
database-backup:
image: database-backup:local
container_name: database-backup
build: .
environment:
# PostgreSQL
DATABASE_HOST: "${DATABASE_HOST}"
DATABASE_PORT: "${DATABASE_PORT}"
DATABASE_USER: "${DATABASE_USER}"
DATABASE_PASSWORD: "${DATABASE_PASSWORD}"
DATABASE_NAME: "${DATABASE_NAME}"

# Google Drive
GDRIVE_TOKEN: "${GDRIVE_TOKEN}"
GDRIVE_FOLDER_ID: "${GDRIVE_FOLDER_ID}"

RETENTION_DAILY: "${RETENTION_DAILY}"
RETENTION_WEEKLY: "${RETENTION_WEEKLY}"

CRON_SCHEDULE: "${CRON_SCHEDULE}"
TZ: "${TZ}"
26 changes: 26 additions & 0 deletions script/database-backup/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/sh

echo "Configurando rclone..."
mkdir -p /root/.config/rclone
cat <<EOF > /root/.config/rclone/rclone.conf
[gdrive]
type = drive
scope = drive.file
token = ${GDRIVE_TOKEN}
root_folder_id = ${GDRIVE_FOLDER_ID}
EOF

echo "Configuring cron job with schedule: $CRON_SCHEDULE ($TZ)"

printenv | grep -E "^(DATABASE_|GDRIVE_|RETENTION_|CRON_SCHEDULE|TZ)" >> /etc/environment

echo "$CRON_SCHEDULE /backup.sh 1> /proc/1/fd/1 2> /proc/1/fd/2" | crontab -

if ! crontab -l >/dev/null 2>&1; then
echo "Failed to configure cron job"
exit 1
fi

crontab -l

exec "$@"
Loading