tva
← Insights

Recuperação de Desastres para Serviços Auto-Hospedados: Nossa Estratégia de Backup

A auto-hospedagem oferece um controle que os serviços gerenciados não oferecem: você escolhe o hardware, a rede, a residência dos dados e a estrutura de custos. Mas na realidade, esse controle vem com uma responsabilidade que os serviços gerenciados lidam silenciosamente: quando algo dá errado, é totalmente seu problema. Para uma configuração de servidor único executando múltiplos serviços de produção – Supabase, um CRM, um DAM, um servidor Git – a estratégia de backup e recuperação não é uma preocupação secundária. É a diferença entre um incidente recuperável e uma perda de dados permanente.

Esta publicação documenta a estratégia de backup que utilizamos para um ambiente auto-hospedado de produção. Os serviços envolvidos são uma stack Supabase (PostgreSQL como repositório de dados primário), um CRM auto-hospedado (Twenty, que também roda PostgreSQL), um servidor Git Gitea e um sistema de Gerenciamento de Ativos Digitais. Todos eles rodam como contêineres Docker em um único VPS da Hetzner. A estratégia precisa contabilizar todos eles sem criar janelas de carga inaceitáveis ou dependências complexas de orquestração.

Por Que Um Único Servidor Eleva as Apostas

Uma configuração de servidor único não tem redundância integrada. Não há réplica de standby, nenhum failover de múltiplas zonas de disponibilidade, nenhuma restauração automática de banco de dados acionada por uma verificação de integridade. Quando o servidor falha – seja por falha de hardware, uma atualização mal feita, um problema de armazenamento ou um incidente de segurança – o único caminho de recuperação é um backup. Se o backup estiver incompleto, desatualizado ou não testado, o caminho de recuperação será limitado de acordo.

Esse é um modelo de ameaça diferente de um serviço de nuvem gerenciado, onde a falha de hardware é em grande parte invisível porque o provedor de infraestrutura lida com o failover. Em um único VPS, você planeja explicitamente para os modos de falha que os serviços gerenciados abstraem. O planejamento não é particularmente complexo, mas requer fazê-lo antes do incidente, não durante.

O Que Precisa de Backup

Cada serviço tem seu próprio perfil de dados. Entender quais dados são insubstituíveis e onde estão é o pré-requisito para projetar a estratégia de backup.

O Supabase armazena seus dados em uma instância PostgreSQL gerenciada pela stack do Supabase. O banco de dados contém todos os dados da aplicação, registros de usuários e os próprios metadados de autenticação e armazenamento do Supabase. Os buckets de armazenamento (uploads de arquivos) são armazenados em disco e precisam de tratamento separado do backup do banco de dados.

O Twenty CRM é suportado por sua própria instância PostgreSQL, separada do Supabase. Contém registros de contatos, dados de oportunidades e estado de fluxos de trabalho. Esse banco de dados é menor em volume, mas altamente sensível – perder dados do CRM é mais perturbador operacionalmente do que perder dados de aplicação que podem ser recriados.

O Gitea armazena repositórios em disco como objetos Git bare. Os dados do repositório são em princípio reconstruíveis a partir das estações de trabalho dos desenvolvedores (já que cada clone é um backup completo), mas os dados do rastreador de problemas, comentários de pull requests e configuração de equipe vivem apenas no banco de dados do Gitea e não estão presentes em nenhum clone. Tanto os objetos Git quanto o banco de dados do Gitea precisam de backup.

O DAM (Gerenciamento de Ativos Digitais) armazena arquivos originais, derivados processados e metadados. Os arquivos originais são a parte insubstituível; os derivados podem ser regenerados. Os metadados vivem em um banco de dados que registra relacionamentos de arquivos, tags e direitos de uso.

Orquestração do pg_dump

O pg_dump do PostgreSQL é a ferramenta certa para backups lógicos de banco de dados. Ele exporta uma representação SQL do banco de dados que pode ser restaurada para qualquer versão compatível do PostgreSQL, o que é mais portátil do que backups físicos (que requerem a mesma versão do PostgreSQL e layout binário).

O script de backup para cada instância PostgreSQL segue o mesmo padrão:

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

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="$1"
CONTAINER_NAME="$2"
BACKUP_DIR="/opt/backups/postgres"
OUTPUT_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"

mkdir -p "${BACKUP_DIR}"

docker exec "${CONTAINER_NAME}"   pg_dump -U postgres -d "${DB_NAME}"   | gzip -9 > "${OUTPUT_FILE}"

echo "Backup complete: ${OUTPUT_FILE} ($(du -sh "${OUTPUT_FILE}" | cut -f1))"

O set -euo pipefail no início é importante: faz o script sair imediatamente se qualquer comando falhar, incluindo comandos em um pipeline. Sem pipefail, um pg_dump com falha seguido de um gzip bem-sucedido produziria um arquivo comprimido contendo uma mensagem de erro, que a tentativa de restauração trataria como um backup corrompido.

A saída é comprimida com gzip -9 inline em vez de como uma etapa de pós-processamento. Isso mantém o espaço em disco pequeno e evita escrever um dump não comprimido em disco primeiro, o que importa em servidores onde o espaço livre é uma restrição gerenciada.

Agendamento Escalonado

Rodar todos os trabalhos de backup simultaneamente criaria uma janela de contenção de I/O que poderia degradar os serviços sendo copiados. A solução é o agendamento escalonado – cada trabalho de backup começa em um horário diferente, com espaçamento suficiente para permitir que o trabalho anterior seja concluído antes que o próximo comece.

O agendamento que utilizamos, expresso como entradas cron:

# Backup do PostgreSQL do Supabase — 02:00 diariamente
0 2 * * * /opt/scripts/pg-backup.sh supabase supabase-db

# Backup do PostgreSQL do Twenty CRM — 02:30 diariamente
30 2 * * * /opt/scripts/pg-backup.sh twenty twenty-db

# Backup do banco de dados do Gitea — 03:00 diariamente
0 3 * * * /opt/scripts/pg-backup.sh gitea gitea-db

# Backup do banco de dados do DAM — 03:30 diariamente
30 3 * * * /opt/scripts/pg-backup.sh dam dam-db

# Objetos de repositório do Gitea — 04:00 diariamente
0 4 * * * /opt/scripts/git-objects-backup.sh

# Arquivos de armazenamento (Supabase + originais do DAM) — 04:30 diariamente
30 4 * * * /opt/scripts/files-backup.sh

Os intervalos de 30 minutos entre os trabalhos são conservadores – a maioria dos backups de banco de dados é concluída em poucos minutos para bancos de dados desse tamanho. Mas os intervalos também consideram a etapa de upload que segue cada backup: o arquivo comprimido é enviado para o armazenamento de objetos antes que o próximo trabalho comece, de modo que o disco local não está acumulando vários dias de backups simultaneamente.

Upload para Armazenamento de Objetos e Retenção

Backups que existem apenas no mesmo servidor que os serviços que protegem não são backups – são snapshots que serão perdidos no mesmo incidente que destrói os dados que protegem. Todo backup deve ser enviado para um local de armazenamento separado antes que a cópia local possa ser considerada completa.

Usamos um provedor de armazenamento de objetos compatível com S3 (separado do provedor de VPS) e fazemos upload com rclone, que lida automaticamente com novas tentativas, transferências retomáveis e verificação:

rclone copy "${OUTPUT_FILE}" "backup-remote:tva-backups/postgres/${DB_NAME}/"   --checksum   --transfers 1   --log-level INFO

O sinalizador --checksum verifica a transferência usando checksums MD5 em vez de apenas hora de modificação e tamanho, o que detecta qualquer corrupção durante a transferência.

A retenção é aplicada por um trabalho de limpeza separado que roda semanalmente. A política de retenção é: backups diários retidos por sete dias, backups semanais retidos por quatro semanas, backups mensais retidos por seis meses. Isso fornece uma janela razoável para detectar perda de dados que não é imediatamente óbvia – uma linha corrompida introduzida há três semanas ainda pode ser recuperada de um backup semanal.

A limpeza de retenção usa rclone delete com um filtro na hora de modificação em vez de excluir por padrão de nome de arquivo, o que é mais confiável quando as convenções de nomenclatura de arquivo não são perfeitamente consistentes:

rclone delete "backup-remote:tva-backups/postgres/"   --min-age 7d   --include "*_daily_*"   --dry-run

O sinalizador --dry-run é usado durante o teste do trabalho de limpeza. Remova-o somente após confirmar que os padrões de filtro correspondem exatamente ao que deve ser excluído.

Testes de Restauração

Um backup que nunca foi testado é uma hipótese. A única maneira de saber que um backup é restaurável é restaurá-lo. Realizamos um teste de restauração mensal para cada banco de dados, usando um contêiner Docker temporário isolado da stack de produção:

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

BACKUP_FILE="$1"
TEST_CONTAINER="restore-test-$(date +%s)"

# Iniciar um contêiner PostgreSQL temporário
docker run -d   --name "${TEST_CONTAINER}"   -e POSTGRES_PASSWORD=testpass   -e POSTGRES_DB=testdb   postgres:15-alpine

# Aguardar o PostgreSQL estar pronto
sleep 5

# Restaurar o backup
zcat "${BACKUP_FILE}" | docker exec -i "${TEST_CONTAINER}"   psql -U postgres -d testdb

# Verificar se as contagens de linhas correspondem às expectativas
docker exec "${TEST_CONTAINER}"   psql -U postgres -d testdb   -c "SELECT schemaname, tablename, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;"

# Limpeza
docker stop "${TEST_CONTAINER}" && docker rm "${TEST_CONTAINER}"

A verificação de contagem de linhas não é exaustiva – verifica se as tabelas principais têm contagens de linhas plausíveis, não se cada linha está correta. Um teste mais completo executaria as próprias verificações de integridade da aplicação no banco de dados restaurado, mas a verificação de contagem de linhas detecta os modos de falha mais comuns: um backup truncado, uma restauração com falha que produziu um banco de dados vazio, ou uma incompatibilidade de versão que causou perda silenciosa de dados.

Como é a Recuperação Real

A estratégia de backup é tão útil quanto o procedimento de recuperação que a utiliza. O runbook de recuperação – escrito como um documento Markdown no repositório de configuração do servidor – documenta a sequência exata de etapas para restaurar cada serviço em um novo VPS.

A sequência para uma falha de servidor completo é: provisionar um novo VPS, instalar Docker e a stack de aplicações a partir do repositório de configuração, baixar os arquivos de backup mais recentes do armazenamento de objetos, restaurar cada banco de dados na ordem de dependência (Supabase primeiro, depois CRM, depois os outros), restaurar o armazenamento de arquivos, atualizar o DNS e verificar cada serviço. O tempo estimado para uma recuperação completa é inferior a duas horas se todos os backups estiverem atualizados e o runbook for seguido sem improvisação.

A palavra-chave nessa estimativa é “seguido sem improvisação.” O runbook deve ser específico o suficiente para que uma pessoa que nunca tocou no sistema antes possa executá-lo com sucesso. Os comandos devem ser copiáveis e colados, não descritos em prosa. Cada etapa deve ter uma verificação de confirmação. A ambiguidade em um runbook de recuperação é uma responsabilidade que se agrava sob o estresse de um incidente real.

Testamos o runbook anualmente realizando uma recuperação completa em um ambiente de staging. O teste detecta de forma confiável pelo menos uma etapa que mudou desde a última vez que o runbook foi escrito – uma versão de imagem Docker que mudou, uma chave de configuração que foi renomeada, uma variável de ambiente que foi adicionada. Detectar isso em um teste agendado é muito preferível a descobrir durante uma recuperação real.

Insights Relacionados

Artigos relacionados