Backup de Bases de Dados SQLite em Produção: Estratégias para Aplicações Self-Hosted

Linux · SQLite · Backup · Self-Hosted · Databases | ✎ Duarte Spínola | 11 de Julho de 2026

O SQLite é o motor de base de dados mais usado no mundo — está em praticamente todas as aplicações self-hosted que um sysadmin PME encontra: Nextcloud, Authelia, Grafana, Home Assistant, Prometheus, e centenas de serviços Docker. Ao contrário do PostgreSQL ou MySQL, o SQLite é um ficheiro único no disco, o que torna o backup aparentemente trivial — copiar o ficheiro. Mas copiar um ficheiro SQLite enquanto está em uso pode produzir um backup corrompido. Este guia cobre estratégias de backup consistentes (hot e cold), automação com cron, validação de integridade, retenção 3-2-1, e recuperação passo-a-passo.

Conteúdos

1. O que Está a Acontecer — SQLite em Produção

O SQLite é um base de dados embutida (embedded) — não tem servidor separado, corre no mesmo processo da aplicação. A base de dados inteira é um ficheiro (.db, .sqlite, .sqlite3) no disco. Isto tem vantagens (simplicidade, sem configuração, sem portas de rede) mas traz um desafio crítico para backups: copiar o ficheiro enquanto a aplicação está a escrever produz um backup inconsistente ou corrompido.

O SQLite usa WAL (Write-Ahead Logging) por defeito em muitas aplicações. Com WAL activo, há três ficheiros: database.db, database.db-wal (transacções pendentes), e database.db-shm (memória partilhada). Copiar apenas o .db sem o -wal perde transacções não confirmadas.

NUNCA faça cp database.db backup.db enquanto a aplicação está a correr. O ficheiro pode estar a meio de uma escrita. O backup resultante pode estar corrompido ou ter transacções perdidas. Use o comando .backup do SQLite (Método 1) que garante consistência.

2. Cenários em que o Backup SQLite é Crítico

Aplicação Localização típica do DB Método recomendado
Nextcloud /var/www/nextcloud/data/owncloud.db Método 1 (.backup) ou occ command
Authelia /config/authelia.sqlite Método 4 (Docker exec)
Grafana /var/lib/grafana/grafana.db Método 1 (.backup)
Home Assistant /config/home-assistant_v2.db Método 1 (.backup) ou HA backup
Prometheus N/A (usa TSDB própria) Usar snapshots do Prometheus
Vaultwarden (Bitwarden) /data/db.sqlite3 Método 4 (Docker exec)
App custom (Python, Go) Definido pelo developer Método 1 (.backup) ou Método 3 (VACUUM INTO)

3. Método 1 — Hot Backup com .backup (Recomendado)

O comando .backup do SQLite CLI faz um backup consistente mesmo com a base de dados em uso. Usa a API sqlite3_backup_init() que lê transação a transação de forma segura:

# Backup consistente com .backup
sqlite3 /path/to/database.db ".backup '/backup/database_backup.db'"

# Com timestamp
sqlite3 /path/to/database.db ".backup '/backup/database_$(date +%Y%m%d_%H%M%S).db'"

# Verificar que o backup é válido
sqlite3 /backup/database_backup.db "PRAGMA integrity_check;"
# Deve retornar: ok

# Verificar que tem as mesmas tabelas
sqlite3 /backup/database_backup.db ".tables"
sqlite3 /path/to/database.db ".tables"
# As tabelas devem ser idênticas

Script de hot backup

#!/bin/bash
# sqlite-hot-backup.sh — Backup consistente de SQLite
# Testado em Ubuntu 24.04, Debian 12 (2026-07)

DB_PATH="/var/www/nextcloud/data/owncloud.db"
BACKUP_DIR="/backup/sqlite"
RETENTION_DAYS=30

mkdir -p "$BACKUP_DIR"

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/owncloud_${TIMESTAMP}.db"

# Hot backup consistente
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"

# Verificar integridade
RESULT=$(sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;")
if [ "$RESULT" = "ok" ]; then
    echo "✅ Backup OK: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
else
    echo "❌ Backup CORROMPIDO: $BACKUP_FILE"
    echo "  Integrity check: $RESULT"
    rm -f "$BACKUP_FILE"
    exit 1
fi

# Comprimir para poupar espaço
gzip -f "$BACKUP_FILE"
echo "✅ Compressed: ${BACKUP_FILE}.gz"

# Rotação — apagar backups antigos
find "$BACKUP_DIR" -name "owncloud_*.db.gz" -mtime +$RETENTION_DAYS -delete
echo "✅ Old backups purged (>${RETENTION_DAYS} days)"

echo "DONE: $(date)"

4. Método 2 — Cold Backup (Cópia de Ficheiro)

Se a aplicação pode ser parada (janela de manutenção), o cold backup é o método mais simples — copiar todos os ficheiros relacionados com a base de dados:

# 1. Parar a aplicação
sudo systemctl stop nextcloud
# OU parar container Docker
sudo docker stop nextcloud

# 2. Copiar TODOS os ficheiros SQLite (.db, .db-wal, .db-shm)
cp /var/www/nextcloud/data/owncloud.db /backup/owncloud_cold.db
cp /var/www/nextcloud/data/owncloud.db-wal /backup/owncloud_cold.db-wal
cp /var/www/nextcloud/data/owncloud.db-shm /backup/owncloud_cold.db-shm

# 3. Reiniciar aplicação
sudo systemctl start nextcloud

# 4. Verificar backup
sqlite3 /backup/owncloud_cold.db "PRAGMA integrity_check;"

ℹ O cold backup é seguro mas requer paragem de serviço. Para PMEs com janelas de manutenção noturnas, é aceitável. Para serviços 24/7, use o Método 1 (hot backup).

5. Método 3 — VACUUM INTO (Backup Otimizado)

O VACUUM INTO (disponível desde SQLite 3.27+, 2019) cria uma cópia optimizada da base de dados — sem fragmentação, sem WAL, sem páginas livres. É mais lento que .backup mas produz um ficheiro mais pequeno e optimizado:

# Backup optimizado com VACUUM INTO
sqlite3 /path/to/database.db "VACUUM INTO '/backup/database_optimized.db'"

# Vantagens:
# - Ficheiro mais pequeno (sem páginas livres/fragmentadas)
# - Sem WAL (todas as transacções são consolidated)
# - Pode ser movido para outro servidor sem ficheiros auxiliares

# Verificar tamanho antes/depois
ls -lh /path/to/database.db
# 1.2G
ls -lh /backup/database_optimized.db
# 800M (após remoção de fragmentação)

6. Método 4 — Backup via Docker Exec

Para aplicações em containers Docker, o backup deve ser feito dentro do container:

# Backup de SQLite dentro de container Docker
# Exemplo: Vaultwarden

CONTAINER="vaultwarden"
DB_PATH="/data/db.sqlite3"
BACKUP_DIR="/backup/vaultwarden"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

# Método A: Usar sqlite3 dentro do container (se disponível)
docker exec $CONTAINER sqlite3 $DB_PATH ".backup '/data/backup_temp.db'"
docker cp $CONTAINER:/data/backup_temp.db "$BACKUP_DIR/vaultwarden_${TIMESTAMP}.db"
docker exec $CONTAINER rm /data/backup_temp.db

# Método B: Se sqlite3 não está no container, usar docker cp + .backup externo
# Primeiro parar o container temporariamente
docker stop $CONTAINER
docker cp $CONTAINER:$DB_PATH "$BACKUP_DIR/vaultwarden_${TIMESTAMP}.db"
docker cp $CONTAINER:${DB_PATH}-wal "$BACKUP_DIR/vaultwarden_${TIMESTAMP}.db-wal" 2>/dev/null || true
docker cp $CONTAINER:${DB_PATH}-shm "$BACKUP_DIR/vaultwarden_${TIMESTAMP}.db-shm" 2>/dev/null || true
docker start $CONTAINER

# Verificar
sqlite3 "$BACKUP_DIR/vaultwarden_${TIMESTAMP}.db" "PRAGMA integrity_check;"

7. Automação com Cron — Script de Backup

# /usr/local/bin/sqlite-backup-all.sh
#!/bin/bash
# Backup de todas as bases SQLite no sistema
# Testado em Ubuntu 24.04 (2026-07)

BACKUP_ROOT="/backup/sqlite"
RETENTION_DAYS=30
LOG="/var/log/sqlite-backup.log"

echo "=== SQLite Backup $(date) ===" >> "$LOG"

# Lista de bases de dados para backup
DATABASES=(
    "/var/www/nextcloud/data/owncloud.db:nextcloud"
    "/var/lib/grafana/grafana.db:grafana"
    "/opt/authelia/authelia.sqlite:authelia"
    "/config/home-assistant_v2.db:homeassistant"
)

for entry in "${DATABASES[@]}"; do
    DB_PATH="${entry%%:*}"
    NAME="${entry##*:}"
    BACKUP_DIR="$BACKUP_ROOT/$NAME"
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    BACKUP_FILE="$BACKUP_DIR/${NAME}_${TIMESTAMP}.db"
    
    mkdir -p "$BACKUP_DIR"
    
    if [ ! -f "$DB_PATH" ]; then
        echo "  ⚠ SKIP: $NAME — ficheiro não encontrado ($DB_PATH)" >> "$LOG"
        continue
    fi
    
    # Hot backup
    if sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'" 2>>"$LOG"; then
        # Validar
        CHECK=$(sqlite3 "$BACKUP_FILE" "PRAGMA integrity_check;")
        if [ "$CHECK" = "ok" ]; then
            gzip -f "$BACKUP_FILE"
            SIZE=$(du -h "${BACKUP_FILE}.gz" | cut -f1)
            echo "  ✅ $NAME: ${BACKUP_FILE}.gz ($SIZE)" >> "$LOG"
        else
            echo "  ❌ $NAME: backup corrompido ($CHECK)" >> "$LOG"
            rm -f "$BACKUP_FILE"
        fi
    else
        echo "  ❌ $NAME: falha no backup" >> "$LOG"
    fi
    
    # Rotação
    find "$BACKUP_DIR" -name "${NAME}_*.db.gz" -mtime +$RETENTION_DAYS -delete
done

echo "=== Done $(date) ===" >> "$LOG"
# Instalar script
sudo chmod +x /usr/local/bin/sqlite-backup-all.sh

# Cron — executar diariamente às 02:00
sudo crontab -e
# Adicionar:
0 2 * * * /usr/local/bin/sqlite-backup-all.sh

# Ou systemd timer (mais robusto)
sudo tee /etc/systemd/system/sqlite-backup.timer << 'EOF'
[Unit]
Description=SQLite Daily Backup

[Timer]
OnCalendar=02:00
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo tee /etc/systemd/system/sqlite-backup.service << 'EOF'
[Unit]
Description=SQLite Backup Service
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/sqlite-backup-all.sh
EOF

sudo systemctl enable sqlite-backup.timer
sudo systemctl start sqlite-backup.timer

8. Validação de Integridade — PRAGMA integrity_check

# Verificação completa de integridade
sqlite3 /backup/database.db "PRAGMA integrity_check;"
# Output se OK: ok
# Output se erro: row X col Y: erro descrição

# Verificação rápida (apenas estrutura)
sqlite3 /backup/database.db "PRAGMA quick_check;"

# Verificar foreign keys
sqlite3 /backup/database.db "PRAGMA foreign_key_check;"

# Verificar contagem de tabelas
sqlite3 /backup/database.db "SELECT count(*) FROM sqlite_master WHERE type='table';"

# Comparar contagem de registos entre original e backup
sqlite3 /path/to/database.db "SELECT count(*) FROM users;"
sqlite3 /backup/database.db "SELECT count(*) FROM users;"
# Devem ser iguais

# Validar schema
sqlite3 /backup/database.db ".schema" > /tmp/schema_backup.txt
sqlite3 /path/to/database.db ".schema" > /tmp/schema_original.txt
diff /tmp/schema_original.txt /tmp/schema_backup.txt
# Sem diferenças = schema idêntico

9. Retenção 3-2-1 e Rotação de Backups

A regra 3-2-1 aplica-se a backups SQLite como a qualquer outro dado:

Regra Aplicação SQLite
3 cópias Original + backup local + backup remoto
2 tipos de media Disco local + object storage (S3/Backblaze)
1 cópia offsite Sincronizar para Backblaze/Wasabi/S3
# Sincronizar backups para Backblaze B2
# Instalar rclone
curl https://rclone.org/install.sh | sudo bash

# Configurar remote
rclone config
# Escolher Backblaze B2, seguir prompts

# Sincronizar backups
rclone sync /backup/sqlite backblaze:kbase-sqlite-backups/

# Adicionar ao script de backup (após gzip):
rclone sync $BACKUP_ROOT backblaze:kbase-sqlite-backups/ --transfers 4

# Rotação granular:
# - Diário: 7 dias
# - Semanal: 4 semanas
# - Mensal: 12 meses
find $BACKUP_ROOT -name "*.db.gz" -mtime +7 -delete  # diários > 7 dias
# Manter um semanal (não apagar backups de Domingo)
find $BACKUP_ROOT -name "*.db.gz" -mtime +30 ! -name "*_Sunday*" -delete

10. Recuperação — Restaurar de Backup

# 1. Parar a aplicação
sudo systemctl stop nextcloud

# 2. Fazer backup do estado actual (mesmo corrompido, por segurança)
cp /var/www/nextcloud/data/owncloud.db /var/www/nextcloud/data/owncloud.db.broken

# 3. Descomprimir backup
gunzip /backup/sqlite/nextcloud/owncloud_20260711_020000.db.gz

# 4. Validar backup antes de restaurar
sqlite3 /backup/sqlite/nextcloud/owncloud_20260711_020000.db "PRAGMA integrity_check;"
# Deve retornar: ok

# 5. Restaurar
cp /backup/sqlite/nextcloud/owncloud_20260711_020000.db /var/www/nextcloud/data/owncloud.db

# 6. Apagar WAL e SHM antigos (forçar rebuild)
rm -f /var/www/nextcloud/data/owncloud.db-wal
rm -f /var/www/nextcloud/data/owncloud.db-shm

# 7. Corrigir permissões
chown www-data:www-data /var/www/nextcloud/data/owncloud.db
chmod 640 /var/www/nextcloud/data/owncloud.db

# 8. Reiniciar aplicação
sudo systemctl start nextcloud

# 9. Verificar que a aplicação funciona
sudo -u www-data php /var/www/nextcloud/occ status
# Deve mostrar: "Nextcloud is in maintenance mode: no" e "version: X.Y.Z"

Recuperação via .restore

# Alternativa: usar .restore do SQLite CLI
sqlite3 /var/www/nextcloud/data/owncloud.db ".restore '/backup/sqlite/nextcloud/owncloud_20260711.db'"
# .restore substitui o conteúdo da BD actual pelo backup

11. FAQ para Sysadmins

1. Posso simplesmente copiar o ficheiro .db?

Não enquanto a aplicação está a correr. O SQLite pode estar a meio de uma transacção. Use sqlite3 database.db ".backup 'destino.db'" que é seguro e consistente.

2. Preciso de copiar os ficheiros -wal e -shm?

Com o comando .backup, não — ele lê o WAL internamente e produz um backup completo. Com cold backup (cópia directa), sim — copie os três ficheiros (.db, .db-wal, .db-shm).

3. Comprimir backups SQLite?

Sim, SQLite comprime muito bem (40-70% redução com gzip). Use gzip após validar com PRAGMA integrity_check. Para compressão máxima, use xz -6.

4. Com que frequência devo fazer backup?

Diário para a maioria das aplicações. A cada hora para bases críticas (ex: Vaultwarden, Authelia). O .backup é rápido (segundos) e não bloqueia a aplicação.

5. Como migrar SQLite para PostgreSQL?

Use sqlite3to_postgres ou o modo de exportação do SQLite: sqlite3 database.db .dump | psql -d postgres_db. Para Nextcloud, há um script de migração oficial.

6. WAL mode vs DELETE mode — afecta o backup?

WAL mode é o padrão em muitas aplicações. O .backup funciona com ambos. Em WAL mode, garantir que copia os 3 ficheiros no cold backup. Em DELETE mode, apenas o .db é necessário.

7. Como verificar se um backup está corrompido?

sqlite3 backup.db "PRAGMA integrity_check;"
# ok = válido
# anything else = corrompido

# Contar registos
sqlite3 backup.db "SELECT count(*) FROM sqlite_master;"
# Deve ser > 0

8. Backups SQLite em Docker volumes?

Monte um volume para backups e use docker exec para correr o .backup dentro do container. Alternativa: use Docker backup tools como offen/docker-volume-backup que comprime e sincroniza volumes automaticamente.

12. Erros Comuns e Resoluções

Erro Causa Solução
database is locked Outro processo a escrever Usar .backup (lida com locks) ou PRAGMA busy_timeout=5000
PRAGMA integrity_check: row X is corrupt Backup corrompido Descartar backup, fazer novo; verificar SSD/SATA
unable to open database file Permissões ou path errado chown user:user backup.db && chmod 640 backup.db
file is encrypted or is not a database Ficheiro não é SQLite ou é WAL sem .db Verificar que copiou o ficheiro .db correcto
no such table: X Backup incompleto ou schema errado Re-fazer backup; comparar .schema
sqlite3: command not found SQLite não instalado sudo apt install sqlite3
VACUUM INTO: syntax error Versão SQLite antiga (< 3.27) Actualizar SQLite ou usar .backup
Backup de tamanho 0 Falha silenciosa no .backup Verificar espaço em disco e permissões

Artigos Relacionados

Voltar ao Índice

Este artigo foi útil?

Duarte Spínola

Deixe um Comentário