SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-07

SQLite in productie op een VPS: de juiste aanpak

Ontdek waarom SQLite ideaal is voor kleine applicaties op één VPS. Leer hoe u WAL mode en busy_timeout configureert en gebruik Litestream voor betrouwbare back-ups.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Wanneer SQLite de juiste productiedatabase is op een VPS

Het draaien van SQLite in productie op een VPS is voor de meeste kleine applicaties de juiste keuze, en de reden is simpel: één proces op één machine dat naar één bestand schrijft, heeft geen databaseserver nodig. Er is geen daemon om te bewaken, geen poort om te beveiligen met een firewall, geen wachtwoord om te roteren en geen tweede machine om in de lucht te houden. Een query is een functie-aanroep in plaats van een netwerk-roundtrip, dus een pagina die veertig queries uitvoert, kost u veertig functie-aanroepen.

De beperking is specifiek en reëel. SQLite staat één schrijver tegelijk toe voor het gehele databasebestand, en het bestand kan niet worden gedeeld tussen twee machines. Beide limieten zijn acceptabel voor een enkele VPS die één applicatie draait. Beide zijn fataal op het moment dat u uit die opzet groeit. Deze handleiding behandelt de instellingen die SQLite veilig maken op een server, continue back-ups met Litestream, en het punt waarop u moet stoppen.

Installeer eerst de command line tool. Alles hieronder is uitgevoerd op Ubuntu 24.04.

sudo apt update
sudo apt install -y sqlite3
sqlite3 --version

Dit print een versie die begint met 3., gevolgd door een build-datum en een source-hash. Ubuntu 24.04 levert SQLite 3.45.1 per juli 2026. Uw applicatie gebruikt waarschijnlijk niet dit binaire bestand: de meeste language runtimes bundelen hun eigen kopie van de SQLite-bibliotheek, vaak een nieuwere, dus controleer de versie die uw database-driver rapporteert voordat u vertrouwt op een recente functie.

Waarom WAL-modus de eerste instelling is die u wijzigt

Standaard gebruikt SQLite een rollback journal. Voordat een pagina wordt gewijzigd, kopieert het de originele pagina naar een -journal-bestand en bewerkt het vervolgens de database op de huidige locatie. Om dit veilig te doen, wordt een exclusieve lock op het gehele bestand geplaatst, waardoor elke lezer moet wachten zolang er een schrijfactie plaatsvindt. Op een laptop merkt niemand dit. Op een webserver zorgt één trage schrijfactie er echter voor dat elk verzoek dat de database raadpleegt, wordt opgehouden.

WAL-modus (write-ahead log) draait deze volgorde om. Een schrijvend proces voegt de nieuwe pagina's toe aan een afzonderlijk -wal-bestand en laat de hoofddatabase ongemoeid. Lezers blijven het hoofdbestand lezen op basis van de snapshot waarmee zij zijn begonnen; hierdoor blokkeren lezers de schrijver niet en blokkeert de schrijver de lezers niet. Later kopieert een checkpoint de verzamelde WAL-pagina's terug naar de hoofddatabase. Deze enkele wijziging is grotendeels de reden waarom SQLite bruikbaar is achter een webapplicatie.

Schakel WAL-modus in en bevestig dat deze actief is

mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"

Het commando toont wal. Die uitvoer is geen versiering. PRAGMA journal_mode geeft de modus terug waarin de database zich daadwerkelijk bevindt; een antwoord van delete betekent dus dat de wijziging is mislukt en u nog steeds het rollback-journal gebruikt.

WAL-modus is persistent. Het is een vlag in de database-header in plaats van een verbindingsinstelling. U voert dit dus eenmalig uit per databasebestand en elke daaropvolgende verbinding neemt dit over, ook na een herstart. Controleer dit met een nieuwe verbinding.

sqlite3 ~/app/app.db "PRAGMA journal_mode;"

Maak nu een tabel aan en bekijk wat er op de schijf verschijnt.

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/

Er zijn nu drie bestanden: app.db, app.db-wal en app.db-shm. Het bestand -wal bevat gecommitteerde pagina's die nog niet zijn gecheckpoint. Het bestand -shm is een gedeelde geheugenindex die elke verbinding mapt, zodat ze allemaal overeenstemmen over de inhoud van de WAL. Beide horen bij de database en zijn geen tijdelijke bestanden. Kopieer app.db op zichzelf terwijl de applicatie draait en u krijgt een bestand dat alle recente commits mist. Verwijder app.db en laat de andere twee staan; SQLite zal die verouderde WAL-pagina's toepassen op elk nieuw bestand dat onder die naam verschijnt. Dit is hoe men een nieuwe database corrumpeert tijdens een poging deze te resetten.

De verbindingsinstellingen die elke productie-applicatie nodig heeft

Alleen journal_mode wordt opgeslagen in de database. Elke andere instelling hieronder is per verbinding, wat betekent dat uw applicatie deze moet uitvoeren op elke verbinding die wordt geopend, inclusief elke verbinding die een pool op de achtergrond aanmaakt.

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;

busy_timeout = 5000 instrueert SQLite om bij een vergrendelde database tot 5000 milliseconden te blijven proberen voordat database is locked wordt geretourneerd. De standaardwaarde is 0, waardoor SQLite standaard direct faalt zodra twee schrijfacties elkaar overlappen. Het instellen van deze waarde verhelpt de meeste vergrendelingsfouten die ten onrechte aan SQLite zelf worden toegeschreven.

synchronous = NORMAL is de juiste instelling in WAL-modus, en het is belangrijk om de afweging te begrijpen. Bij FULL roept SQLite fsync aan op de WAL bij elke commit. Bij NORMAL synchroniseert het pas bij checkpoints. De documentatie van SQLite is duidelijk over wat u opgeeft: transacties zijn niet langer duurzaam na een stroomstoring of een harde reset. De database raakt niet corrupt door dat stroomverlies, u verliest simpelweg de laatste commits die de schijf nog niet hadden bereikt. Op een VPS is dit meestal de juiste afweging, omdat het een fsync verwijdert uit het pad van elke individuele schrijfactie.

foreign_keys = ON staat standaard uit vanwege achterwaartse compatibiliteit en geldt per verbinding. Een schema vol REFERENCES-clausules dwingt niets af totdat elke verbinding dit inschakelt.

Eén instelling is pas later van belang. SQLite voert automatisch checkpoints uit zodra de WAL groeit tot voorbij 1000 pagina's, en het werk wordt uitgevoerd door de verbinding die op dat moment toevallig een transactie voltooit. Dat is op zichzelf prima. Het wordt pas een punt van aandacht wanneer Litestream draait, omdat Litestream controle wil over het moment waarop checkpoints plaatsvinden.

Waarom database is locked blijft optreden nadat u busy_timeout heeft ingesteld

Dit is de foutmelding die gebruikers terug naar Postgres drijft, en deze heeft één specifieke oorzaak.

Een busy timeout instellen activeert een busy handler, maar SQLite garandeert niet dat deze wordt aangeroepen.

Als SQLite vaststelt dat het aanroepen van de busy handler tot een deadlock kan leiden, zal het direct SQLITE_BUSY teruggeven aan de applicatie in plaats van de busy handler aan te roepen.

De deadlock die wordt vermeden, treedt op bij een transactie-upgrade. Een kale BEGIN in SQLite betekent BEGIN DEFERRED. Als het eerste statement daarna een SELECT is, bevindt u zich in een leestransactie. Wanneer een latere UPDATE in diezelfde transactie een schrijftransactie moet worden, en een andere verbinding heeft geschreven sinds uw leesactie begon, kan SQLite u niet laten wachten. Uw snapshot is immers al verouderd en wachten zou enkel leiden tot een deadlock tussen de twee verbindingen. De documentatie stelt het resultaat direct vast:

Latere schrijstatements zullen de transactie upgraden naar een schrijftransactie indien mogelijk, of SQLITE_BUSY retourneren.

Uw timeout van 5000 milliseconden wordt nooit geraadpleegd. De foutmelding verschijnt onmiddellijk, wat de reden is dat het lijkt alsof de instelling geen effect heeft.

De oplossing is één woord.

BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;

BEGIN IMMEDIATE neemt het schrijflock aan het begin, nog voordat er iets wordt gelezen. Er is geen sprake van een upgrade, dus er is geen deadlock om te vermijden. Hierdoor wordt de busy handler wel toegepast en wacht de verbinding op zijn beurt in plaats van te falen. Houd alleen-lezen transacties op deferred. Elke transactie die een schrijfactie bevat, moet immediate zijn.

De tweede oorzaak van lock-fouten is lastiger te detecteren: het openhouden van een schrijftransactie tijdens trage processen. SQLite serialiseert schrijvers, dus een transactie die opent, een externe API via het netwerk aanroept en vervolgens commit, blokkeert elke andere schrijver gedurende de volledige duur van die aanroep. Lees wat u nodig heeft, sluit de transactie, voer het trage werk uit en open daarna een korte schrijftransactie om het resultaat op te slaan.

Continue back-ups met Litestream

Een dagelijkse kopie betekent dat er tot een dag aan schrijfacties verloren kan gaan, en het uitvoeren van cp op een actieve SQLite-database kan resulteren in een kopie die niet kan worden geopend. Twee methoden zijn veilig. sqlite3 app.db ".backup /path/to/backup.db" gebruikt de online back-upinterface van SQLite en werkt op een database die in gebruik is. Litestream gaat verder: het monitort de WAL en verstuurt wijzigingen continu naar object storage, waardoor het maximale gegevensverlies wordt beperkt van een dag tot ongeveer een seconde.

Litestream is een enkel Go-binary dat naast uw applicatie draait. Het bevindt zich niet tussen de applicatie en de database. Uw applicatie schrijft op precies dezelfde wijze naar SQLite als voorheen, terwijl Litestream de WAL leest en de wijzigingen uploadt.

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 version

v0.5.14 is de release die de officiële Linux-installatiepagina in juli 2026 documenteert, en v0.5.15 volgde op 21 juli 2026. Wijzig het versienummer in beide regels zodat dit overeenkomt met de huidige tag op de releases-pagina, en kies het bijbehorende arm64-pakket als uw VPS op arm64 draait.

Het configuratiebestand bevindt zich op /etc/litestream.yml. Begin met een lokale bestandsreplica, omdat dit de volledige cyclus bewijst zonder dat er cloud-inloggegevens nodig zijn.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      type: file
      path: /var/backups/litestream/app

Let op dat het veld replica is, in het enkelvoud. Litestream 0.5 verving de replicas-array uit de 0.3-serie door een enkel replica-blok; een configuratie met twee vermeldingen faalt nu bij het opstarten. Veel handleidingen van derden tonen nog steeds de oude array, dus kopieer de structuur hierboven in plaats van het eerste voorbeeld dat u via een zoekmachine vindt. De 0.5-serie hernoemde ook het litestream wal-subcommando naar litestream ltx, omdat het back-upformaat op schijf is gewijzigd.

Controleer of de configuratie correct wordt geparseerd voordat u iets inschakelt.

sudo litestream databases -config /etc/litestream.yml

Controleer vervolgens handmatig de volledige cyclus. Deze vorm slaat het configuratiebestand over en repliceert één database naar één pad.

mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/app

Dit proces draait op de voorgrond en blijft actief. Schrijf in een tweede shell een rij en herstel de replica naar een nieuw bestand.

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;"

Het aantal bevat de nieuwe rij. Als dit niet het geval is, is de wijziging nog niet gesynchroniseerd: Litestream pusht op basis van een sync-interval die standaard op 1 seconde staat, dus wacht even en herstel opnieuw. Die ene seconde is tevens uw herstelpunt. Een crash zorgt voor het verlies van maximaal de schrijfacties van het laatste synchronisatie-interval, en er is geen configuratie die dit tot nul reduceert.

Voor daadwerkelijke opslag vervangt u het replica-blok door een S3 URL. Dit werkt met Amazon S3 en met S3-compatibele object storage van andere aanbieders.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      url: s3://your-bucket-name/app
      region: us-east-1

snapshot:
  interval: 24h
  retention: 24h

Houd inloggegevens uit dat bestand. Litestream leest LITESTREAM_ACCESS_KEY_ID en LITESTREAM_SECRET_ACCESS_KEY vanuit de omgeving, dus plaats deze in een systemd drop-in die eigendom is van root met modus 600.

De snapshot-waarden hierboven zijn de standaardwaarden, en de standaardretentie verrast veel gebruikers. Retentie bepaalt hoe lang Litestream snapshots en de bijbehorende bestanden bewaart; het bepaalt dus ook hoe ver u in de tijd kunt terugkeren bij een herstel. Vierentwintig uur betekent dat een foutieve migratie die u op woensdagochtend opmerkt, al niet meer herstelbaar is vanuit de status van maandag. Stel retention: 168h in op een week en betaal voor de extra opslagruimte.

Controleer de restore voordat u deze nodig heeft

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;"

Gegeven een databasepad, zoekt litestream restore de bijbehorende replica op in /etc/litestream.yml en haalt deze binnen. PRAGMA integrity_check print ok bij een gezond bestand; elke andere uitvoer betekent dat de herstelde kopie niet bruikbaar is. Voer dit volgens een schema uit met een systemd service en timer en lees de uitvoer. Totdat u eenmaal een backup heeft hersteld, weet u niet of deze werkt.

Litestream uitvoeren onder systemd

Het Debian-pakket installeert een litestream-unit die /etc/litestream.yml leest.

sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f

Bij een gezonde status noemt de output elke database uit de configuratie en blijft daarna stil, afgezien van periodieke synchronisatieregels. Een foutmelding van no such file or directory voor uw databasepad betekent dat het pad in de configuratie onjuist is, of dat het proces het niet kan lezen. De unit draait standaard als root, wat meer rechten zijn dan deze taak vereist. Litestream moet zowel de database als de map waarin deze zich bevindt kunnen lezen en schrijven, omdat het werkt met de -wal- en -shm-bestanden naast uw database. Wijs daarom het account toe dat uw applicatie al gebruikt.

# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser

Pas dit toe met sudo systemctl daemon-reload en sudo systemctl restart litestream. Het instellen van een toegewezen service-account met minimale rechten kost enkele minuten en vormt het verschil tussen een backup-agent en een tweede root-proces op de server.

Eén detail met betrekking tot de volgorde is van belang als u de machine ooit volledig opnieuw opbouwt. U wilt dat de database is hersteld voordat de applicatie start. litestream restore accepteert -if-db-not-exists, wat de exitcode 0 geeft wanneer het bestand al aanwezig is; het is dus veilig om dit bij elke boot uit te voeren. Plaats dit in een ExecStartPre-regel in de unit van uw applicatie, zodat een nieuwe VPS de database ophaalt terwijl een bestaande VPS niets doet. litestream replicate heeft een bijbehorende -restore-if-db-not-exists-vlag als u dit liever op één plek beheert.

Waar SQLite faalt op een VPS

Netwerkbestandssystemen. Dit is de beperking waar u niet omheen kunt configureren. De WAL-modus vereist dat elk proces dat de database gebruikt, een kleine geheugenregio deelt; dit is wat het -shm-bestand faciliteert. De documentatie van SQLite stelt de regel zonder voorbehoud:

Alle processen die een database gebruiken, moeten zich op dezelfde hostcomputer bevinden; WAL werkt niet via een netwerkbestandssysteem.

Een database op een gemounte NFS (network file system) of SMB-share kan dus corrupt raken, en geen enkele pragma voorkomt dit. Er is hier een onderscheid dat vaak over het hoofd wordt gezien. Een netwerk-block device, wat de meeste VPS-aanbieders als extra opslag koppelen, verschijnt voor Linux als een gewone schijf met een gewoon bestandssysteem, en dat is in orde. Een gemounte file share is dat niet.

Een tweede applicatieserver. Geen enkele instelling maakt dit werkend. Zodra u twee machines nodig heeft die dezelfde data serveren, heeft u een database nodig die via het netwerk communiceert. Besluit tot die overstap terwijl u nog tijd heeft om deze te plannen.

Schrijfintensieve workloads. Eén schrijver tegelijk is een eigenschap van het bestandsformaat, geen instelbare parameter. Korte schrijfacties zijn goedkoop omdat elke commit een toevoeging aan de WAL is; de doorvoer volgt daarom nauwer de latentie van kleine schrijfacties van uw schijf dan die van uw CPU. Zie NVMe versus SATA SSD-opslag op een VPS voor hoe dat verschil eruitziet. Lange transacties zijn het werkelijke probleem, omdat deze elke andere schrijver in de wachtrij plaatsen.

Analytische queries. SQLite is een rij-georiënteerde opslag ontworpen voor transacties. Een dashboard dat honderd miljoen rijen scant, is een andere taak voor een ander hulpmiddel, en DuckDB vergeleken met SQLite voor serverwerk behandelt waar die grens ligt.

VACUUM onder replicatie. Een volledige VACUUM herschrijft het gehele databasebestand, wat betekent dat Litestream het opnieuw volledig moet uploaden. De documentatie van Litestream raadt af om dit uit te voeren terwijl replicatie actief is. Stop de replicator, voer de vacuum uit, start deze opnieuw en houd rekening met een nieuwe volledige snapshot.

Twee replicators op één database. Draai nooit twee Litestream-processen tegen dezelfde database of dezelfde replicabestemming. De documentatie is expliciet dat het voorkomen hiervan uw verantwoordelijkheid is, en het resultaat is een replica die u niet kunt herstellen.

Wat Litestream niet dekt

Litestream beschermt uitsluitend het databasebestand. Geüploade bestanden, de applicatieconfiguratie, TLS (transport layer security)-certificaten en de unit-bestanden moet u zelf beheren. Combineer dit met versleutelde off-box back-ups via restic volgens een vast schema om beide onderdelen af te dekken. Als u een nieuwe machine gebruikt, behandelt de eerste tien minuten op een nieuwe VPS het gebruikersaccount en de firewall-configuratie waarvan deze handleiding uitgaat.

FAQ

Is SQLite geschikt voor een productie-applicatie?

Voor één applicatie op één server wel, mits u de WAL-modus inschakelt, een busy timeout instelt en continu back-ups maakt. De beperkingen die ertoe doen zijn structureel: slechts één schrijver tegelijk en één hostmachine. Een applicatie die binnen deze grenzen past, krijgt een database zonder netwerk-hop en zonder apart proces om te monitoren. Een applicatie die hier niet in past, vereist een client-server database; geen enkele hoeveelheid optimalisatie verandert dat.

Waarom krijg ik nog steeds database is locked na het instellen van busy_timeout?

Omdat SQLite de busy handler overslaat wanneer wachten een deadlock zou kunnen veroorzaken. Een transactie die begint met een kale BEGIN is uitgesteld: een openende SELECT plaatst deze in een leestransactie, en een latere schrijfactie moet deze upgraden. Als een andere verbinding tussentijds heeft geschreven, geeft SQLite direct SQLITE_BUSY terug in plaats van uw busy handler aan te roepen, aangezien uw lees-snapshot dan al verouderd is. Start elke transactie die gaat schrijven met BEGIN IMMEDIATE, zodat de schrijfvergrendeling direct wordt aangevraagd en de timeout van toepassing is.

Kan ik mijn SQLite-database op netwerkopslag bewaren?

Niet op een netwerkbestandssysteem zoals NFS of SMB. De WAL-modus vereist dat alle processen het geheugen delen via het -shm-bestand, en de documentatie van SQLite stelt dat elk proces dat de database gebruikt zich op dezelfde hostcomputer moet bevinden. Een netwerk-block-device dat door uw provider is gekoppeld, is iets anders: Linux ziet een normale schijf met een normaal bestandssysteem, en daarop werkt SQLite wel.

Heb ik Litestream nodig als ik al dagelijkse back-ups draai?

Dat hangt ervan af hoeveel dataverlies u kunt tolereren. Een dagelijkse taak betekent dat u tot vierentwintig uur aan schrijfacties kunt verliezen. Litestream synchroniseert ongeveer één keer per seconde, dus een crash kost u grofweg de laatste seconde. Het is ook veiliger dan het kopiëren van het databasebestand met cp, wat een database midden in een schrijfactie kan vastleggen. Litestream dekt alleen de database, dus houd daarnaast een algemene bestandsback-up bij.

#sqlite#wal#litestream#backups#production