← Voltar para Documentação

Enviar backup de banco para um bucket

Um script de dump e envio para PostgreSQL e MySQL/MariaDB, com retenção e o cuidado que evita backup corrompido.

Atualizado em

Backup de servidor e backup de banco resolvem problemas diferentes. O backup do servidor devolve a máquina inteira; o dump do banco devolve os dados de uma aplicação específica, num formato que você consegue restaurar em outro host, em outra versão do banco ou dentro de um container de teste. Se você ainda está decidindo o que usar, vale ler backup ou snapshot: qual usar em cada caso — este artigo cobre a terceira camada, o dump lógico enviado para um bucket.

O cuidado que evita backup corrompido

O erro mais comum é copiar o diretório de dados do banco com o serviço rodando. tar ou rclone sobre /var/lib/postgresql ou /var/lib/mysql com o processo ativo produz um conjunto de arquivos capturados em momentos diferentes: páginas de dados de um instante, índices de outro, WAL de um terceiro. O arquivo resultante existe, tem tamanho, sobe para o bucket sem erro — e falha na hora de restaurar. É o pior tipo de backup, porque parece pronto.

Use as ferramentas de dump do próprio banco. Elas abrem uma transação e leem um estado consistente, mesmo com escrita acontecendo em paralelo:

  • pg_dump no PostgreSQL usa um snapshot MVCC. O dump reflete o banco no instante em que a transação começou.
  • mysqldump --single-transaction faz o mesmo em tabelas InnoDB. Em tabelas MyISAM esse flag não protege nada, porque MyISAM não é transacional; nesse caso o dump só é consistente com --lock-tables, que bloqueia escrita durante a cópia.

O segundo cuidado é conferir o resultado antes de considerar o backup feito. Um dump pode terminar pela metade por disco cheio, timeout ou OOM kill. Sem verificação, você descobre isso no dia da restauração.

Antes de começar

Você precisa de um bucket e de credenciais funcionando na VPS. Se ainda não fez isso, veja criar um bucket compatível com S3 e configurar a AWS CLI e o rclone no seu bucket.

Crie também um usuário de banco só para backup, com permissão de leitura. Rodar dump como superusuário funciona, mas amplia o estrago de uma credencial vazada.

Guarde a senha fora do script, num arquivo de credenciais que só o dono lê:

# PostgreSQL
printf 'localhost:5432:*:backup:SUA_SENHA\n' > ~/.pgpass
chmod 600 ~/.pgpass

# MySQL / MariaDB
printf '[client]\nuser=backup\npassword=SUA_SENHA\n' > ~/.my.cnf
chmod 600 ~/.my.cnf

Senha em linha de comando aparece em ps para qualquer usuário do sistema. Os dois arquivos acima evitam isso e são lidos automaticamente pelas ferramentas.

O script

Salve como /usr/local/bin/backup-db.sh e ajuste as variáveis do topo.

#!/usr/bin/env bash
set -euo pipefail

ENGINE="postgres"                   # postgres | mysql
DB_NAME="app_producao"
DB_USER="backup"
BUCKET="s3://meus-backups/db/app_producao"
LOCAL_DIR="/var/backups/db"
RETENCAO_LOCAL=3                    # dias
RETENCAO_BUCKET=30                  # dias

umask 077
mkdir -p "$LOCAL_DIR"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"

dump_postgres() {
  pg_dump --format=custom --compress=6 --no-owner --no-privileges \
    --username="$DB_USER" --dbname="$DB_NAME" --file="$1"
}

verifica_postgres() { pg_restore --list "$1" > /dev/null; }

dump_mysql() {
  mysqldump --single-transaction --quick --routines --events \
    "$DB_NAME" | gzip -6 > "$1"
}

verifica_mysql() { gzip -t "$1"; }

if [ "$ENGINE" = "postgres" ]; then
  ARQ="$LOCAL_DIR/${DB_NAME}_${STAMP}.dump"
  dump_postgres "$ARQ"
  verifica_postgres "$ARQ"
else
  ARQ="$LOCAL_DIR/${DB_NAME}_${STAMP}.sql.gz"
  dump_mysql "$ARQ"
  verifica_mysql "$ARQ"
fi

[ -s "$ARQ" ] || { echo "dump vazio: $ARQ" >&2; exit 1; }

aws s3 cp "$ARQ" "$BUCKET/$(basename "$ARQ")" --only-show-errors

TAM_LOCAL=$(stat -c%s "$ARQ")
TAM_REMOTO=$(aws s3api head-object \
  --bucket "$(echo "$BUCKET" | cut -d/ -f3)" \
  --key "$(echo "$BUCKET" | cut -d/ -f4-)/$(basename "$ARQ")" \
  --query ContentLength --output text)

[ "$TAM_LOCAL" = "$TAM_REMOTO" ] || { echo "tamanho divergente" >&2; exit 1; }

find "$LOCAL_DIR" -type f -mtime "+$RETENCAO_LOCAL" -delete

LIMITE=$(date -u -d "-$RETENCAO_BUCKET days" +%Y-%m-%d)
aws s3 ls "$BUCKET/" | while read -r data _ _ nome; do
  [ "$data" \< "$LIMITE" ] && aws s3 rm "$BUCKET/$nome"
done

echo "ok: $(basename "$ARQ") ($TAM_LOCAL bytes)"

Três detalhes que fazem esse script ser diferente de um one-liner:

set -euo pipefail — sem pipefail, o pipe mysqldump | gzip retorna o status do gzip. Se o mysqldump morrer no meio, o gzip termina com sucesso e o script segue enviando um arquivo truncado.

A comparação de tamanho depois do upload — a AWS CLI já valida checksum na transferência, mas confirmar o objeto do lado do bucket detecta o caso em que o cp foi interrompido e nada chegou.

A retenção local só roda depois do upload confirmado. Se o envio falhou, set -e aborta antes e o dump de hoje permanece no disco.

Agendar

sudo chmod 750 /usr/local/bin/backup-db.sh
sudo crontab -e
15 3 * * * /usr/bin/flock -n /var/lock/backup-db /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1

flock -n impede que um dump lento ainda em andamento seja atropelado pela execução do dia seguinte — dois pg_dump simultâneos competem por I/O e podem derrubar o tempo de resposta da aplicação. Se as noites do seu servidor já estão apertadas, VPS lenta: como diagnosticar mostra como medir o impacto com iostat.

O que o dump não inclui

pg_dump salva um banco, não o cluster. Roles, senhas e permissões globais ficam de fora. Adicione ao seu backup:

pg_dumpall --globals-only --no-role-passwords > globais.sql

No MySQL, os usuários vivem no banco mysql, que mysqldump app_producao não toca. Inclua-o num dump separado.

Teste a restauração

Backup nunca testado é hipótese. Faça o teste em um banco descartável, nunca no de produção:

# PostgreSQL
createdb teste_restore
pg_restore --dbname=teste_restore --no-owner app_producao_20260805T031500Z.dump

# MySQL / MariaDB
mysql -e "CREATE DATABASE teste_restore"
gunzip -c app_producao_20260805T031500Z.sql.gz | mysql teste_restore

Depois confira contagem de linhas nas tabelas principais e a data do registro mais recente. Se o número não fecha com produção, o problema é no dump — e é melhor descobrir agora.

Se o cenário for perda do servidor inteiro e não de um banco, o caminho é outro: veja como restaurar um backup. Dúvida sobre limites de armazenamento ou volume de requisições no bucket, falar com o suporte.

Não resolveu?

Nosso suporte é humano e em português. Se este guia não cobriu o seu caso, fale com a gente.

Falar com o suporte →