PostgreSQL: Backups Fiáveis com pg_dump e pgBackRest

O OpenProject, o Zabbix e o Gitea da PME partilham um ponto único de falha: a base de dados PostgreSQL. O servidor faz RAID, o snapshot do hypervisor corre todas as noites — e nenhuma das duas coisas cobre um DROP TABLE executado às 14h de terça-feira. A resposta do PostgreSQL é dupla: os dumps lógicos do pg_dump, que servem a maioria das PMEs, e o pgBackRest quando o histórico, os incrementais e o point-in-time recovery entram na equação. Este guia configura os dois e termina com o teste de restauro que valida tudo.

⚠️ Antes de aplicar em produção

Este guia altera backups e inclui passos de restauro que apagam dados. Segue esta ordem:
1. Servidor de testes primeiro. Executa o guia completo, incluindo o restauro e o teste da secção 6. Só avanças com o teste a passar.
2. Backup independente. Antes de tocares em produção, garante uma cópia actual que não dependa das ferramentas deste artigo (snapshot do hypervisor ou dump feito noutra máquina).
Se não conseguires cumprir as duas fases hoje, adia. A base de dados de produção não é sítio para experimentar.

Neste artigo

  1. Dois tipos de backup
  2. pg_dump: o backup lógico
  3. pg_basebackup: o backup físico
  4. pgBackRest: incrementais e PITR
  5. Agendamento com cron
  6. O teste que ninguém salta
  7. Erros Comuns
  8. Checklist
  9. Artigos Relacionados
  10. Fontes Oficiais

1. Dois tipos de backup

O PostgreSQL oferece dois métodos. O dump SQL (pg_dump) gera comandos SQL que, recarregados, recriam a base no estado em que estava — é uma aplicação cliente normal, pode correr de qualquer host com acesso, e funciona independentemente da versão do servidor, o que o torna o caminho de migração entre versões. O backup a nível de ficheiros (pg_basebackup) copia os ficheiros do cluster em execução, sem afectar os outros clientes, e é a base do point-in-time recovery.

A regra prática: um pg_dump -Fc diário cobre o cenário de desastre comum, e o pgBackRest entra quando o volume de dados cresce — incrementais ao nível de bloco e PITR tornam-se mais valiosos do que o dump completo diário.

Atenção

O pg_dump não tem permissões especiais — corre como qualquer utilizador com permissão de leitura à base. O pg_dumpall, em contraste, exige acesso de superutilizador para restaurar, porque captura roles e tablespaces de todo o cluster.

2. pg_dump: o backup lógico

O uso básico escreve SQL em texto:

pg_dump openproject > openproject.sql

O que o comando faz, peça a peça: pg_dump é a ferramenta de backup lógico, openproject é o nome da base a copiar — o único argumento do comando, e é através dele que o pg_dump sabe a que base ligar — e > openproject.sql envia a saída do stdout para o ficheiro openproject.sql. Só está lá o nome da base porque o resto é por omissão: a máquina é a local e o utilizador é o que corre o comando (no OpenProject, tipicamente o utilizador do próprio serviço). Se a base ficar noutro servidor, o comando ganha duas flags que o exemplo não mostra — pg_dump -h host -U utilizador openproject > openproject.sql — e a autenticação segue a regra do psql: se pedir password, pede também ao pg_dump, ou lê-a do .pgpass. O resultado é texto plano legível — instruções SQL, SET, comentários e as ordens COPY dos dados — por isso se inspeciona até com less, e restaura-se com psql openproject < openproject.sql.

Para bases com mais que uns poucos GB, o formato custom comprime como gzip e permite restauro selectivo de tabelas:

pg_dump -Fc openproject > openproject.dump

A flag -Fc é -F de format mais c de custom: em vez de SQL em texto, produz um ficheiro comprimido com catálogo interno — o ficheiro não se lê com editor de texto nem restaura com psql, e é exactamente por isso o ficheiro fica bem menor e o restauro pode escolher só o que interessa (uma tabela em particular, por exemplo) com pg_restore --table nome.

O restauro de um dump custom não passa pelo psql — usa o pg_restore:

pg_restore -d openproject openproject.dump

O -d openproject diz a base de destino (que tem de existir antes, criada com createdb openproject ou equivalente — o pg_restore não a cria por si, a menos que se acrescente --create). O último argumento é o ficheiro .dump do backup. Sem o par --clean --if-exists, nada é substituído numa base que já tem o mesmo esquema (ver a caixa seguinte): o pg_restore junta erros "already exists" e duplica as linhas das tabelas.

⚠️ O restauro substitui os dados actuais

Sem flags extra, o pg_restore não substitui nada: cada CREATE encontra o objecto que já existe, os erros do tipo "already exists" sucedem-se e as linhas de cada COPY ficam por cima das que já lá estavam — o resultado é uma mistura dos dois estados. Para restaurar por cima de uma base existente, o comando leva o par --clean --if-exists: o --clean emite um DROP antes de cada CREATE (a base fica no estado exacto do dump, incluindo apagar o que não está nele) e o --if-exists evita que o primeiro DROP falhe quando o objecto não existe. Antes de restaurar por cima de produção, confirma por dois lados: que o backup é o ficheiro certo (pg_restore --list openproject.dump mostra a data e o conteúdo) e que não há dados de hoje que só existam na base activa. Se a dúvida existir, restaura para uma base nova (createdb openproject_teste + pg_restore -d openproject_teste openproject.dump) e compara antes de tocar na original.

Duas propriedades úteis do formato custom: o pg_restore pode restaurar em paralelo com -j N, e o catálogo do dump permite seleccionar objectos.

Razão de ser do pg_dumpall: o pg_dump é por base — não apanha roles (utilizadores do cluster), tablespaces nem as outras bases do servidor. O pg_dumpall > dumpfile corre em todo o cluster e escreve SQL de tudo, e é por isso que o restauro volta ao psql: psql -X -f dumpfile postgres executa esse SQL (o -X ignora o .psqlrc do utilizador, e -f lê o ficheiro — ligando à base postgres, que sempre existe).

O dump captura o estado no momento do início, e as alterações posteriores ficam de fora. É o modelo certo para bases de PME onde perder algumas horas de dados é aceitável, e errado para quem precisa de voltar a um minuto.

3. pg_basebackup: o backup físico

O pg_basebackup tira uma cópia dos ficheiros do cluster em execução, sem parar o serviço:

pg_basebackup -h 127.0.0.1 -U backup -D /backup/base -F tar -X stream -P

Cada parâmetro do comando:

  • -h 127.0.0.1 — o host onde corre o servidor PostgreSQL (aqui a própria máquina por loopback). É opcional quando o pg_basebackup corre local: sem ele, liga via socket Unix local.
  • -U backup — o utilizador PostgreSQL com que se liga. Tem de ser um role com permissão de REPLICATION (o artigo assume um role criado para o efeito e autorizado no pg_hba.conf, como indicado mais abaixo).
  • -D /backup/base — o directório de destino onde o backup é escrito. Tem de estar vazio ou não existir (o pg_basebackup não escreve sobre conteúdo) — é a pasta que os exemplos de restauro desta secção usam para extrair o base.tar.
  • -F tar — o formato da saída: um ficheiro base.tar com os ficheiros do cluster (o restauro com tar depende deste formato).
  • -X stream — copia os segmentos WAL gerados durante o backup por uma segunda ligação em paralelo, de modo que o backup fique consistente. Em formato tar, este WAL vai para um ficheiro pg_wal.tar separado do base.tar — o restauro só fica consistente se os dois forem extraídos.
  • -P — ativa a barra de progresso (progress).

A relação com o PITR: este backup é um instantâneo do cluster num momento fixo — sozinho, só devolve esse momento. Como os segmentos WAL registam cada alteração posterior, aplicar WAL sobre o base é o que leva o restauro até qualquer segundo antes do incidente (o point-in-time recovery), e é esse caminho que a secção 4 cobre com o pgBackRest.

Em comparação com o dump: mais rápido para bases grandes, mas só restaura a base inteira — não objectos individuais — e a cópia tem de ser da mesma versão maior do PostgreSQL. O acesso do utilizador de backup passa pelo pg_hba.conf (replication por IP do servidor de backup).

Para restaurar este backup, a sequência é. O 17 é a versão maior do PostgreSQL e o demo é o nome do cluster — o nome que pg_createcluster 17 demo deu quando o cluster foi criado, e que o pg_lsclusters lista (no Ubuntu/Debian cada par versão+nome tem o seu directório em /var/lib/postgresql/<versão>/<nome> e o seu postgresql.conf em /etc/postgresql/<versão>/<nome>/). Numa instalação real o nome é o que tu escolheste — substitui demo pelo teu:

# parar o cluster
sudo pg_ctlcluster 17 demo stop

# limpar o directório de dados actual
rm -rf /var/lib/postgresql/17/demo/*

# extrair o base.tar do backup para lá
tar -xf /backup/base/base.tar -C /var/lib/postgresql/17/demo

# extrair o WAL que acompanha o backup (pg_wal.tar)
tar -xf /backup/base/pg_wal.tar -C /var/lib/postgresql/17/demo/pg_wal

# reposicionar a propriedade e arrancar
chown -R postgres:postgres /var/lib/postgresql/17/demo
sudo pg_ctlcluster 17 demo startdemo start

⚠️ O “rm -rf” do bloco acima apaga os dados actuais

O comando rm -rf /var/lib/postgresql/17/demo/* é o passo 2 da sequência — é ele que elimina o directório de dados onde o PostgreSQL guarda todas as bases. Confirma o caminho antes de o correr: sem o espaço entre demo e /*, o padrão fica confinado ao directório do cluster — com um espaço, o shell entrega dois argumentos ao rm e o segundo apaga o conteúdo da raiz do sistema. A partir desse momento, o que o cluster tinha já não está em disco: se o backup de onde vais restaurar estiver estragado ou for antigo demais, aquele conteúdo não volta. Antes de correr o rm, confirma por dois lados: que o backup serve (tar -tf /backup/base/base.tar mostra a data e o conteúdo — neste artigo o backup foi tirado pelo pg_basebackup para /backup/base) e que há espaço em disco para a extracção. Por ser tudo-ou-nada, não há restauro parcial por este caminho — para recuperar um único objecto, usa antes um dump lógico (secção 2) ou o PITR da secção 4.

4. pgBackRest: incrementais e PITR

O pgBackRest (v2.59.2) acrescenta o que os dumps não têm: backups full, differential e incremental ao nível de ficheiro ou de bloco, retenção automática, compressão, repositórios múltiplos (local rápido + S3 remoto), encriptação client-side, e PITR real via arquivação contínua de WAL. Nota de contexto: a 27 de Abril de 2026 o projecto anunciou que deixava de ser mantido, e a 18 de Maio uma coligação de patrocinadores assegurou a continuação — o desenvolvimento retomou (a 2.59.2 saiu a 27 de Setembro).

Instalação e caminhos padrão (Ubuntu/Debian):

sudo apt install pgbackrest
sudo mkdir -p -m 770 /var/log/pgbackrest
sudo chown postgres:postgres /var/log/pgbackrest
sudo touch /etc/pgbackrest/pgbackrest.conf
sudo chmod 640 /etc/pgbackrest/pgbackrest.conf
sudo chown postgres:postgres /etc/pgbackrest/pgbackrest.conf

Configuração mínima — /etc/pgbackrest/pgbackrest.conf — a stanza é o nome lógico que o pgBackRest dá a um cluster PostgreSQL que passa a vigiar — escolhe-se na secção [demo] do ficheiro (aqui chamo-lhe demo — usa o nome do teu cluster, o mesmo de pg_lsclusters) e o pg1-path é o directório de dados dele (aqui PostgreSQL 17 — ajusta à tua versão):

[demo]
pg1-path=/var/lib/postgresql/17/demo

[global]
repo1-path=/var/lib/pgbackrest
repo1-retention-full=2

O PostgreSQL tem de arquivar WAL para o pgBackRest funcionar — em /etc/postgresql/17/demo/postgresql.conf:

archive_command = 'pgbackrest --stanza=demo archive-push %p'
archive_mode = on

Com o serviço reiniciado, criar a stanza e validar:

sudo -u postgres pgbackrest --stanza=demo --log-level-console=info stanza-create
sudo -u postgres pgbackrest --stanza=demo check

O check valida o archive_command e a configuração — o teste que evita backups incompletos por WAL que nunca chega ao repositório. O primeiro backup:

sudo -u postgres pgbackrest --stanza=demo backup

E o restauro (parar o cluster, limpar, restaurar, arrancar):

sudo pg_ctlcluster 17 demo stop
sudo -u postgres pgbackrest --stanza=demo restore
sudo pg_ctlcluster 17 demo start

As opções do PITR acrescentam-se ao comando de restauro. O --type=time --target="2026-09-28 14:30:00+01" distingue um restauro comum (voltar ao fim do WAL arquivado) de um restauro a um instante escolhido. O --target-action=promote manda o PostgreSQL promover o cluster ao atingir o alvo — sem ele, o PostgreSQL atinge o instante e para em modo só de leitura, à espera de decisão. E o --delta tolera um directório de dados não vazio: em vez de falhar, compara ficheiro a ficheiro e só escreve o que difere do backup (o aviso do directório não vazio desaparece). Nota: o backup restaurado é o mais recente anterior ao instante alvo — é o PostgreSQL que depois faz o salto até à hora pedida com o WAL.

sudo pg_ctlcluster 17 demo stop
sudo pg_ctlcluster 17 demo stop
sudo -u postgres pgbackrest --stanza=demo --type=time --target="2026-09-28 14:30:00+01" --target-action=promote --delta restore
sudo pg_ctlcluster 17 demo start

O que acontece por baixo: o pgBackRest primeiro devolve os ficheiros do último backup full (o mesmo mecanismo do restauro simples da sequência acima, com o seu rm do directório de dados já avisado) e depois, em vez de parar aí, aplica os segmentos WAL arquivados — aqueles que o archive_command da configuração envia para o repositório cada vez que um segmento enche (o archive_timeout define quanto tempo um segmento pode ficar aberto antes de ser enviado ainda pela metade) — um segmento a um segmento, até o relógio das transacções atingir o instante pedido. O resultado é uma base no estado exacto de 2026-09-28 às 14h30: um DROP TABLE às 14h22 desfaz-se restaurando para as 14h21, coisa que nenhum dump de madrugada consegue. É esta capacidade que justifica o pgBackRest em vez de dumps sozinhos.

5. Agendamento com cron

Uma agenda comprovada: full ao domingo e diferenciais no resto da semana. Nota que, instalado no meio da semana, o primeiro backup diferencial corre automaticamente como full:

30 06 * * 0 pgbackrest --stanza=demo backup
30 06 * * 1-6 pgbackrest --stanza=demo --type=diff backup

O repo1-retention-full=2 que a configuração da secção 4 definiu no /etc/pgbackrest/pgbackrest.conf é a regra de expiração que decide o que sobra no repositório: mantém os dois fulls mais recentes, e quando um terceiro termina o mais antigo é expirado, levando consigo os diferenciais e incrementais que dependiam dele. Sem esta linha na config, o repositório cresce indefinidamente — o cron não define retenção nenhuma, por isso a regra de expiração não pode existir senão na config.

Monitorizar execução — ver o estado no momento

O info dá o estado de cada stanza em JSON, aproveitável por qualquer monitorização. O estado geral da stanza vive no objecto status — o message interno traz ok em funcionamento normal e mixed quando há mais de um repositório e um deles não está saudável:

pgbackrest --stanza=demo info --output=json | jq '.[0] .status.message'

A data do último backup que a stanza teve sucesso e o último WAL arquivado — a dupla que responde "o backup correu ontem?":

pgbackrest --output=json --stanza=demo info | jq '.[0] | .backup[-1] | .timestamp.stop'
pgbackrest --output=json --stanza=demo info | jq '.[0] | .archive[-1] | .max'

Alertas — falha de full, diferencial e incrementais

O pgBackRest não tem alertas próprios: quem corre os backups é o cron — edita o crontab do utilizador postgres (crontab -e -u postgres), não o do root — e é nele (ou na monitorização que já existe) que os alertas se anexam. O padrão mais simples: cada linha do cron executa o backup e — só se o comando falhar — envia o alerta com o output do erro, via mail local ou webhook (o /usr/local/bin/backs-notify dos exemplos é um script exemplo para criares com o canal que usares — mail, webhook, Zabbix):

30 06 * * 0 pgbackrest --stanza=demo backup || /usr/local/bin/backs-notify "FALHA full $(hostname)"
30 06 * * 1-6 pgbackrest --stanza=demo --type=diff backup || /usr/local/bin/backs-notify "FALHA diff $(hostname)"

Este alerta apanha um full falhado (a 1.ª linha) e um diferencial falhado (2.ª). Para incrementais consecutivos sem full: os incrementais têm de partir de um backup base válido — se um full e o seu diferencial falharem duas semanas seguidas, o info continua a mostrar o último backup bem sucedido, cujo timestamp.stop fica cada vez mais velho. Um alerta de “backup não corre há N dias” apanha o caso que as duas linhas de cima não apanham — a monitorização passa a ser pela idade do último backup bem sucedido, em vez da execução isolada de cada comando:

pgbackrest --output=json --stanza=demo info | jq '.[0] | .backup[-1] | .timestamp.stop'
# compara com o date +%s — distância > 7 dias = alerta

ℹ️ Nota: Se a organização já corre Zabbix, Prometheus ou Monitorix, o info --output=json alimenta-os directamente (o output em JSON é estável entre versões) e as views de monitorização são exemplo da documentação — a função monitor.pgbackrest_info() da doc do pgBackRest não vem instalada: é código para criar no teu servidor a partir do que a doc fornece — e dão as mesmas respostas em SQL. O check (secção 4) permanece a verificação activa que valida o archive_command e a configuração sempre que corre — mas só corre quando alguém o executa ou o agenda: sem agendamento próprio, não é um vigilante contínuo.

6. O teste que ninguém salta

Um backup não verificado é uma esperança, não um backup. O ficheiro pode estar completo no repositório e ainda assim o restauro falhar — por permissões, versão do PostgreSQL, WAL em falta, ou a própria corrupção que o backup copiou fielmente. O teste valida a cadeia inteira: restaurar a última cópia numa máquina descartável, arrancar o PostgreSQL, e verificar que a aplicação liga e os dados de ontem lá estão. Mensal, no calendário, como qualquer outro trabalho de manutenção.

O restauro de teste, numa VM descartável

A VM nova instala o pgBackRest e aponta para o repositório — a mesma configuração da secção 4, com o repositório montado só de leitura para o teste não poder tocar nas cópias reais. No Ubuntu/Debian:

sudo apt install pgbackrest postgresql-17
# /etc/pgbackrest/pgbackrest.conf — igual à secção 4
[demo]
pg1-path=/var/lib/postgresql/17/demo

[global]
repo1-path=/mnt/repo-pgbackrest
repo1-retention-full=2

O cluster da VM de teste tem de estar parado e o seu directório de dados vazio antes do restore — o pgBackRest restaura por cima, e o aviso da secção 3 aplica-se na mesma (aqui é irrelevante: a VM é descartável). O restore com --set escolhe o backup a restaurar: o label completo aparece no info da stanza (ex.: 20260927-101911F para um full, 20260927-101911F_20260927-101914D para um diferencial que parte dele). Sem --set, o pgBackRest escolhe o mais recente:

sudo systemctl stop postgresql
sudo -u postgres pgbackrest --stanza=demo --set=20260927-101911F_20260927-101914D \
--log-level-console=info restore
sudo systemctl start postgresql

A confirmação final é o que fecha o teste — ligar à base e ver os dados que deviam lá estar:

sudo -u postgres psql -c "SELECT count(*) FROM users;"

Aqui users é uma tabela do esquema do OpenProject — no Zabbix seria hosts e no Gitea user. Substitui pela tabela da tua aplicação. O número devolve igual ao que a aplicação tinha na véspera do teste. A ligação da aplicação apontada à VM de teste (ou um dump do restaurado comparado com o de produção) também serve — o que não serve é declarar o backup válido sem arrancar um PostgreSQL que leia dados reais dele.

⚠️ Nunca testes o restauro sobre o cluster de produção

O teste corre na VM descartável — nunca com o pg_ctlcluster 17 demo stop seguido de restore no servidor activo. O restauro da secção 3 e da secção 4 limpa o directório de dados: numa VM de teste essa é a intenção, em produção é um incidente. Se um dia o restauro for para valer, a sequência é a da secção 4 — e a degradação do serviço faz parte dela.

Com a regra 3-2-1 (três cópias, dois suportes, uma fora de casa), o repositório do pgBackRest num bucket S3 fecha o círculo: dois locais no próprio servidor não são backup.

Erros Comuns

Erro Causa provável Correcção
check falha com WAL ausente archive_command errado ou archive_mode off Corrigir postgresql.conf e reiniciar o cluster
Backup incremental avisou “changed to full” Sem backup full prévio Normal no primeiro backup; agendar o full
Restauro avisa “directório não vazio” Ficheiros residuais no directório de dados Limpar por completo ou repetir com –delta
pg_dump interrompido a meio Sessão SSH terminada Correr dentro de tmux ou apontar a saída a ficheiro com nohup
Cron não executa o backup PATH do cron sem o pgBackRest Caminho completo no cron ou script wrapper

Checklist

  • [ ] pg_dump -Fc diário testado e restaurado uma vez
  • [ ] pg_basebackup ou pgBackRest para base física
  • [ ] pgBackRest com stanza criada e check sem erros
  • [ ] archive_command e archive_mode activos
  • [ ] Retenção repo1-retention-full definida (2 ou mais)
  • [ ] Cron agendado (full semanal + diferenciais)
  • [ ] Repositório remoto ou S3 para a regra 3-2-1
  • [ ] Teste de restauro executado e registado
  • [ ] Monitorização do info --output=json

Artigos Relacionados

Fontes Oficiais