SQLite em produção no VPS: configuração e limites
Veja quando SQLite funciona em um único VPS, como usar WAL e busy_timeout, replicar com Litestream e reconhecer o limite de um gravador por vez.
Quando o SQLite é o banco de dados de produção correto em um VPS
Executar SQLite em produção em um VPS é a escolha correta para a maioria das aplicações pequenas, e o motivo é simples: um processo em uma máquina gravando em um arquivo não precisa de um servidor de banco de dados. Não há daemon para supervisionar, porta para proteger com firewall, senha para alternar nem uma segunda máquina para manter ativa. Uma consulta é uma chamada de função, e não uma ida e volta pela rede. Portanto, uma página que executa quarenta consultas custa quarenta chamadas de função.
O custo é específico e real. O SQLite permite apenas um gravador por vez em todo o arquivo do banco de dados, e o arquivo não pode ser compartilhado entre duas máquinas. Essas duas limitações não são um problema para um único VPS executando uma única aplicação. Elas se tornam impeditivas assim que você ultrapassa esse modelo. Este guia aborda as configurações que tornam o SQLite seguro em um servidor, o backup contínuo com Litestream e o ponto em que você deve parar.
Instale primeiro a ferramenta de linha de comando. Tudo abaixo foi executado no Ubuntu 24.04.
sudo apt update
sudo apt install -y sqlite3
sqlite3 --versionIsso exibe uma versão iniciada por 3., seguida por uma data de compilação e um hash do código-fonte. O Ubuntu 24.04 fornece o SQLite 3.45.1 desde julho de 2026. Sua aplicação provavelmente não usa esse binário: a maioria dos runtimes de linguagem inclui sua própria cópia da biblioteca SQLite, geralmente uma versão mais recente. Portanto, verifique a versão informada pelo driver do banco de dados antes de depender de um recurso recente.
Por que o modo WAL é a primeira alteração que você faz
Por padrão, o SQLite usa um journal de rollback. Antes de alterar uma página, ele copia a página original para um arquivo -journal e depois edita o banco de dados diretamente. Para fazer isso com segurança, ele obtém um bloqueio exclusivo no arquivo inteiro. Assim, todos os leitores esperam enquanto qualquer operação de gravação está em andamento. Em um laptop, isso passa despercebido. Em um servidor web, uma gravação lenta atrasa todas as requisições que acessam o banco de dados.
O modo WAL (write-ahead log) inverte a ordem. Um processo de gravação acrescenta as novas páginas a um arquivo -wal separado e deixa o banco de dados principal intacto. Os leitores continuam lendo o arquivo principal no snapshot em que começaram. Assim, os leitores não bloqueiam o processo de gravação, e o processo de gravação não bloqueia os leitores. Depois, um checkpoint copia as páginas acumuladas no WAL de volta para o banco de dados principal. Essa única alteração é responsável pela maior parte do que torna o SQLite adequado para uso por trás de uma aplicação web.
Ativar o modo WAL e confirmar que ele permanece ativo
mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"O comando imprime wal. Essa saída não é decorativa. PRAGMA journal_mode retorna o modo em que o banco de dados realmente está, portanto uma resposta delete significa que a alteração falhou e que você ainda está usando o journal de rollback.
O modo WAL é persistente. Ele é uma flag no cabeçalho do banco de dados, não uma configuração da conexão. Por isso, você o define uma vez por arquivo de banco de dados, e todas as conexões seguintes o herdam, inclusive após uma reinicialização. Comprove isso usando uma nova conexão.
sqlite3 ~/app/app.db "PRAGMA journal_mode;"Agora crie uma tabela e observe o que aparece no disco.
sqlite3 ~/app/app.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes (body) VALUES ('first row');
SQL
ls -l ~/app/Agora há três arquivos: app.db, app.db-wal e app.db-shm. O arquivo -wal contém páginas confirmadas que ainda não passaram por checkpoint. O arquivo -shm é um índice de memória compartilhada que cada conexão mapeia, para que todas concordem sobre o conteúdo do WAL. Ambos pertencem ao banco de dados e não são arquivos temporários. Copie app.db sozinho enquanto a aplicação estiver em execução e você obterá um arquivo sem todos os commits recentes. Exclua app.db e mantenha os outros dois no lugar, e o SQLite aplicará essas páginas obsoletas do WAL ao novo arquivo que aparecer com esse nome. É assim que as pessoas corrompem um banco de dados novo ao tentar redefini-lo.
As configurações de conexão de que todo aplicativo de produção precisa
Somente journal_mode é armazenado no banco de dados. Todas as outras configurações abaixo são por conexão. Isso significa que seu aplicativo precisa executá-las em cada conexão aberta, incluindo todas as conexões que um pool cria em segundo plano.
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;busy_timeout = 5000 instrui o SQLite a continuar tentando acessar um banco de dados bloqueado por até 5000 milissegundos antes de retornar database is locked. O padrão é 0. Portanto, por padrão, o SQLite falha imediatamente na primeira vez que dois gravadores operam simultaneamente. Definir esse único valor elimina a maioria dos erros de bloqueio atribuídos ao próprio SQLite.
synchronous = NORMAL é a configuração correta no modo WAL, e vale entender essa compensação. Com FULL, o SQLite chama fsync no WAL a cada commit. Com NORMAL, ele sincroniza os dados nos checkpoints. A documentação do SQLite é direta sobre o que é perdido: as transações deixam de ser duráveis após uma falha de energia ou uma reinicialização forçada. O banco de dados não pode ser corrompido por essa perda de energia. Você simplesmente perde os últimos commits que ainda não tinham sido gravados no disco. Em um VPS, essa geralmente é a compensação correta, porque remove um fsync do caminho de cada gravação.
foreign_keys = ON é desativado por padrão por compatibilidade com versões anteriores e é definido por conexão. Um esquema cheio de cláusulas REFERENCES não impõe nenhuma restrição até que cada conexão ative essa configuração.
Outra configuração só importa mais adiante. O SQLite executa checkpoints automaticamente quando o WAL ultrapassa 1000 páginas, e o trabalho é realizado pela conexão que, naquele momento, terminar uma transação. Isso não é um problema por si só. A situação muda quando o Litestream está em execução, porque o Litestream precisa controlar quando os checkpoints ocorrem.
Por que database is locked ainda ocorre depois que você define busy_timeout
Esta é a falha que faz as pessoas voltarem ao Postgres, e ela tem uma causa específica.
Um timeout de ocupação instala um busy handler, mas o SQLite não garante que irá chamá-lo.
Se o SQLite determinar que chamar o busy handler pode resultar em um deadlock, ele retornará SQLITE_BUSY à aplicação em vez de chamar o busy handler.
O deadlock que ele evita ocorre quando uma transação é promovida. Um BEGIN sem outras cláusulas no SQLite significa BEGIN DEFERRED. Se a primeira instrução depois dele for um SELECT, você estará em uma transação de leitura. Quando um UPDATE posterior na mesma transação precisar se tornar uma transação de escrita, e outra conexão tiver escrito desde o início da sua leitura, o SQLite não poderá fazer você esperar, porque o snapshot já estará desatualizado e esperar apenas faria as duas conexões entrarem em deadlock. A documentação declara o resultado diretamente:
As instruções de escrita subsequentes promoverão a transação para uma transação de escrita, se possível, ou retornarão SQLITE_BUSY.
O timeout de 5000 milissegundos nunca é consultado. O erro chega imediatamente, por isso parece que a configuração não teve efeito.
A correção tem uma palavra.
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE obtém o bloqueio de escrita no início, antes de ler qualquer coisa. Não há promoção, portanto não há deadlock a evitar. Assim, o busy handler é aplicado e a conexão espera a sua vez em vez de falhar. Mantenha as transações somente de leitura como deferred. Qualquer transação que contenha uma escrita deve ser immediate.
A segunda causa dos erros de bloqueio é mais difícil de identificar: manter uma transação de escrita aberta durante uma operação lenta. O SQLite serializa os escritores. Portanto, uma transação que é aberta, chama uma API externa pela rede e depois faz commit bloqueará todos os outros escritores durante toda essa chamada. Leia o que for necessário, feche a transação, execute a operação lenta e depois abra uma transação de escrita curta para armazenar o resultado.
Backup contínuo com Litestream
Uma cópia noturna pode perder até um dia de gravações, e executar cp em um banco de dados SQLite ativo pode produzir uma cópia que não será aberta. Há duas opções seguras. sqlite3 app.db ".backup /path/to/backup.db" usa a interface de backup online do SQLite e funciona com um banco de dados em uso. O Litestream vai além: monitora o WAL e envia as alterações continuamente para o armazenamento de objetos. Isso reduz a perda de dados no pior caso de um dia para cerca de um segundo.
O Litestream é um único binário Go que é executado ao lado da aplicação. Ele não fica entre a aplicação e o banco de dados. A aplicação grava no SQLite exatamente como antes, e o Litestream lê o WAL e envia o que foi alterado.
cd /tmp
curl -fsSL -O https://github.com/benbjohnson/litestream/releases/download/v0.5.14/litestream-0.5.14-linux-x86_64.deb
sudo dpkg -i litestream-0.5.14-linux-x86_64.deb
litestream versionv0.5.14 é a versão documentada pela página oficial de instalação para Linux em julho de 2026, e v0.5.15 foi lançada em 21 July 2026. Altere a versão nas duas linhas para corresponder à tag atual na página de releases e use o pacote arm64 correspondente se o seu VPS for arm64.
O arquivo de configuração fica em /etc/litestream.yml. Comece com uma réplica em um arquivo local, porque isso comprova todo o fluxo sem exigir credenciais de nuvem.
dbs:
- path: /home/appuser/app/app.db
replica:
type: file
path: /var/backups/litestream/appObserve que o campo é replica, no singular. O Litestream 0.5 substituiu o array replicas da série 0.3 por um único bloco de réplica, e uma configuração com duas entradas agora falha na inicialização. Muitos guias de terceiros ainda mostram o array antigo. Portanto, copie a estrutura acima em vez do primeiro exemplo encontrado em uma pesquisa. A série 0.5 também renomeou o subcomando litestream wal para litestream ltx, porque o formato de backup em disco foi alterado.
Verifique se a configuração é analisada corretamente antes de habilitar qualquer recurso.
sudo litestream databases -config /etc/litestream.ymlEm seguida, teste manualmente o ciclo completo. Esta forma ignora o arquivo de configuração e replica um banco de dados para um único caminho.
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/appEsse comando é executado em primeiro plano e continua em execução. Em outro shell, grave uma linha e restaure a réplica em um arquivo novo.
sqlite3 ~/app/app.db "INSERT INTO notes (body) VALUES ('written after replication started');"
litestream restore -o /tmp/restored.db file:///tmp/replica/app
sqlite3 /tmp/restored.db "SELECT count(*) FROM notes;"A contagem inclui a nova linha. Se não incluir, a alteração ainda não foi sincronizada: o Litestream envia alterações em um sync-interval que tem o padrão de 1 segundo. Aguarde e restaure novamente. Esse segundo também é o seu ponto de recuperação. Uma falha perde no máximo as gravações do último intervalo de sincronização, e nenhuma configuração elimina essa perda.
Para usar armazenamento real, substitua o bloco de réplica por uma URL S3. Isso funciona com o Amazon S3 e com armazenamento de objetos compatível com S3 de outros provedores.
dbs:
- path: /home/appuser/app/app.db
replica:
url: s3://your-bucket-name/app
region: us-east-1
snapshot:
interval: 24h
retention: 24hMantenha as credenciais fora desse arquivo. O Litestream lê LITESTREAM_ACCESS_KEY_ID e LITESTREAM_SECRET_ACCESS_KEY do ambiente. Portanto, coloque-as em um drop-in do systemd pertencente a root, com modo 600.
Os valores de snapshot acima são os padrões, e o padrão de retenção costuma surpreender. Retenção é o período durante o qual o Litestream mantém os snapshots e os arquivos pertencentes a eles. Portanto, também determina até quando no passado você pode restaurar. Vinte e quatro horas significa que uma migração incorreta percebida na manhã de quarta-feira já não pode ser recuperada usando o estado de segunda-feira. Defina retention: 168h como uma semana e pague pelo armazenamento adicional.
Comprove a restauração antes de precisar dela
litestream restore -o /tmp/check.db /home/appuser/app/app.db
sqlite3 /tmp/check.db "PRAGMA integrity_check;"
sqlite3 /tmp/check.db "SELECT count(*) FROM notes;"Dado um caminho de banco de dados, litestream restore consulta a réplica correspondente em /etc/litestream.yml e a baixa. PRAGMA integrity_check exibe ok para um arquivo íntegro, e qualquer outra saída significa que a cópia restaurada não pode ser usada. Execute isso regularmente com um serviço e um timer do systemd e leia a saída. Até restaurar um backup pelo menos uma vez, você não sabe se ele funciona.
Executar o Litestream com systemd
O pacote Debian instala uma unidade litestream que lê /etc/litestream.yml.
sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -fUma saída normal lista cada banco de dados definido na configuração e depois permanece silenciosa, exceto pelas linhas periódicas de sincronização. Um erro no such file or directory no caminho do banco de dados indica que o caminho na configuração está incorreto ou que o processo não consegue lê-lo. Por padrão, a unidade é executada como root, o que concede mais privilégios do que esta tarefa precisa. O Litestream precisa conseguir ler e gravar tanto no banco de dados quanto no diretório que o contém, porque trabalha com os arquivos -wal e -shm ao lado do banco de dados. Portanto, atribua à unidade a conta que sua aplicação já utiliza.
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuserAplique isso com sudo systemctl daemon-reload e sudo systemctl restart litestream. Configurar uma conta de serviço dedicada com privilégios mínimos leva alguns minutos. Essa configuração diferencia um agente de backup de um segundo processo root no servidor.
Um detalhe da ordem é importante se você precisar reconstruir a máquina do zero. O banco de dados deve ser restaurado antes de a aplicação ser iniciada. litestream restore aceita -if-db-not-exists, que retorna 0 quando o arquivo já existe. Portanto, é seguro executá-lo em toda inicialização. Coloque-o em uma linha ExecStartPre na unidade da aplicação, e uma nova VPS baixará o banco de dados enquanto uma máquina existente não fará nada. litestream replicate tem uma opção -restore-if-db-not-exists correspondente, caso você prefira manter isso em um único local.
Onde o SQLite apresenta limitações em um VPS
Sistemas de arquivos de rede. Este é o limite que não pode ser contornado por configuração. O modo WAL exige que todos os processos que usam o banco de dados compartilhem uma pequena região de memória, fornecida pelo arquivo -shm. A documentação do SQLite declara esta regra sem exceções:
Todos os processos que usam um banco de dados devem estar no mesmo computador host; o WAL não funciona em um sistema de arquivos de rede.
Portanto, um banco de dados em um compartilhamento NFS (sistema de arquivos de rede) ou SMB montado pode ser corrompido, e nenhum pragma impede isso. Há uma distinção que costuma ser ignorada. Um dispositivo de bloco de rede, que é como a maioria dos provedores de VPS anexa armazenamento adicional, aparece para o Linux como um disco comum com um sistema de arquivos comum, e isso funciona. Um compartilhamento de arquivos montado não funciona.
Um segundo servidor de aplicação. Nenhuma configuração faz isso funcionar. Quando duas máquinas precisam servir os mesmos dados, você precisa de um banco de dados que se comunique pela rede. Tome essa decisão enquanto ainda houver tempo para planejá-la.
Cargas de trabalho com muitas gravações. Um gravador por vez é uma propriedade do formato do arquivo, não uma opção ajustável. Gravações curtas têm baixo custo porque cada commit é anexado ao WAL, portanto, o throughput depende mais da latência de pequenas gravações do disco do que da CPU. Consulte NVMe comparado ao armazenamento SSD SATA em um VPS para ver essa diferença. Transações longas são o problema real, porque colocam todos os outros gravadores em fila.
Consultas analíticas. O SQLite é um banco orientado a linhas, criado para transações. Um dashboard que examina cem milhões de linhas é uma tarefa diferente para uma ferramenta diferente, e DuckDB comparado ao SQLite para uso em servidores explica onde essa distinção se aplica.
VACUUM durante a replicação. Um VACUUM completo reescreve todo o arquivo do banco de dados. Isso significa que o Litestream precisa fazer o upload do arquivo inteiro novamente, e a documentação do Litestream recomenda não executá-lo no local enquanto a replicação estiver ativa. Pare o replicador, execute o vacuum, inicie-o novamente e espere um novo snapshot completo.
Dois replicadores no mesmo banco de dados. Nunca execute dois processos do Litestream no mesmo banco de dados ou no mesmo destino de réplica. A documentação declara explicitamente que impedir isso é sua responsabilidade, e o resultado é uma réplica que não pode ser restaurada.
O que o Litestream não cobre
O Litestream protege o arquivo do banco de dados e nada mais. Os arquivos enviados, a configuração da aplicação, os certificados TLS (transport layer security) e os arquivos de unidade ainda precisam ser administrados por você. Combine-o com backups criptografados fora do servidor usando restic em uma programação, e as duas partes ficarão cobertas. Se a máquina for nova, os primeiros dez minutos em um novo VPS cobre a configuração da conta de usuário e do firewall que este guia pressupõe já concluída.
FAQ
O SQLite é suficiente para uma aplicação em produção?
Para uma aplicação em um servidor, sim, desde que você ative o modo WAL, defina um tempo limite de espera e faça backups contínuos. Os limites relevantes são estruturais: apenas um gravador por vez e uma única máquina host. Uma aplicação que se enquadra nesses limites obtém um banco de dados sem salto de rede e sem um processo separado para monitorar. Uma aplicação que não se enquadra precisa de um banco de dados cliente-servidor, e nenhum ajuste muda isso.
Por que ainda recebo database is locked depois de definir busy_timeout?
Porque o SQLite ignora o busy handler quando a espera pode causar um deadlock. Uma transação que começa com um BEGIN simples é adiada: um SELECT inicial a coloca em uma transação de leitura, e uma gravação posterior precisa fazer upgrade. Se outra conexão gravou nesse intervalo, o SQLite retorna SQLITE_BUSY imediatamente em vez de chamar o busy handler, pois o snapshot de leitura já está desatualizado. Inicie com BEGIN IMMEDIATE qualquer transação que fará uma gravação, para que o bloqueio de gravação seja obtido no início e o tempo limite seja aplicado.
Posso manter meu banco de dados SQLite em armazenamento de rede?
Não em um sistema de arquivos de rede, como NFS ou SMB. O modo WAL precisa que todos os processos compartilhem memória por meio do arquivo -shm, e a documentação do SQLite declara que todo processo que usa o banco de dados deve estar no mesmo computador host. Um dispositivo de bloco de rede anexado pelo provedor é diferente: o Linux o vê como um disco normal com um sistema de arquivos normal, e o SQLite funciona nele.
Preciso do Litestream se já executo backups noturnos?
Depende de quanto de dados você pode perder. Um job noturno pode causar a perda de até vinte e quatro horas de gravações. O Litestream sincroniza aproximadamente uma vez por segundo, portanto uma falha custa aproximadamente o último segundo. Ele também é mais seguro do que copiar o arquivo do banco de dados com cp, que pode capturar o banco durante uma gravação. O Litestream cobre apenas o banco de dados, portanto mantenha também um backup geral de arquivos em execução.