SQLite in produzione su un VPS: quando conviene
SQLite è adatto alla maggior parte delle piccole app su un VPS: scopri WAL, busy_timeout, replica con Litestream e i limiti che richiedono un altro database.
Quando SQLite è la scelta giusta come database di produzione su un VPS
Usare SQLite in produzione su un VPS è la scelta giusta per la maggior parte delle applicazioni di piccole dimensioni. Il motivo è semplice: un processo su una macchina che scrive in un unico file non ha bisogno di un server di database. Non c’è alcun demone da supervisionare, nessuna porta da filtrare con il firewall, nessuna password da ruotare e nessuna seconda macchina da mantenere operativa. Una query è una chiamata di funzione, non un round trip di rete. Una pagina che esegue quaranta query comporta quindi quaranta chiamate di funzione.
Il limite è circoscritto, ma concreto. SQLite consente un solo writer alla volta sull’intero file del database e il file non può essere condiviso tra due macchine. Entrambi i limiti sono compatibili con un singolo VPS che esegue una sola applicazione. Diventano entrambi bloccanti non appena l’architettura supera questo modello. Questa guida tratta le impostazioni che rendono SQLite sicuro su un server, il backup continuo con Litestream e il punto in cui è necessario fermarsi.
Installa prima lo strumento da riga di comando. Tutto ciò che segue è stato eseguito su Ubuntu 24.04.
sudo apt update
sudo apt install -y sqlite3
sqlite3 --versionIl comando stampa una versione che inizia con 3., seguita da una data di build e da un hash del codice sorgente. A ottobre 2026 Ubuntu 24.04 distribuisce SQLite 3.45.1. Probabilmente la tua applicazione non usa questo binario: la maggior parte dei runtime dei linguaggi include una propria copia della libreria SQLite, spesso più recente. Controlla quindi la versione riportata dal driver del database prima di fare affidamento su una funzionalità recente.
Perché la modalità WAL è la prima impostazione da modificare
Per impostazione predefinita, SQLite usa un rollback journal. Prima di modificare una pagina, copia la pagina originale in un file -journal, quindi modifica direttamente il database. Per eseguire questa operazione in sicurezza, acquisisce un lock esclusivo sull'intero file. Di conseguenza, ogni lettore attende mentre è in corso una scrittura. Su un laptop questo comportamento passa inosservato. Su un web server, una scrittura lenta rallenta ogni richiesta che accede al database.
La modalità WAL (write-ahead log) inverte l'ordine delle operazioni. Un writer aggiunge le nuove pagine a un file -wal separato e lascia invariato il database principale. I reader continuano a leggere il file principale nello snapshot acquisito all'inizio. In questo modo, i reader non bloccano il writer e il writer non blocca i reader. In seguito, un checkpoint copia le pagine accumulate nel WAL nel database principale. Questa singola modifica è il fattore principale che rende SQLite utilizzabile dietro un'applicazione web.
Attivare la modalità WAL e verificare che sia persistente
mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"Il comando stampa wal. Questo output non è ornamentale. PRAGMA journal_mode restituisce la modalità effettiva del database, quindi una risposta delete indica che la modifica non è riuscita e che il database usa ancora il rollback journal.
La modalità WAL è persistente. È un flag nell'header del database, non un'impostazione della connessione. Per questo motivo va configurata una sola volta per ogni file di database e viene ereditata da tutte le connessioni successive, anche dopo un riavvio. Verificatelo usando una nuova connessione.
sqlite3 ~/app/app.db "PRAGMA journal_mode;"Ora create una tabella e controllate quali file vengono creati su 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/Ora sono presenti tre file: app.db, app.db-wal e app.db-shm. Il file -wal contiene le pagine con commit che non sono ancora state sottoposte a checkpoint. Il file -shm è un indice di memoria condivisa che ogni connessione mappa, in modo che tutte concordino sul contenuto del WAL. Entrambi fanno parte del database e non sono file temporanei. Se copiate solo app.db mentre l'applicazione è in esecuzione, ottenete un file privo di tutti i commit più recenti. Se eliminate app.db e lasciate gli altri due file al loro posto, SQLite applicherà quelle pagine WAL obsolete al nuovo file creato con quel nome. È così che si corrompe un database vuoto quando si tenta di reimpostarlo.
Impostazioni di connessione necessarie per ogni applicazione in produzione
Solo journal_mode viene salvato nel database. Tutte le altre impostazioni riportate di seguito sono specifiche della connessione. L'applicazione deve quindi eseguirle su ogni connessione che apre, incluse tutte le connessioni create in background da un pool.
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;busy_timeout = 5000 indica a SQLite di continuare a ritentare l'accesso a un database bloccato per un massimo di 5000 millisecondi prima di restituire database is locked. Il valore predefinito è 0. Per impostazione predefinita, quindi, SQLite fallisce immediatamente quando due operazioni di scrittura si sovrappongono. Impostare questo singolo valore elimina la maggior parte degli errori di blocco attribuiti a SQLite.
synchronous = NORMAL è l'impostazione corretta in modalità WAL, ma è importante comprenderne il compromesso. Con FULL, SQLite chiama fsync sul WAL a ogni commit. Con NORMAL, esegue la sincronizzazione durante i checkpoint. La documentazione di SQLite è esplicita su ciò a cui si rinuncia: dopo un'interruzione dell'alimentazione o un riavvio forzato, le transazioni non sono più persistenti. La perdita di alimentazione non può corrompere il database; si perdono semplicemente gli ultimi commit che non erano ancora stati scritti sul disco. Su un VPS questo è generalmente il compromesso corretto, perché rimuove una chiamata a fsync dal percorso di ogni singola scrittura.
foreign_keys = ON è disattivato per impostazione predefinita per motivi di compatibilità con le versioni precedenti ed è specifico della connessione. Uno schema pieno di clausole REFERENCES non applica alcun vincolo finché ogni connessione non abilita questa impostazione.
Un'altra impostazione diventa importante solo in seguito. SQLite esegue automaticamente un checkpoint quando il WAL supera 1000 pagine. L'operazione viene eseguita dalla connessione che in quel momento completa una transazione. Questo comportamento è normalmente adeguato. Diventa rilevante quando è in esecuzione Litestream, perché Litestream deve poter controllare quando vengono eseguiti i checkpoint.
Perché database is locked si verifica ancora dopo aver impostato busy_timeout
Questo è l'errore che spinge gli utenti a tornare a Postgres e ha una causa precisa.
Un busy timeout installa un busy handler, ma SQLite non garantisce di chiamarlo.
Se SQLite determina che l'invocazione del busy handler potrebbe causare un deadlock, restituisce SQLITE_BUSY all'applicazione invece di invocare il busy handler.
Il deadlock che SQLite evita si verifica quando una transazione viene convertita. In SQLite, un BEGIN senza altre clausole significa BEGIN DEFERRED. Se la prima istruzione eseguita dopo è un SELECT, ci si trova in una transazione di lettura. Quando un successivo UPDATE nella stessa transazione deve diventare una transazione di scrittura e un'altra connessione ha eseguito una scrittura dall'inizio della lettura, SQLite non può farvi attendere. Lo snapshot non è più aggiornato e l'attesa causerebbe soltanto un deadlock tra le due connessioni. La documentazione descrive direttamente il risultato:
Le istruzioni di scrittura successive aggiorneranno la transazione trasformandola in una transazione di scrittura, se possibile, oppure restituiranno SQLITE_BUSY.
Il timeout di 5000 millisecondi non viene mai consultato. L'errore arriva immediatamente, perciò sembra che l'impostazione non abbia avuto effetto.
La correzione consiste in una parola.
BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;BEGIN IMMEDIATE acquisisce il write lock all'inizio, prima di eseguire qualsiasi lettura. Non è necessaria alcuna conversione, quindi non c'è alcun deadlock da evitare. Il busy handler viene applicato e la connessione attende il proprio turno invece di fallire. Lasciare differite le transazioni di sola lettura. Qualsiasi transazione che contiene una scrittura deve essere immediate.
La seconda causa degli errori di lock è più difficile da individuare: mantenere aperta una transazione di scrittura durante operazioni lente. SQLite serializza le scritture, quindi una transazione che si apre, chiama un'API esterna tramite la rete e poi esegue il commit blocca ogni altro writer per tutta la durata della chiamata. Leggere i dati necessari, chiudere la transazione, eseguire l'operazione lenta, quindi aprire una breve transazione di scrittura per salvare il risultato.
Backup continuo con Litestream
Una copia notturna può perdere fino a un giorno di scritture, mentre eseguire cp su un database SQLite in uso può produrre una copia che non si apre. Esistono due opzioni sicure. sqlite3 app.db ".backup /path/to/backup.db" usa l'interfaccia di backup online di SQLite e funziona su un database in uso. Litestream va oltre: monitora il WAL e invia continuamente le modifiche allo storage a oggetti, riducendo la perdita di dati massima da un giorno a circa un secondo.
Litestream è un unico binario Go che viene eseguito accanto all'applicazione. Non si interpone tra l'applicazione e il database. L'applicazione scrive in SQLite esattamente come prima, mentre Litestream legge il WAL e carica le modifiche.
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 è ancora la release documentata dalla pagina ufficiale di installazione per Linux a ottobre 2026. Nel frattempo sono state pubblicate release più recenti: v0.5.15 il 21 luglio 2026 e v0.5.17, il tag più recente, il 31 agosto 2026. Modifica la versione in entrambe le righe in modo che corrisponda al tag corrente nella pagina delle release. Se il tuo VPS usa arm64, scarica invece il pacchetto arm64 corrispondente.
Il file di configurazione si trova in /etc/litestream.yml. Inizia con una replica in un file locale. In questo modo puoi verificare l'intero flusso senza credenziali cloud.
dbs:
- path: /home/appuser/app/app.db
replica:
type: file
path: /var/backups/litestream/appIl campo è replica, al singolare. Litestream 0.5 ha sostituito l'array replicas della serie 0.3 con un unico blocco di replica. Una configurazione con due elementi ora causa un errore all'avvio. Molte guide di terze parti mostrano ancora il vecchio array. Usa quindi la struttura riportata sopra, invece del primo esempio restituito dalla ricerca. La serie 0.5 ha anche rinominato il sottocomando litestream wal in litestream ltx, perché il formato del backup su disco è cambiato.
Verifica che la configurazione venga analizzata correttamente prima di abilitare qualsiasi operazione.
sudo litestream databases -config /etc/litestream.ymlPoi verifica manualmente il percorso completo. Questa forma ignora il file di configurazione e replica un database in un unico percorso.
mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/appIl comando viene eseguito in primo piano e continua a funzionare. In una seconda shell, scrivi una riga e ripristina la replica in un nuovo file.
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;"Il conteggio include la nuova riga. Se non la include, la modifica non è ancora stata sincronizzata. Litestream invia le modifiche a un intervallo sync-interval che per impostazione predefinita è di 1 secondo. Attendi e ripeti il ripristino. Quel secondo è anche il tuo punto di ripristino. In caso di arresto anomalo, perdi al massimo le scritture dell'ultimo intervallo di sincronizzazione. Nessuna configurazione può ridurre questa perdita a zero.
Per lo storage reale, sostituisci il blocco della replica con un URL S3. Funziona con Amazon S3 e con storage a oggetti compatibile con S3 fornito da altri provider.
dbs:
- path: /home/appuser/app/app.db
replica:
url: s3://your-bucket-name/app
region: us-east-1
snapshot:
interval: 24h
retention: 24hNon inserire le credenziali in quel file. Litestream legge LITESTREAM_ACCESS_KEY_ID e LITESTREAM_SECRET_ACCESS_KEY dall'ambiente. Inseriscile quindi in un drop-in di systemd di proprietà di root, con modalità 600.
I valori dello snapshot riportati sopra sono quelli predefiniti, ma il valore predefinito della conservazione può sorprendere. La conservazione indica per quanto tempo Litestream mantiene gli snapshot e i file associati. Indica quindi anche quanto indietro nel tempo puoi eseguire un ripristino. Ventiquattro ore significano che una migrazione errata rilevata mercoledì mattina non è più recuperabile dallo stato di lunedì. In Litestream 0.5, la conservazione è un'impostazione globale nel blocco radice snapshot, non un'impostazione per replica. Per conservare i dati per una settimana, modifica quel blocco in retention: 168h e considera lo spazio di storage aggiuntivo. Una riga retention: all'interno del blocco della replica appartiene alla sintassi della serie 0.3. Litestream 0.5 la ignora senza segnalare errori e mantiene il valore predefinito di 24 ore.
Verifica il ripristino prima di averne bisogno
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;"Dato il percorso di un database, litestream restore individua la replica corrispondente in /etc/litestream.yml e la scarica. PRAGMA integrity_check stampa ok per un file integro; qualsiasi altro output indica che la copia ripristinata non è utilizzabile. Esegui questa verifica secondo una pianificazione, usando un servizio e un timer systemd, e controlla l’output. Finché non hai ripristinato almeno un backup, non puoi sapere se funziona.
Eseguire Litestream con systemd
Il pacchetto Debian installa un'unità litestream che legge /etc/litestream.yml.
sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -fUn output corretto elenca ogni database definito nella configurazione e poi rimane silenzioso, salvo le righe di sincronizzazione periodiche. Un errore no such file or directory relativo al percorso del database indica che il percorso nella configurazione è errato oppure che il processo non può leggerlo. Per impostazione predefinita, l'unità viene eseguita come root, con privilegi superiori a quelli necessari per questo compito. Litestream deve poter leggere e scrivere sia il database sia la directory che lo contiene, perché utilizza i file -wal e -shm accanto al database. Per questo, assegnategli l'account utente con cui viene già eseguita l'applicazione, appuser in questi esempi.
# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuserApplicate la configurazione con sudo systemctl daemon-reload e sudo systemctl restart litestream. Configurare un account di servizio dedicato con privilegi minimi richiede pochi minuti e fa la differenza tra un agente di backup e un secondo processo root sul server.
Un dettaglio relativo all'ordine delle operazioni è importante se ricostruite la macchina da zero. Il database deve essere ripristinato prima dell'avvio dell'applicazione. litestream restore accetta -if-db-not-exists, che restituisce 0 quando il file esiste già; è quindi sicuro eseguirlo a ogni avvio. Inseritelo in una riga ExecStartPre nell'unità dell'applicazione: un VPS nuovo scaricherà il database, mentre uno esistente non eseguirà alcuna operazione. litestream replicate dispone di un flag -restore-if-db-not-exists corrispondente, se preferite mantenere questa configurazione in un unico punto.
Dove SQLite non è adatto su un VPS
File system di rete. Questo è il limite che non si può aggirare con la configurazione. La modalità WAL richiede che ogni processo che usa il database condivida una piccola area di memoria, fornita dal file -shm. La documentazione di SQLite enuncia la regola senza eccezioni:
Tutti i processi che usano un database devono trovarsi sullo stesso computer host; WAL non funziona su un file system di rete.
Un database su una condivisione NFS (network file system) o SMB montata può quindi corrompersi e nessun pragma lo impedisce. È importante distinguere i due casi. Un dispositivo a blocchi di rete, utilizzato dalla maggior parte dei provider VPS per collegare spazio di archiviazione aggiuntivo, viene esposto a Linux come un normale disco con un normale file system, quindi non presenta questo problema. Una condivisione di file montata è invece incompatibile.
Un secondo server applicativo. Nessuna impostazione rende possibile questo scenario. Quando due macchine devono fornire gli stessi dati, serve un database che comunichi attraverso la rete. Pianifica questa migrazione quando hai ancora tempo per farlo.
Carichi con molte scritture. Il fatto che possa scrivere un solo processo alla volta è una proprietà del formato dei file, non un parametro regolabile. Le scritture brevi sono efficienti perché ogni commit viene aggiunto al WAL, quindi il throughput dipende più dalla latenza del disco nelle piccole scritture che dalla CPU. Consulta NVMe e storage SSD SATA su un VPS per vedere la differenza. Il problema reale sono le transazioni lunghe, perché mettono in coda tutti gli altri processi di scrittura.
Query analitiche. SQLite è un database orientato alle righe, progettato per le transazioni. Un dashboard che analizza cento milioni di righe richiede uno strumento diverso per un'attività diversa; DuckDB a confronto con SQLite per l'uso sui server descrive il punto in cui questa distinzione diventa rilevante.
VACUUM durante la replica. Un VACUUM completo riscrive l'intero file del database. Questo invalida il rilevamento a livello di pagina su cui si basa Litestream, e la documentazione di Litestream sconsiglia di eseguirlo mentre la replica è attiva. La documentazione consiglia VACUUM INTO, che scrive una copia compattata in un file separato e lascia invariato l'originale. Se devi eseguire il vacuum sul file originale, la procedura documentata è più lunga di un riavvio: arresta il replicatore, esegui il vacuum, elimina la directory nascosta di tracciamento di Litestream accanto al database e i dati della replica remota esistente, quindi riavvia il replicatore per iniziare da una nuova snapshot completa. Questo passaggio elimina la cronologia di ripristino disponibile fino a quel momento.
Due replicatori sullo stesso database. Non eseguire mai due processi Litestream sullo stesso database o verso la stessa destinazione di replica. La documentazione specifica chiaramente che impedire questa configurazione è tua responsabilità. Il risultato sarebbe una replica che non puoi ripristinare.
Cosa non copre Litestream
Litestream protegge il file del database e nient’altro. I file caricati, la configurazione dell’applicazione, i certificati TLS (transport layer security) e i file delle unità restano a tuo carico. Abbinalo a backup cifrati fuori dal server con restic eseguiti secondo una pianificazione, così coprirai entrambi gli aspetti. Se la macchina è nuova, i primi dieci minuti su un nuovo VPS illustrano la configurazione dell’account utente e del firewall che questa guida presuppone già completata.
FAQ
SQLite è sufficiente per un'applicazione in produzione?
Per un'applicazione su un solo server, sì, a condizione di attivare la modalità WAL, impostare un timeout di attesa e creare backup continui. I limiti importanti sono strutturali: un solo writer alla volta e una sola macchina host. Un'applicazione che rientra in questi limiti utilizza un database senza passaggi di rete e senza un processo separato da monitorare. Un'applicazione che non rientra in questi limiti richiede un database client-server; nessuna ottimizzazione può cambiare questo requisito.
Perché continuo a ricevere database is locked dopo aver impostato busy_timeout?
Perché SQLite ignora il busy handler quando l'attesa potrebbe causare un deadlock. Una transazione che inizia con un semplice BEGIN è deferred: un SELECT iniziale la porta in una transazione di lettura e una scrittura successiva deve effettuare un upgrade. Se nel frattempo un'altra connessione ha eseguito una scrittura, SQLite restituisce immediatamente SQLITE_BUSY invece di chiamare il busy handler, perché lo snapshot di lettura è già obsoleto. Avvia con BEGIN IMMEDIATE qualsiasi transazione che eseguirà scritture, in modo da acquisire subito il write lock e applicare il timeout.
Posso mantenere il database SQLite su uno storage di rete?
Non su un filesystem di rete come NFS o SMB. La modalità WAL richiede che tutti i processi condividano la memoria tramite il file -shm e la documentazione di SQLite stabilisce che ogni processo che utilizza il database deve trovarsi sullo stesso computer host. Un network block device collegato dal provider è un caso diverso: Linux lo vede come un normale disco con un normale filesystem e SQLite funziona correttamente.
Mi serve Litestream se eseguo già backup notturni?
Dipende dalla quantità di dati che puoi permetterti di perdere. Un job notturno può comportare la perdita di fino a ventiquattro ore di scritture. Litestream esegue la sincronizzazione circa una volta al secondo, quindi un crash comporta una perdita approssimativa dell'ultimo secondo. È anche più sicuro della copia del file di database con cp, che può acquisire il database durante una scrittura. Litestream protegge solo il database, quindi mantieni attivo anche un backup generale dei file.