Ferramentas
Borg como solução de backup.
Launchd para criação de backups periódicos
Passos
- Criar um repositório do Borg: Será armazenado nesse diretório todos os backups feitos e seus snapshots incrementais.
borg init -e=repokey ~/home-backup
- (opcional) Exclusão / Inclusão de arquivos no backup: Pode-se criar um arquivo para incluir ou excluir arquivos ou diretórios do backup.
- Crie um Arquivo para exclusão / inclusão
- (opcional) Remover arquivos/diretórios que já estão nos backups existentes: editar o arquivo de patterns só vale para os próximos backups — para retroagir e liberar espaço use
recreate. - Comando para criar os backups:
borg create --progress --stats --patterns-from ~/borg-backup-home.lst ~/home-backup::'{now}'
- Listando os backups:
borg list ~/home-backup
- Compactando os backups: Rode o seguinte comando para compactar os backups eliminando segmentos não utilizados.
borg compact /Users/my-user/home-backup
- Prune os backups: Para limpeza dos backups antigos ou repetidos, pode-se usar o comando
prune:borg prune --keep-within=2d --keep-daily=7 --keep-weekly=8 --keep-monthly=12 --keep-yearly=3 ~/home-backup- A explicação desse comando pode ser vista em Explicação do comando prune
- (opcional) Sync do backup para um bucket: Para enviar para a nuvem, pode usar um serviço do tipo “Google Drive” / “Dropbox” ou subir em um bucket. Exemplo:
gsutil rsync -d ~/home-backup gs://my-bucket
- Automatizar backup e sync: No OsX (Mac), pode-se usar o Launchd para realizar o backup periodicamente. Para isso
- Crie um Shell para backup
- Crie um Arquivo launchd para macosx
Outros
Shell para backup
Pode-se guarda-lo em ~/borg-home-backup.sh
Versão mínima (ok para rodar na mão):
#!/usr/bin/env bash
export BORG_PASSPHRASE="my-passprase"
/opt/homebrew/bin/borg create --progress --stats --patterns-from /Users/my-user/borg-backup-home.lst /Users/my-user/home-backup::'{now}'
/opt/homebrew/bin/borg prune --keep-daily=5 --keep-weekly=14 --keep-monthly=4 --keep-yearly=12 /Users/my-user/home-backup
/opt/homebrew/bin/borg compact /Users/my-user/home-backup
echo "Syncing to GCP Bucket"
/Users/my-user/google-cloud-sdk/bin/gsutil rsync -d ~/home-backup gs://my-backup-bucket
# OR
aws s3 sync /Users/my-user/home-backup/ s3://my-bucket/ --deleteVersão para rodar sob Launchd
Para execução automática e frequente (a cada 15min), a versão mínima acima tem
problemas: sem tratamento de erro, --progress polui o log fora do terminal, e
prune + compact a cada execução são caros. Ver Armadilhas ao rodar backup sob launchd.
#!/usr/bin/env bash
#
# Backup do home com borg. Projetado para rodar via launchd
# (~/Library/LaunchAgents/com.my-user.borg.plist) e também na mão.
set -uo pipefail
BORG=/opt/homebrew/bin/borg
PASS=/opt/homebrew/bin/pass
REPO_DIR=/Users/my-user/home-backup
PATTERNS=/Users/my-user/borg-backup-home.lst
LOCK_DIR=/tmp/com.my-user.borg.lock
PRUNE_STAMP=/Users/my-user/.last-prune
# Evita rodar duas instâncias ao mesmo tempo (ex: launchd + execução manual).
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "[$(date '+%F %T')] já existe backup em andamento ($LOCK_DIR) — saindo"
exit 0
fi
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT
log() { echo "[$(date '+%F %T')] $*"; }
# Passphrase vem do pass (ver "Automação sem expor a passphrase em texto plano").
if ! BORG_PASSPHRASE="$("$PASS" my-store/borg-passphrase)" || [[ -z "$BORG_PASSPHRASE" ]]; then
log "ERRO: não conseguiu ler a passphrase do pass — abortando"
exit 1
fi
export BORG_PASSPHRASE
# Sem isso o borg pergunta interativamente (e travaria no launchd).
export BORG_RELOCATED_REPO_ACCESS_IS_OK=no
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=no
status=0
# borg: rc=0 sucesso, rc=1 warning (ex: arquivo sem permissão de leitura),
# rc>=2 erro real. Só rc>=2 deve marcar a execução como falha.
run_borg() {
local what="$1"; shift
local rc=0
log "borg $what — início"
"$BORG" "$@" || rc=$?
case $rc in
0) log "borg $what — ok" ;;
1) log "borg $what — ok com warnings (rc=1)" ;;
*) log "borg $what — FALHOU (rc=$rc)"; status=$rc ;;
esac
return $rc
}
run_borg create create --stats --lock-wait 600 \
--patterns-from "$PATTERNS" "${REPO_DIR}::{now}"
# prune + compact são pesados; uma vez por dia é suficiente para a
# cadência de 15min. --keep-within=2d preserva os pontos intra-dia
# recentes (sem ele o prune apagaria os backups do mesmo dia).
if [[ $status -eq 0 && "$(cat "$PRUNE_STAMP" 2>/dev/null)" != "$(date +%F)" ]]; then
run_borg prune prune --lock-wait 600 --stats \
--keep-within=2d --keep-daily=7 --keep-weekly=10 \
--keep-monthly=3 --keep-yearly=1 "$REPO_DIR"
if [[ $status -eq 0 ]]; then
run_borg compact compact --lock-wait 600 "$REPO_DIR"
date +%F > "$PRUNE_STAMP"
fi
fi
log "fim (rc=$status)"
exit $statusPontos de projeto:
- Lock por diretório (
mkdiré atômico): o launchd já não roda duas instâncias do mesmo label, mas isso protege contra execução manual concorrente. Junto com--lock-wait 600, evita falha por lock do repositório. rc=1não é falha: o borg usarc=1para warnings. Tratar tudo!= 0como erro faz o job parecer quebrado quando só houve um arquivo ilegível.prune/compact1x/dia via arquivo de stamp:compactreescreve segmentos do repositório e é caro para rodar dezenas de vezes por dia.- Sem
--progress: fora de um TTY só gera ruído no log.
Arquivo launchd para macosx
Crie o arquivo abaixo que periodicamente o shell ~/borg-home-backup.sh
Este serviço é programado para iniciar em quatro momentos diferentes no decorrer do dia, especificamente às 10:00, 13:00, 18:00 e 20:00.
Além disso, qualquer saída padrão ou erro gerado pelo serviço será redirecionado para dois arquivos diferentes localizados no diretório “/tmp”, nomeados como “com.my-user.borg.out” e “com.my-user.borg.err”, respectivamente.
Pode-se guarda-lo em ~/Library/LaunchAgents/com.my-user.borg.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.my-user.borg</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/opt/gnu-getopt/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/my-user</string>
<key>GNUPGHOME</key>
<string>/Users/my-user/.gnupg</string>
<key>LANG</key>
<string>en_US.UTF-8</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Users/my-user/borg-home-backup.sh</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>10</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>13</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>18</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>20</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
<key>StandardOutPath</key>
<string>/tmp/com.my-user.borg.out</string>
<key>StandardErrorPath</key>
<string>/tmp/com.my-user.borg.err</string>
</dict>
</plist>Cadência alta (ex: 15min no horário comercial, 1h à noite)
StartCalendarInterval não aceita intervalo — só uma lista de horários fixos
(um dict por horário; a chave omitida é curinga). Para “a cada 15min das 10h às
18h, depois de hora em hora até a meia-noite” são 39 dicts. Vale gerar o plist
em vez de escrever à mão:
entries = []
for h in range(10, 18): # 10:00 -> 17:45, a cada 15min
for m in (0, 15, 30, 45):
entries.append((h, m))
entries.append((18, 0))
for h in list(range(19, 24)) + [0]: # de hora em hora até 00:00
entries.append((h, 0))
cal = "\n".join(
f" <dict>\n"
f" <key>Hour</key>\n"
f" <integer>{h}</integer>\n"
f" <key>Minute</key>\n"
f" <integer>{m}</integer>\n"
f" </dict>"
for h, m in entries
)Detalhes que importam nessa cadência:
- Um label = uma instância. O launchd não dispara um job que já está rodando; se o backup passar dos 15min, o horário seguinte é simplesmente perdido (não enfileira). Por isso vale manter tudo num único plist em vez de dois (um de 15min + um horário), que seriam dois labels e poderiam se sobrepor.
- Máquina dormindo: horários perdidos durante o sono rodam uma vez ao acordar.
- Prioridade:
Nice+LowPriorityIOpara o backup não competir com o trabalho interativo. RunAtLoad falsepara obootstrapnão disparar um backup na hora.
<key>Nice</key>
<integer>5</integer>
<key>LowPriorityIO</key>
<true/>
<key>RunAtLoad</key>
<false/>Validar o XML antes de carregar: plutil -lint ~/Library/LaunchAgents/com.my-user.borg.plist
Carregar / ativar o serviço
LaunchAgents são serviços de nível de usuário — não usar sudo (caso contrário o launchctl espera um caminho de LaunchDaemons e falha).
# Carregar (sintaxe legada, ainda funciona)
launchctl load ~/Library/LaunchAgents/com.my-user.borg.plist
# Carregar (sintaxe moderna recomendada)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.my-user.borg.plistDescarregar / desativar o serviço
# Sintaxe legada
launchctl unload ~/Library/LaunchAgents/com.my-user.borg.plist
# Sintaxe moderna
launchctl bootout gui/$(id -u)/com.my-user.borgRecarregar (descarregar e carregar novamente)
launchctl bootout gui/$(id -u)/com.my-user.borg
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.my-user.borg.plistVerificar status
# Listar serviço e ver se está rodando
launchctl list | grep com.my-user.borg
# Detalhes do serviço (erros, última execução, etc.)
launchctl print gui/$(id -u)/com.my-user.borg⚠️ Erro comum: usar
sudo launchctl loadcom LaunchAgents causaInput/output error. Sempre use semsudopara agentes em~/Library/LaunchAgents/.
Testar o job sem esperar o horário
kickstart -p dispara o job no ambiente real do launchd (PATH, HOME, sem TTY,
sem Full Disk Access). Testar rodando o shell no terminal não vale — o terminal
tem permissões e variáveis que o launchd não tem.
: > /tmp/com.my-user.borg.out; : > /tmp/com.my-user.borg.err
launchctl kickstart -p gui/$(id -u)/com.my-user.borg
# depois de terminar:
launchctl print gui/$(id -u)/com.my-user.borg | grep -E 'last exit|runs ='
cat /tmp/com.my-user.borg.out
cat /tmp/com.my-user.borg.errArmadilhas ao rodar backup sob launchd
Três problemas que só aparecem no ambiente do launchd (nenhum acontece rodando o script no terminal):
1. TCC / Full Disk Access → rc=1 em toda execução
O processo do launchd não tem Full Disk Access, então não consegue entrar em
diretórios protegidos pelo TCC: ~/Library/Mail, ~/Library/Containers,
~/Library/Group Containers, ~/Library/Application Support/AddressBook,
~/Pictures/*.photoslibrary, ~/.Trash, etc. O log enche de:
/Users/my-user/Library/Mail: dir_open: [Errno 1] Operation not permitted: 'Mail'
e o borg termina com rc=1. Duas saídas:
- Se você não quer esses diretórios no backup (caso comum): use
!em vez de-no arquivo de patterns, para o borg nem entrar neles. Ver Arquivo para exclusão / inclusão. Além de calar os erros, corta muito I/O — num home de ~108k arquivos ocreatecaiu de 1m28s para 31s. - Se você quer (
~/Documents,~/Library…): conceder Full Disk Access ao binário do borg (/opt/homebrew/bin/borg) em Ajustes → Privacidade e Segurança.
2. prune a cada execução apaga os backups intra-dia
borg prune --keep-daily=N mantém apenas o último arquivo de cada dia. Rodando
prune a cada 15min, todos os pontos anteriores do dia corrente são apagados na
hora — o que anula completamente o sentido de fazer backup a cada 15min.
Solução: --keep-within=2d (mantém tudo das últimas 48h) e rodar prune/compact
só uma vez por dia.
3. pass / GPG sem TTY
pass depende do gpg-agent, que pode precisar de pinentry — e no launchd não há
TTY nem sessão gráfica para exibi-lo. Verificar antes, simulando o ambiente:
env -i HOME=$HOME PATH=/opt/homebrew/bin:/usr/bin:/bin pass my-store/borg-passphraseSe funcionar, a chave está sem passphrase no disco (gpg-connect-agent 'keyinfo --list' /bye
mostra C = clear no campo de proteção). Se pedir passphrase, é preciso
instalar pinentry-mac e deixar o agente com cache longo, ou usar
BORG_PASSPHRASE_FILE com permissão restrita.
Arquivo para exclusão / inclusão
Pode-se guarda-lo em ~/borg-backup-home.lst
Prefixos (avaliados na ordem do arquivo — o primeiro match ganha):
| Prefixo | Significado |
|---|---|
R | Root — o caminho raiz a partir do qual o borg recursa |
+ | Inclui |
- | Exclui, mas ainda entra no diretório para testar os filhos |
! | Exclui e não entra no diretório (no-recurse) |
A diferença entre - e ! é a armadilha principal: com - ~/Library o borg
ainda percorre toda a árvore do Library só para descobrir que cada item está
excluído — gastando I/O e, sob launchd, batendo em erros de permissão do TCC
(ver Armadilhas ao rodar backup sob launchd).
Cuidado com a ordem: um ! num diretório pai impede que um + posterior
alcance um filho dele. Se você precisa de ~/Library/LaunchAgents, o + dele
tem que vir antes do ! ~/Library/** — e o padrão precisa ser
! /Users/my-user/Library/** (com /**), não ! /Users/my-user/Library, senão
o borg nem entra no Library e nunca chega no LaunchAgents.
R /Users/my-user/
# Any inside dir...
- **/cache
- **/cache-chat
- **/.git
- **/node_modules
# Emacs temp
- **/*~
- **/\#*
# borg backup script — só precisa excluir na versão que tem a passphrase
# inline; usando `pass` o script pode (e deve) ir para o backup.
- /Users/my-user/borg-home-backup.sh
# Dirs from home
- /Users/my-user/.docker
- /Users/my-user/.pyenv
- /Users/my-user/.nuget
- /Users/my-user/.nvm
- /Users/my-user/.npm
- /Users/my-user/.vscode
- /Users/my-user/.local
- /Users/my-user/Applications
- /Users/my-user/javasharedresources
- /Users/my-user/nltk_data
- /Users/my-user/google-cloud-sdk
- /Users/my-user/go
- /Users/my-user/confluent
- /Users/my-user/home-backup # backup itself
# Library: quero só o LaunchAgents — o "+" vem antes do "!" do pai.
# "!" (e não "-") para não percorrer a árvore inteira nem bater no TCC.
+ /Users/my-user/Library/LaunchAgents
! /Users/my-user/Library/**
! /Users/my-user/.Trash
! /Users/my-user/Pictures
# Personal workspace temp dir (trash things...)
- /Users/my-user/wo/personal/temp
# dirs with data from kafka (messages in files)
- **/kafka-consumer/data-*
Como remover arquivos e diretórios já existentes nos backups (recreate)
Editar o arquivo de patterns (ver Arquivo para exclusão / inclusão) e deixar o create rodar só afeta os próximos backups — o que já foi gravado em snapshots antigos continua ocupando espaço no repositório, porque prune só apaga snapshots inteiros, não arquivos de dentro deles. Para remover retroativamente de todos os snapshots existentes (e recuperar o espaço em disco), é preciso reescrever o histórico com recreate:
- Editar o arquivo de patterns, adicionando a(s) regra(s) de exclusão (prefixo
-ou!) para o que não deve mais entrar no backup. Ex: parar de guardar.git/node_modulesdentro de um diretório específico:- /Users/my-user/algum-dir/**/.git - /Users/my-user/algum-dir/**/node_modules - Rodar
recreateapontando para o mesmo arquivo de patterns editado — ele reaplica os filtros em todos os snapshots já existentes no repositório, não só nos novos:BORG_PASSPHRASE="..." borg recreate --progress --stats --lock-wait 600 \ --patterns-from ~/borg-backup-home.lst ~/home-backup - Compactar para liberar o espaço em disco de fato (o
recreatesozinho reescreve os archives mas não encolhe o repositório):borg compact ~/home-backup - (opcional) Checar integridade depois de uma operação grande:
borg check --repository-only ~/home-backup
Pontos de atenção:
recreatenão lê o arquivo de patterns sozinho — é preciso passar--patterns-from(ou-e/--exclude-from/--pattern) explicitamente, mesmo reaproveitando o mesmo arquivo usado pelocreate. Sem isso, ele reescreve os archives sem excluir nada.- A linha
R /Users/my-user/do arquivo de patterns define a raiz que ocreateusa para recursar o filesystem; norecreateela não tem esse papel (não há filesystem envolvido, só o conteúdo já arquivado) — mas as regras+/-/!continuam funcionando normalmente como filtro sobre os caminhos já salvos. - É uma operação lenta e pesada: reescreve todos os archives do repositório — num repo com dezenas de snapshots e vários GB, pode levar minutos.
- É irreversível: os IDs dos archives mudam e o conteúdo removido não pode ser recuperado depois — diferente do
prune, que só descarta snapshots inteiros. - Evitar rodar junto com o backup automático: como o
recreatesegura o lock do repositório, umcreateagendado via Launchd pode falhar por timeout de lock (--lock-wait) se disparar no meio. Vale descarregar o agent antes (launchctl bootout gui/$(id -u)/com.my-user.borg) e recarregar depois (launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.my-user.borg.plist) — ver Carregar / ativar o serviço e Descarregar / desativar o serviço.
Explicação do comando prune
Exemplo: borg prune --keep-within=2d --keep-daily=7 --keep-weekly=8 --keep-monthly=12 --keep-yearly=3 ~/home-backup
Explicação:
Este comando é usado para podar (ou seja, excluir) backups antigos.
Aqui está o que cada opção faz:
--keep-within=2d: Mantém todos os backups feitos dentro dos últimos 2 dias (48h).
--keep-daily=7: Mantém um backup diário pelos últimos 7 dias.
--keep-weekly=8: Mantém um backup semanal pelas últimas 8 semanas.
--keep-monthly=12: Mantém um backup mensal pelos últimos 12 meses.
--keep-yearly=3: Mantém um backup anual pelos últimos 3 anos.
O último argumento ~/home-backup é o diretório que contém os backups.
Portanto, este comando irá excluir todos os backups que não se enquadrem nessas regras.
Gerenciamento de Chaves e Criptografia
O Borg usa criptografia AES-256 em duas camadas:
Passphrase (você memoriza/guarda)
└─► decripta ──► Repository Key (AES-256)
└─► decripta ──► dados dos backups
Modos de criptografia
| Modo | Onde a chave fica | Quando usar |
|---|---|---|
repokey-blake2 | No próprio repositório | Recomendado — maioria dos casos |
keyfile-blake2 | Só na máquina cliente (~/.config/borg/keys/) | Servidor não confiável (ex: cloud) |
none | — | Repositórios locais sem necessidade de sigilo |
Diferença crucial: no modo repokey, a chave cifrada fica embutida no repositório. No modo keyfile, a chave não está no repositório — sem ela e a passphrase, os dados são inacessíveis mesmo com acesso total ao servidor.
Exportar a chave (faça isso logo após criar o repo)
# Exportar como arquivo
borg key export /path/to/repo ~/borg.key
# Exportar em formato legível por humanos (para imprimir/papel)
borg key export --paper /path/to/repo⚠️ Com
keyfile, se perder a máquina sem esse export, perde os dados para sempre.
Proteger a chave exportada com GPG
gpg --symmetric --cipher-algo AES256 ~/borg.key
shred -u ~/borg.key # apagar versão em texto plano
# guarde o borg.key.gpg em local separado do repositórioTrocar passphrase
borg key change-passphrase /path/to/repoAutomação sem expor a passphrase em texto plano
Evite BORG_PASSPHRASE="texto" direto no script. Prefira:
# Via arquivo com permissão restrita
export BORG_PASSPHRASE_FILE=/root/.borg-passphrase # chmod 400
# Via GPG (requer gpg-agent rodando)
export BORG_PASSPHRASE=$(gpg --quiet --batch --decrypt ~/.safe/borg-pass.gpg)Verificar integridade do repositório
borg check /path/to/repo # verificação rápida (metadados)
borg check --verify-data /path/to/repo # completa (mais lento)Checklist de disaster recovery
Guarde em local seguro (gerenciador de senhas, cofre):
- Passphrase do repositório
- Export da chave (
borg key export) - Endereço/caminho do repositório
- Versão do Borg usada
- Comando para restaurar:
borg mount /repo /mnt/borg