← Blog

Backups automáticos do Postgres que você realmente consegue restaurar

Railyard

15 de maio de 2026 · 12 min de leitura

Siga isso no seu próprio servidor. Comece grátis, sem cartão

Um cron job que roda pg_dump toda noite e escreve um arquivo em algum lugar não é um sistema de backup. Ele se torna um sistema de backup no dia em que alguém realmente precisa restaurar a partir dele, e não um dia antes disso. Muitas equipes descobrem isso do jeito difícil: o cron parou de disparar há três meses por causa de uma mudança no PATH que ninguém notou, ou o dump noturno vem gravando uma mensagem de erro de 40 bytes num arquivo .dump em vez de um banco há semanas, ou os backups estão sentados bem ao lado do banco que deveriam proteger e os dois desaparecem na mesma falha de servidor. Um arquivo no disco que nunca foi restaurado é um palpite, não um backup.

Ao final deste tutorial você vai ter um sistema de verdade: um pg_dump roteirizado no formato certo, um agendamento no cron que de fato executa isso, o dump enviado pra um armazenamento que não fica no mesmo servidor, uma política de retenção que descarta cópias antigas sem te deixar com zero backups, e, o mais importante, um job agendado que restaura seu backup mais recente num banco descartável e checa se ele realmente está bom. Essa última parte é a diferença entre um backup e uma esperança.

O que você vai precisar

  • Um servidor Postgres sob seu controle: uma VPS, uma máquina dedicada ou qualquer servidor self-hosted rodando Postgres com pg_dump, pg_restore, psql e createdb/dropdb disponíveis.
  • Um destino fora do servidor para os dumps: armazenamento de objetos compatível com S3 (AWS S3, DigitalOcean Spaces, Backblaze B2, Cloudflare R2 ou similar) e a AWS CLI ou o rclone configurado com credenciais pra ele. O ponto central deste exercício é que o backup não vive no mesmo disco do banco, então pular essa etapa anula o sistema todo.
  • Acesso de shell pra agendar jobs no cron no servidor Postgres (ou onde você rodar tarefas agendadas).
  • Espaço livre em disco suficiente no servidor pra um dump pousar localmente antes de ser enviado, e o mesmo espaço de novo pra um banco de teste de restauração durante a etapa de verificação.

Um domínio não faz parte disso. Você está fazendo backup de dados, não servindo tráfego.

Passo 1: roteirizar o dump

Use o formato customizado do pg_dump (-Fc, equivalente a --format=custom). Ele vem comprimido por padrão e, diferente de um dump SQL puro, é o que o pg_restore espera, o que te dá restaurações seletivas e um formato que o pg_restore pode paralelizar depois, se você precisar.

Adicione --no-owner e --no-acl pra que o dump não carregue instruções ALTER OWNER e GRANT/REVOKE ligadas a roles que podem não existir (ou ter nomes diferentes) onde você eventualmente for restaurá-lo, incluindo seu banco de verificação no Passo 5.

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

DB_NAME="myapp_production"
DB_USER="myapp"
BACKUP_DIR="/var/backups/postgres"
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
DUMP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump"

mkdir -p "${BACKUP_DIR}"

pg_dump -Fc --no-owner --no-acl \
  -U "${DB_USER}" -d "${DB_NAME}" \
  -f "${DUMP_FILE}"

if [ ! -s "${DUMP_FILE}" ]; then
  echo "pg_dump produced an empty file, aborting" >&2
  exit 1
fi

echo "Backed up ${DB_NAME} to ${DUMP_FILE}"

set -euo pipefail importa aqui: sem ele, um pg_dump que falha ainda pode deixar o script seguir e reportar sucesso. A checagem [ ! -s "${DUMP_FILE}" ] pega o caso específico em que o pg_dump sai limpo, mas o arquivo que ele escreveu está vazio, o que acontece mais do que você imagina quando o disco de destino está cheio.

Você deve ver um arquivo novo em /var/backups/postgres com um tamanho que acompanha, grosso modo, o tamanho real do seu banco (um dump em formato customizado é comprimido, então não espere uma correspondência 1:1, mas um banco de 500 MB gerando um arquivo de 2 KB é sinal de que algo está errado).

Passo 2: agendar com cron

Os campos do crontab vêm nesta ordem: minuto, hora, dia do mês, mês, dia da semana. Uma execução noturna às 2h da manhã fica assim:

0 2 * * * /usr/local/bin/pg_backup.sh >> /var/log/pg_backup.log 2>&1

Dois detalhes que causam a maioria dos incidentes do tipo “meu backup parou silenciosamente”:

  • O cron roda com um ambiente mínimo, não com o do seu shell interativo. Se seu script chama pg_dump, aws ou rclone pelo nome e depende do PATH do seu shell pra encontrá-los, o PATH reduzido do cron pode não incluir o diretório onde eles estão. Use caminhos absolutos dentro do script (/usr/bin/pg_dump, /usr/local/bin/aws) ou defina o PATH explicitamente no topo do script.
  • Redirecione tanto stdout quanto stderr pra um arquivo de log (o >> ... 2>&1 acima). Sem isso, o cron manda falhas por e-mail pra uma caixa que ninguém lê, ou simplesmente descarta elas dependendo da configuração de e-mail do seu sistema, e você só descobre quando uma restauração falha.

Você deve ver uma linha nova sendo adicionada em /var/log/pg_backup.log todas as noites, e um novo arquivo .dump em /var/backups/postgres com um timestamp recente. Se um dia passar sem arquivo novo e sem linha nova no log, o cron simplesmente não rodou o job, o que é uma falha diferente de o job rodar e falhar.

Passo 3: enviar o dump pra fora do servidor

Um dump sentado no mesmo disco do banco que ele protege não te defende da falha que mais importa: perder o servidor. Envie-o pra outro lugar imediatamente depois que ele é gravado.

Com a AWS CLI, contra o S3 ou qualquer endpoint compatível com S3:

aws s3 cp "${DUMP_FILE}" "s3://my-backups-bucket/postgres/" --only-show-errors

Pra um provedor compatível com S3 que não seja a AWS, adicione --endpoint-url:

aws s3 cp "${DUMP_FILE}" "s3://my-backups-bucket/postgres/" \
  --endpoint-url "https://nyc3.digitaloceanspaces.com" \
  --only-show-errors

Ou com rclone, depois de configurar um remote uma vez com rclone config:

rclone copy "${DUMP_FILE}" "myremote:my-backups-bucket/postgres/"

Acrescente o comando que escolher ao final do seu script de backup, e cheque o código de saída dele do mesmo jeito que você checou o do pg_dump:

if ! aws s3 cp "${DUMP_FILE}" "s3://my-backups-bucket/postgres/" --only-show-errors; then
  echo "upload to S3 failed for ${DUMP_FILE}" >&2
  exit 1
fi

Você deve ver o objeto aparecer no seu bucket (aws s3 ls s3://my-backups-bucket/postgres/ ou o equivalente no painel do seu provedor) poucos instantes depois do cron terminar, com um tamanho igual ao do arquivo local.

Passo 4: descartar backups antigos

Sem uma política de retenção, seu diretório de backup e seu bucket crescem pra sempre. Com uma política agressiva demais, você pode acabar descartando tudo até sobrar zero backup bem na hora que precisar de um. A regra mais segura tem duas partes: manter sempre um número mínimo de backups recentes, independente da idade, e, separadamente, apagar qualquer coisa mais velha que N dias que não esteja entre os que você guardou.

Localmente:

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

BACKUP_DIR="/var/backups/postgres"
KEEP_DAYS=30
KEEP_MIN=7

mapfile -t ALL_DUMPS < <(ls -1t "${BACKUP_DIR}"/*.dump 2>/dev/null)

for i in "${!ALL_DUMPS[@]}"; do
  if [ "$i" -lt "${KEEP_MIN}" ]; then
    continue
  fi
  file="${ALL_DUMPS[$i]}"
  if [ -n "$(find "${file}" -mtime "+${KEEP_DAYS}")" ]; then
    rm -f "${file}"
    echo "pruned ${file}"
  fi
done

ls -1t lista os dumps do mais novo pro mais antigo, então o loop pula incondicionalmente os KEEP_MIN arquivos mais recentes e só apaga os mais antigos que também falham na checagem de idade. Isso significa que uma semana tranquila, sem nada novo sendo criado, nunca te deixa com menos do que KEEP_MIN backups.

Pra cópia remota, a maioria dos provedores compatíveis com S3 suporta uma regra de lifecycle no bucket que expira objetos depois de N dias, o que é mais simples do que escrever um script de descarte remoto você mesmo; defina um valor pelo menos tão longo quanto KEEP_DAYS, pra nunca descartar a única cópia fora do servidor que você tem.

Rode isso como uma segunda entrada no cron, separada do script de backup, pra um bug no descarte não rodar acidentalmente dentro do mesmo script que está tentando criar um backup novo:

30 2 * * * /usr/local/bin/pg_prune.sh >> /var/log/pg_backup.log 2>&1

Você deve ver arquivos .dump antigos desaparecerem de /var/backups/postgres com o tempo, enquanto pelo menos KEEP_MIN backups recentes continuam lá sempre, e uma linha “pruned …” no log sempre que um é removido.

Passo 5: restaurar e verificar, numa agenda fixa

Essa é a etapa que praticamente todo mundo pula, e é justamente a que importa de verdade. Um arquivo de dump existir e ter tamanho maior que zero te diz que o pg_dump rodou. Não te diz nada sobre se os dados ali dentro estão intactos, se o formato é válido, ou se uma restauração realmente funcionaria quando você precisasse dela. A única forma de saber que um backup está bom é restaurá-lo e checar o que voltou.

Automatize isso contra um banco descartável no mesmo servidor (ou num separado, se você preferir não adicionar carga de restauração à produção):

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

BACKUP_DIR="/var/backups/postgres"
LATEST_DUMP="$(ls -1t "${BACKUP_DIR}"/*.dump | head -n1)"
VERIFY_DB="restore_check_$(date +%Y%m%d)"
MIN_EXPECTED_ROWS=1

createdb "${VERIFY_DB}"

pg_restore --no-owner --no-acl -d "${VERIFY_DB}" "${LATEST_DUMP}"

ROW_COUNT="$(psql -d "${VERIFY_DB}" -tAc "SELECT count(*) FROM users;")"

dropdb "${VERIFY_DB}"

if [ "${ROW_COUNT}" -lt "${MIN_EXPECTED_ROWS}" ]; then
  echo "restore verification failed: ${ROW_COUNT} rows in users table" >&2
  exit 1
fi

echo "restore verified: ${LATEST_DUMP}, ${ROW_COUNT} rows in users table"

Troque users pela tabela em que você consegue afirmar com segurança uma contagem mínima de linhas; o ponto da query não é ser exaustiva, é provar que a restauração produziu um banco funcional com dados reais dentro, não só um banco que existe. O próprio pg_restore também vai falhar de forma bem visível num dump genuinamente corrompido ou truncado, então um código de saída diferente de zero só na etapa de restauração já é um sinal útil, separado da checagem de contagem de linhas.

Agende isso depois do seu job de retenção, e com uma frequência menor que o backup em si é razoável, semanal é um ponto de partida sensato pra maioria das equipes:

0 3 * * 0 /usr/local/bin/pg_restore_check.sh >> /var/log/pg_backup.log 2>&1

Você deve ver uma linha restore verified: ... no log toda semana, com uma contagem de linhas que faz sentido pros seus dados. Se essa linha parar de aparecer, ou começar a reportar uma contagem suspeitosamente baixa, você pegou um backup ruim enquanto ainda tem backups antigos bons pra cair de volta, em vez de descobrir isso durante uma queda de verdade.

O caminho mais curto: fazer isso com o Railyard

Tudo acima é a versão honesta, feita do zero. O Railyard roda esse mesmo ciclo pra você em todo banco Postgres que ele gerencia, sem os scripts.

Quando você provisiona um banco num servidor gerenciado pelo Railyard, ele agenda os próprios backups automaticamente num intervalo que você pode ajustar por banco (um dia é um padrão sensato pra maioria das aplicações). Cada backup é verificado pelo Railyard de fato restaurando-o e checando o resultado, não só checando que um arquivo existe, então você consegue ver num piscar de olhos se seu backup mais recente está bom em vez de simplesmente assumir que está. Restaurar é um clique a partir da página do banco, sem script pra lembrar e sem banco descartável pra limpar à mão depois. O descarte é tratado do mesmo jeito que você mesmo trataria: o Railyard mantém um número configurável dos seus backups bem-sucedidos mais recentes e remove qualquer coisa mais velha que um número configurável de dias que não esteja entre os que foram mantidos, então você nunca fica a um descarte ruim de distância do zero.

Pro Postgres especificamente, você também pode ativar a recuperação point-in-time por servidor, direto na página do servidor, o que complementa os dumps agendados: em vez de só conseguir restaurar pro momento do seu último backup, você consegue recuperar pra qualquer momento dentro da janela de recuperação. Mais sobre tudo isso, incluindo como os bancos são provisionados e conectados, está em Databases and backups.

Se você está começando uma aplicação nova em vez de encaixar backups numa já existente, o quickstart e fazer deploy a partir de um repositório cobrem como colocar a aplicação e o banco dela rodando num servidor, do zero.

Resolução de problemas

O cron job rodava e agora não roda mais, sem erro nenhum em lugar nenhum. Isso é quase sempre o problema de PATH do Passo 2: um script que funcionava bem quando testado interativamente falha silenciosamente sob o ambiente mínimo do cron, porque pg_dump, aws ou rclone não estão onde o cron espera encontrá-los. Use caminhos absolutos pra cada binário que o script chama, ou defina o PATH explicitamente no topo do script, e confirme que o job realmente está disparando checando o timestamp do arquivo de log, não só a ausência de erro.

O arquivo de dump existe, mas está vazio ou truncado. A causa mais comum é o disco de destino enchendo no meio da escrita, principalmente se o diretório de backup compartilha disco com o próprio diretório de dados do banco ou com um arquivo de log que cresce sem parar. O pg_dump pode sair com status diferente de zero nesse caso, e é exatamente por isso que o script checa o código de saída e o tamanho do arquivo separadamente em vez de confiar só em um dos dois.

Ninguém nunca testou uma restauração, e os backups estão quebrados silenciosamente há meses. Esse é o cenário que o Passo 5 existe pra prevenir. Um dump corrompido ou incompleto pode ficar sentado no seu bucket parecendo exatamente igual a um saudável enquanto ninguém tenta usá-lo. Agendar o job de restaurar-e-verificar, não só documentar como algo pra fazer manualmente algum dia, é o que de fato fecha essa brecha.

As credenciais do destino fora do servidor expiraram ou foram rotacionadas. Uma access key do S3 ou as credenciais de um remote do rclone podem expirar ou ser revogadas independente de qualquer coisa no servidor Postgres, e a etapa de upload vai começar a falhar enquanto o dump local continua sendo gerado com sucesso. Checar o código de saída do comando de upload (Passo 3) pega isso imediatamente, em vez de semanas depois, quando você for procurar uma cópia fora do servidor que nunca foi de fato escrita.

Perguntas frequentes

Com que frequência os backups devem rodar? Depende de quanto dado você pode se dar ao luxo de perder, seu recovery point objective. Diário é um padrão razoável pra maioria das aplicações. Se seus dados mudam rápido o suficiente pra que perder até um dia deles seja um problema de verdade, rode o pg_dump com mais frequência ou adicione arquivamento de WAL pra recuperação point-in-time entre os dumps.

O pg_dump é suficiente, ou eu preciso de arquivamento de WAL e recuperação point-in-time? O pg_dump te dá um snapshot limpo e consistente exatamente no momento em que rodou. O arquivamento contínuo de WAL (archive_mode, archive_command e base backups periódicos com pg_basebackup) permite reaplicar mudanças até qualquer momento específico usando recovery_target_time, então você consegue recuperar pra um instante antes de uma migration ruim ou de um delete acidental, em vez de só voltar ao dump da noite passada. Pra maioria das equipes, backups agendados com pg_dump cobrem o caso comum (perda do servidor, corrupção), e vale a pena adicionar recuperação point-in-time quando perder até algumas horas de dados entre dumps já te machucaria de verdade.

Como eu sei que um backup está realmente bom? Só uma restauração bem-sucedida prova isso. Um arquivo existir, ter um tamanho razoável, e o pg_dump sair com status 0 são todos necessários, mas não suficientes; o único teste real é restaurar em algum lugar e checar se os dados que voltaram são o que você esperava, o que é exatamente o que o Passo 5 automatiza.

Onde a cópia fora do servidor deveria realmente viver? Em qualquer lugar que não seja o mesmo disco, de preferência nem o mesmo provedor ou a mesma região do servidor do banco, pra que uma única queda de provedor ou um problema de conta não tire do ar seu banco e os backups dele ao mesmo tempo.

Eu preciso criptografar os arquivos de dump? Se o seu destino de armazenamento de objetos suporta criptografia do lado do servidor em repouso, ativar isso não custa nada e vale a pena. Se você quer criptografia independente do provedor de armazenamento, pode passar o dump pelo gpg antes de enviá-lo, embora isso adicione uma chave que agora você também precisa guardar com segurança, o que é um tipo de risco próprio que vale pensar antes de adicionar.

Um backup que você nunca restaurou é uma crença, não um fato. Os cinco passos aqui transformam essa crença em algo que você de fato verifica numa agenda fixa, pra que a primeira vez que você restaurar a partir de um backup não seja também a primeira vez que você descobre se ele funciona.

Por que escolhem o Railyard para isso

  • Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
  • Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
  • Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
  • Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
  • Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.

Deixe o Railyard rodar todo o ciclo de backup pra você.

Conecte um servidor e todo banco Postgres nele passa a ter backups agendados, com restauração verificada e recuperação em um clique, sem script pra manter.

Plano grátis, sem cartão, e 14 dias de Pro para cada equipe nova. Um preço fixo por equipe.