Backup automático em Linux com cron e rsync: rotina diária local + offsite

Configure backup automático em servidores Linux usando cron e rsync, com retenção, log e cópia offsite por SSH. Guia técnico passo a passo.

Backup automatizado em servidor Linux não é luxo — é a única coisa entre você e uma noite ruim quando o disco falha, o filesystem corrompe ou alguém roda rm -rf no diretório errado. Apesar disso, muito sysadmin ainda terceiriza essa decisão pra “depois” e descobre na hora do incidente que o último snapshot é de seis meses atrás.

Este tutorial monta uma rotina diária de backup local com rsync (usando snapshots por hardlink pra economizar espaço) e replicação offsite por SSH, agendada com cron. Tudo em script idempotente, com log estruturado e retenção configurável. Foco em servidor Linux genérico (Debian/Ubuntu/Rocky/Alma) rodando como VPS — sem dependências exóticas, sem agente proprietário.

Tempo estimado de execução: 30-40 minutos pra configurar a primeira vez, depois roda sozinho. Restauração de arquivo individual leva segundos; restauração completa depende do volume de dados.

Pré-requisitos

O que você precisa antes de começar

Servidor Linux com acesso root (ou sudo completo), rsync instalado, cron ativo e um segundo servidor (ou storage remoto) acessível via SSH pra cópia offsite. Espaço em disco no destino local pelo menos 2x o tamanho do que será copiado, pra acomodar histórico de snapshots.

OS testado Ubuntu 24.04 / Debian 12
rsync mínimo 3.2.x
Espaço local >= 2x dataset
Destino offsite SSH com chave

Verifique versões antes de prosseguir:

rsync --version | head -n 1
systemctl status cron      # debian/ubuntu
systemctl status crond     # rhel/rocky/alma

Se rsync não estiver instalado: sudo apt install rsync (Debian/Ubuntu) ou sudo dnf install rsync (Rocky/Alma).

Estrutura de diretórios e estratégia

Antes de escrever script, defina o layout. A receita usa três níveis:

  • /backup/local/daily/YYYY-MM-DD/ — snapshots diários (mantidos 7 dias)
  • /backup/local/weekly/YYYY-WW/ — snapshots semanais (mantidos 4 semanas)
  • /backup/local/monthly/YYYY-MM/ — snapshots mensais (mantidos 6 meses)

Cada snapshot diário é uma cópia completa aparente, mas rsync --link-dest reaproveita os arquivos não alterados como hardlinks do dia anterior. Resultado: 30 dias de backup ocupam pouco mais que 1 cópia completa + deltas de mudanças.

A replicação offsite copia só o snapshot mais recente (current) pra um servidor remoto, criando lá a mesma estrutura de histórico se desejar (ou só substituindo a cópia mais nova).

Script de backup local

Esta seção monta o script principal que cron vai disparar todo dia. Ele faz snapshot incremental com hardlinks, rotaciona retenção e gera log.

01

Crie o diretório base e o usuário dedicado (opcional mas recomendado em servidor compartilhado):

sudo mkdir -p /backup/local/{daily,weekly,monthly}
sudo mkdir -p /var/log/backup
sudo chown -R root:root /backup /var/log/backup
sudo chmod 700 /backup

Permissão 700 garante que só root lê o conteúdo dos backups — proteção básica contra leitura indevida se outra conta for comprometida.

02

Crie o script /usr/local/sbin/backup-daily.sh com o conteúdo abaixo. Ele é idempotente: rodar duas vezes no mesmo dia não duplica nada.

#!/bin/bash
set -euo pipefail

# Configuração
SOURCE_DIRS="/etc /home /var/www /var/lib/mysql-dumps"
BACKUP_ROOT="/backup/local"
LOG_FILE="/var/log/backup/backup-$(date +%Y-%m).log"
RETENTION_DAILY=7
RETENTION_WEEKLY=4
RETENTION_MONTHLY=6

TODAY=$(date +%Y-%m-%d)
DEST="$BACKUP_ROOT/daily/$TODAY"
LATEST_LINK="$BACKUP_ROOT/daily/current"

# Função de log com timestamp
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

log "===== Início do backup $TODAY ====="

# Detecta snapshot anterior pra --link-dest
LINK_DEST_ARG=""
if [ -d "$LATEST_LINK" ]; then
    LINK_DEST_ARG="--link-dest=$(readlink -f "$LATEST_LINK")"
    log "Usando link-dest: $LINK_DEST_ARG"
fi

mkdir -p "$DEST"

# Sync com rsync
rsync -aHAX --delete \
    --numeric-ids \
    $LINK_DEST_ARG \
    $SOURCE_DIRS \
    "$DEST/" >> "$LOG_FILE" 2>&1

# Atualiza symlink 'current'
ln -snf "$DEST" "$LATEST_LINK"

log "Backup local concluído. Tamanho do snapshot:"
du -sh "$DEST" >> "$LOG_FILE" 2>&1

# Retenção: apaga snapshots diários antigos
find "$BACKUP_ROOT/daily" -maxdepth 1 -type d -name "20*" -mtime +$RETENTION_DAILY -exec rm -rf {} \;
log "Retenção diária aplicada (mantendo $RETENTION_DAILY dias)"

log "===== Fim do backup $TODAY ====="

Salve e dê permissão de execução:

sudo chmod 700 /usr/local/sbin/backup-daily.sh
03

Teste o script manualmente antes de agendar:

sudo /usr/local/sbin/backup-daily.sh

Verifique o log gerado:

tail -n 50 /var/log/backup/backup-$(date +%Y-%m).log

Confirme que o snapshot existe e tem conteúdo:

ls -lah /backup/local/daily/
du -sh /backup/local/daily/current/

Rode novamente em sequência. O segundo run deve ser muito mais rápido — rsync só transfere o que mudou e usa hardlink pra resto.

Atenção a dados em uso

Bancos de dados ativos (MySQL, Postgres) não devem ser copiados diretamente do diretório de dados — você corre risco de corromper o snapshot. Gere dump consistente antes do rsync (ex: mysqldump --single-transaction) pra um diretório que o rsync copia. O exemplo acima usa /var/lib/mysql-dumps justamente pra esse fluxo.

Replicação offsite por SSH

Backup local protege contra falha de aplicação ou erro humano. Não protege contra perda total do servidor (incêndio, hipervisor corrompido, acesso comprometido). Pra isso, replique pra outro servidor — idealmente em região diferente.

04

No servidor de origem, gere par de chaves dedicado ao backup:

sudo ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N "" -C "backup@$(hostname)"
sudo chmod 600 /root/.ssh/backup_key

Sem passphrase porque cron não pode digitar. A proteção é a restrição via authorized_keys no destino, mostrada no próximo passo.

05

No servidor de destino, crie usuário dedicado (ex: backup-receiver) e adicione a chave pública com restrição de comando:

# No destino:
sudo useradd -m -s /bin/bash backup-receiver
sudo mkdir -p /home/backup-receiver/.ssh /backup/remote
sudo chown -R backup-receiver: /home/backup-receiver /backup/remote

Edite /home/backup-receiver/.ssh/authorized_keys adicionando a chave pública gerada (/root/.ssh/backup_key.pub do origem) prefixada com a restrição:

command="rsync --server -logDtprRe.iLsfxC --delete . /backup/remote/",no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding ssh-ed25519 AAAA... backup@servidor-origem

A diretiva command="..." força o destino a só aceitar essa invocação específica do rsync — mesmo se a chave vazar, o invasor não consegue shell. Ajuste --server conforme as opções que seu rsync local usa (veja o que ele exige rodando rsync -e "ssh -v" ... em modo verbose pra capturar a linha exata).

06

Crie script /usr/local/sbin/backup-offsite.sh no origem:

#!/bin/bash
set -euo pipefail

REMOTE_USER="backup-receiver"
REMOTE_HOST="backup.exemplo.com"
LOCAL_CURRENT="/backup/local/daily/current/"
LOG_FILE="/var/log/backup/offsite-$(date +%Y-%m).log"
SSH_KEY="/root/.ssh/backup_key"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

log "===== Início replicação offsite ====="

rsync -aHAX --delete \
    --numeric-ids \
    -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new" \
    "$LOCAL_CURRENT" \
    "$REMOTE_USER@$REMOTE_HOST:/backup/remote/" >> "$LOG_FILE" 2>&1

log "===== Fim replicação offsite ====="

Permissão e teste:

sudo chmod 700 /usr/local/sbin/backup-offsite.sh
sudo /usr/local/sbin/backup-offsite.sh
Largura de banda em replicação inicial

O primeiro run copia tudo e pode demorar horas em datasets grandes. Considere fazer a carga inicial fora do horário de pico ou via mídia física se o destino estiver na mesma sala. Runs subsequentes transferem só o delta — geralmente segundos a minutos.

Agendamento com cron

Com os scripts validados, agende execução automática diária.

07

Edite o crontab do root:

sudo crontab -e

Adicione (ajuste horário conforme janela de menor tráfego do seu servidor):

# Garante PATH consistente — cron usa PATH mínimo por padrão
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]

# Backup local todo dia às 03:00
0 3 * * * /usr/local/sbin/backup-daily.sh

# Replicação offsite todo dia às 04:30 (depois do local)
30 4 * * * /usr/local/sbin/backup-offsite.sh

Salve e saia. O cron lê a mudança automaticamente — sem precisar reiniciar nada.

Verificação

Depois de 24-48 horas, confirme que tudo está rodando:

# Snapshots diários acumulando
ls -lah /backup/local/daily/

# Log do último run
tail -n 30 /var/log/backup/backup-$(date +%Y-%m).log

# Log do offsite
tail -n 30 /var/log/backup/offsite-$(date +%Y-%m).log

# No servidor de destino
ls -lah /backup/remote/

Você deve ver diretórios datados (2026-05-28, 2026-05-29, etc.), com du -sh mostrando tamanho aparente igual ao dataset mas espaço real consumido bem menor graças aos hardlinks. Confirme com:

du -sh /backup/local/daily/2026-05-29        # tamanho aparente
du -sh --total /backup/local/daily/          # tamanho real total

A diferença entre os dois números é o ganho dos hardlinks.

Resolução de problemas

Script funciona manual mas falha no cron

Quase sempre é PATH. Cron roda com PATH=/usr/bin:/bin por padrão. Solução: ou usar caminhos absolutos no script (/usr/bin/rsync, /usr/bin/ssh), ou definir PATH= no topo do crontab como mostrado acima. Verifique também se MAILTO está configurado pra receber stderr — sem isso, falhas viram silêncio absoluto.

rsync retorna código 23 ou 24

Código 23 indica “alguns arquivos não foram transferidos” (geralmente arquivos abertos ou com permissão restrita); 24 indica “alguns arquivos sumiram durante a transferência” (típico em /var/log ou /tmp). Esses códigos não são necessariamente erro fatal — ajuste o set -e do script pra tolerar com rsync ... || [ $? -eq 23 ] || [ $? -eq 24 ] se forem esperados no seu cenário.

Permission denied no destino offsite

A diretiva command= no authorized_keys é literal — qualquer mudança nas flags do rsync local quebra. Solução: rode o backup-offsite.sh manual com -e "ssh -v" no rsync, capture a linha exata rsync --server ... que ele envia, e atualize o command= no destino com essa string.

Espaço em disco crescendo descontroladamente

Provavelmente o --link-dest não está pegando o snapshot anterior — alguma mudança quebrou o symlink current ou os snapshots estão em filesystems diferentes (hardlinks não cruzam filesystem). Verifique com stat -c '%i' arquivo em duas datas: se o inode for o mesmo, hardlink ativo; se for diferente, está duplicando.

Próximos passos

Esta rotina cobre o básico de produção. Pra evoluir:

  • Adicione promoção automática semanal/mensal: cópia (não hardlink) do snapshot diário pra weekly/ toda segunda e pra monthly/ todo dia 1.
  • Integre alerta ativo: se o último log tiver mais de 25 horas, dispare notificação (Discord, e-mail, Healthchecks.io). Backup silenciosamente parado é o pior cenário.
  • Teste restauração mensal automatizada: restaure um arquivo aleatório do backup mais antigo, compare checksum com produção, logue resultado.
  • Avalie criptografia em trânsito e em repouso pro destino offsite (já em trânsito via SSH; em repouso considere LUKS no volume do destino).
  • Se você está rodando isso em produção, uma VPS Hostini (/vps) tem volumes NVMe rápidos pra acomodar histórico de snapshots e ainda libera banda generosa pra replicação offsite — o backup deixa de ser fator limitante na escolha de plano.

Perguntas frequentes

Qual a diferença entre rsync com --link-dest e tar incremental?

rsync com --link-dest cria snapshots completos usando hardlinks pra arquivos não alterados — cada diretório de backup parece uma cópia integral mas ocupa só o delta no disco. tar incremental gera arquivos diferentes a cada execução e exige restaurar a base mais todos os incrementais em ordem. Pra restauração rápida e gestão simples, --link-dest é geralmente preferível.

Por que meu cron não está rodando o script de backup?

Causas comuns: PATH não definido no crontab (cron usa PATH mínimo, então comandos como rsync ou ssh podem não ser encontrados — use caminhos absolutos), permissão de execução faltando no script, ou redirecionamento de stderr ausente escondendo o erro real. Adicione MAILTO no topo do crontab ou redirecione com >> /var/log/backup.log 2>&1 pra capturar saída.

Backup offsite por SSH precisa de senha em cada execução?

Não — e não deve. Gere um par de chaves dedicado ao backup com ssh-keygen -t ed25519 -f /root/.ssh/backup_key sem passphrase, adicione a chave pública ao authorized_keys do destino restringindo o comando permitido (command="rsync --server ...",no-pty) e use ssh -i /root/.ssh/backup_key no rsync. Senha interativa em cron simplesmente trava o job.

Quanto tempo de retenção é razoável pra backup diário?

Padrão saudável: 7 dias de diários, 4 semanas de semanais, 6 meses de mensais — varia com o tamanho do dataset e custo de armazenamento. Com --link-dest o custo marginal de manter 30 snapshots diários é baixo (só o delta de mudanças), então não tenha medo de reter mais que a intuição inicial sugere.

Como verificar que um backup realmente funciona sem restaurar tudo?

Restaurar é o único teste real, então automatize-o: uma vez por mês, restaure aleatoriamente um arquivo do backup mais antigo (rsync inverso ou cp direto do snapshot) pra um diretório temporário e compare checksum com produção via sha256sum. Logar o resultado e alertar em divergência. Backup não testado é esperança, não estratégia.

rsync ou borg/restic pra backup em servidor Linux?

rsync é simples, transparente e ideal pra cópias completas com hardlinks (--link-dest) ou sincronizações entre máquinas. borg e restic oferecem deduplicação por bloco, criptografia integrada e compressão — vantagens reais pra datasets grandes (TB) ou backup offsite via internet com banda limitada. Pra servidores pequenos/médios, rsync cobre 90% dos casos sem dependências extras.

Tópicos:
Próximos passos Cloud Ryzen com NVMe e proteção DDoS sempre ativa.Coloque em produção numa VPS Hostini →
Esse tutorial foi útil?
Falar no WhatsApp