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_dumpno PostgreSQL usa um snapshot MVCC. O dump reflete o banco no instante em que a transação começou.mysqldump --single-transactionfaz 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.cnfSenha 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 -e15 3 * * * /usr/bin/flock -n /var/lock/backup-db /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1flock -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.sqlNo 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_restoreDepois 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.