SSD Nodes Learn 8GB de RAM — $66/an
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-02

SQLite en production sur un VPS : le bon choix ?

SQLite convient à la plupart des petites apps sur un VPS unique. Configurez WAL et busy_timeout, ajoutez la réplication Litestream et repérez ses limites.

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

Quand SQLite est le bon choix pour une base de données de production sur un VPS

Utiliser SQLite en production sur un VPS est le bon choix pour la plupart des petites applications. La raison est simple : un seul processus sur une seule machine, qui écrit dans un seul fichier, n’a pas besoin d’un serveur de base de données. Il n’y a aucun daemon à superviser, aucun port à filtrer avec le firewall, aucun mot de passe à renouveler et aucune seconde machine à maintenir en fonctionnement. Une requête est un appel de fonction, et non un aller-retour réseau. Une page qui exécute quarante requêtes vous coûte donc quarante appels de fonction.

La limite est précise et bien réelle. SQLite n’autorise qu’un seul writer à la fois pour l’ensemble du fichier de base de données, et le fichier ne peut pas être partagé entre deux machines. Ces deux limites ne posent aucun problème pour un seul VPS exécutant une seule application. Elles deviennent rédhibitoires dès que vous dépassez cette architecture. Ce guide présente les paramètres qui rendent SQLite fiable sur un serveur, la sauvegarde continue avec Litestream et le moment où vous devez arrêter d’utiliser SQLite.

Installez d’abord l’outil en ligne de commande. Tout ce qui suit a été exécuté sur Ubuntu 24.04.

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

Cette commande affiche une version commençant par 3., suivie d’une date de build et d’un hash des sources. En juillet 2026, Ubuntu 24.04 fournit SQLite 3.45.1. Votre application n’utilise probablement pas ce binaire : la plupart des runtimes fournissent leur propre copie de la bibliothèque SQLite, souvent plus récente. Vérifiez donc la version indiquée par votre database driver avant de dépendre d’une fonctionnalité récente.

Pourquoi le mode WAL est la première chose à modifier

Par défaut, SQLite utilise un rollback journal. Avant de modifier une page, il copie la page d’origine dans un fichier -journal, puis modifie directement la base de données. Pour effectuer cette opération en toute sécurité, il prend un verrou exclusif sur l’ensemble du fichier. Chaque lecteur attend donc tant qu’une écriture est en cours. Sur un ordinateur portable, cela passe inaperçu. Sur un serveur web, une écriture lente bloque chaque requête qui accède à la base de données.

Le mode WAL (write-ahead log) inverse l’ordre des opérations. Un writer ajoute les nouvelles pages à un fichier -wal distinct et laisse la base de données principale inchangée. Les lecteurs continuent de lire le fichier principal dans l’état correspondant au snapshot qu’ils ont commencé à utiliser. Les lecteurs ne bloquent donc pas le writer, et le writer ne bloque pas les lecteurs. Ensuite, un checkpoint recopie les pages WAL accumulées dans la base de données principale. Cette seule modification suffit dans la plupart des cas à rendre SQLite utilisable derrière une application web.

Activer le mode WAL et vérifier qu’il est bien appliqué

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

La commande affiche wal. Ce résultat n’est pas décoratif. PRAGMA journal_mode renvoie le mode réellement utilisé par la base de données. Une réponse delete signifie donc que la modification a échoué et que vous utilisez toujours le rollback journal.

Le mode WAL est persistant. Il s’agit d’un indicateur dans l’en-tête de la base de données, et non d’un paramètre de connexion. Vous l’exécutez donc une seule fois par fichier de base de données. Toutes les connexions suivantes l’utilisent, y compris après un redémarrage. Vérifiez-le avec une nouvelle connexion.

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

Créez maintenant une table et observez les fichiers qui apparaissent sur le disque.

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/

Il y a maintenant trois fichiers : app.db, app.db-wal et app.db-shm. Le fichier -wal contient les pages validées qui n’ont pas encore été traitées par un checkpoint. Le fichier -shm est un index de mémoire partagée que chaque connexion mappe, afin qu’elles aient toutes la même vue du contenu du WAL. Ces deux fichiers appartiennent à la base de données. Ce ne sont pas des fichiers temporaires. Si vous copiez uniquement app.db pendant que l’application fonctionne, vous obtenez un fichier auquel il manque tous les commits récents. Si vous supprimez app.db en laissant les deux autres fichiers en place, SQLite applique ces anciennes pages du WAL au nouveau fichier créé sous ce nom. C’est ainsi qu’une base de données vierge est corrompue lors d’une tentative de réinitialisation.

Les paramètres de connexion nécessaires à toute application en production

Seul journal_mode est stocké dans la base de données. Tous les autres paramètres ci-dessous sont propres à chaque connexion. Votre application doit donc les définir sur chaque connexion qu’elle ouvre, y compris sur chaque connexion créée en arrière-plan par un pool.

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

busy_timeout = 5000 indique à SQLite de continuer à réessayer lorsque la base de données est verrouillée, pendant 5000 millisecondes au maximum, avant de renvoyer database is locked. La valeur par défaut est 0. SQLite échoue donc immédiatement lorsqu’au moins deux writers se chevauchent. La définition de cette seule valeur supprime la plupart des erreurs de verrouillage attribuées à SQLite lui-même.

synchronous = NORMAL est le bon paramètre en mode WAL, mais il faut comprendre le compromis. Avec FULL, SQLite appelle fsync sur le WAL à chaque commit. Avec NORMAL, il synchronise les données lors des checkpoints. La documentation de SQLite est claire sur ce que vous perdez : les transactions ne sont plus durables après une coupure de courant ou un hard reset. La base de données ne peut pas être corrompue par cette perte d’alimentation. Vous perdez simplement les derniers commits qui n’avaient pas encore été écrits sur le disque. Sur un VPS, c’est généralement le bon compromis, car un fsync est retiré du chemin de chaque écriture.

foreign_keys = ON est désactivé par défaut pour assurer la compatibilité ascendante, et ce paramètre est propre à chaque connexion. Un schéma contenant de nombreuses clauses REFERENCES n’applique aucune contrainte tant que chaque connexion n’a pas activé ce paramètre.

Un autre paramètre ne devient important que plus tard. SQLite effectue automatiquement un checkpoint lorsque le WAL dépasse 1000 pages. Le travail est réalisé par la connexion qui termine une transaction à ce moment-là. Cela ne pose pas de problème en soi. La situation devient différente lorsque Litestream s’exécute, car Litestream doit contrôler le moment où les checkpoints ont lieu.

Pourquoi database is locked se produit encore après avoir défini busy_timeout

C’est l’erreur qui pousse certains utilisateurs à revenir à Postgres. Elle a une cause précise.

Un délai d’attente busy installe un gestionnaire busy, mais SQLite ne garantit pas qu’il l’appellera.

Si SQLite détermine que l’appel du gestionnaire busy pourrait provoquer un interblocage, il renvoie directement SQLITE_BUSY à l’application au lieu d’appeler le gestionnaire busy.

L’interblocage évité se produit lorsqu’une transaction est mise à niveau. Dans SQLite, un BEGIN seul signifie BEGIN DEFERRED. Si la première instruction qui le suit est un SELECT, vous êtes dans une transaction de lecture. Lorsqu’un UPDATE ultérieur de cette même transaction doit devenir une transaction d’écriture, et qu’une autre connexion a écrit depuis le début de votre lecture, SQLite ne peut pas vous faire attendre. Votre snapshot est déjà obsolète, et l’attente provoquerait seulement un interblocage entre les deux connexions. La documentation décrit directement le résultat :

Les instructions d’écriture suivantes mettent à niveau la transaction vers une transaction d’écriture si possible, ou renvoient SQLITE_BUSY.

Votre délai d’attente de 5000 millisecondes n’est jamais consulté. L’erreur arrive immédiatement. C’est pourquoi le paramètre semble n’avoir aucun effet.

La correction tient en un mot.

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

BEGIN IMMEDIATE prend le verrou d’écriture au début, avant toute lecture. Il n’y a pas de mise à niveau, donc aucun interblocage à éviter. Le gestionnaire busy s’applique et la connexion attend son tour au lieu d’échouer. Conservez les transactions en lecture seule en mode deferred. Toute transaction qui contient une écriture doit être immediate.

La deuxième cause des erreurs de verrouillage est plus difficile à repérer : maintenir une transaction d’écriture ouverte pendant une opération lente. SQLite sérialise les écritures. Une transaction qui s’ouvre, appelle une API externe sur le réseau, puis valide la transaction bloque donc toutes les autres écritures pendant toute la durée de cet appel. Lisez les données nécessaires, fermez la transaction, effectuez l’opération lente, puis ouvrez une transaction d’écriture courte pour enregistrer le résultat.

Sauvegarde continue avec Litestream

Une copie nocturne peut perdre jusqu’à une journée d’écritures. L’exécution de cp sur une base de données SQLite active peut produire une copie qui ne s’ouvre pas. Deux solutions sont sûres. sqlite3 app.db ".backup /path/to/backup.db" utilise l’interface de sauvegarde en ligne de SQLite et fonctionne sur une base de données utilisée. Litestream va plus loin : il surveille le WAL et envoie continuellement les modifications vers un object storage. La perte de données maximale passe ainsi d’une journée à environ une seconde.

Litestream est un binaire Go qui s’exécute à côté de votre application. Il ne s’intercale pas entre l’application et la base de données. Votre application écrit dans SQLite exactement comme avant, et Litestream lit le WAL puis envoie les modifications.

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 est la version documentée par la page officielle d’installation Linux en juillet 2026, et v0.5.15 a suivi le 21 juillet 2026. Modifiez la version dans les deux lignes pour utiliser le tag actuel de la page des releases. Si votre VPS utilise arm64, prenez plutôt le package arm64 correspondant.

Le fichier de configuration se trouve à /etc/litestream.yml. Commencez avec une replica locale. Cela permet de valider toute la chaîne sans identifiants cloud.

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

Notez que le champ est replica, au singulier. Litestream 0.5 a remplacé le tableau replicas de la série 0.3 par un seul bloc de replica. Une configuration contenant deux entrées échoue désormais au démarrage. De nombreux guides tiers présentent encore l’ancien tableau. Reprenez donc la structure ci-dessus plutôt que le premier exemple trouvé par une recherche. La série 0.5 a également renommé la sous-commande litestream wal en litestream ltx, car le format de sauvegarde sur disque a changé.

Vérifiez que la configuration est correctement analysée avant d’activer quoi que ce soit.

sudo litestream databases -config /etc/litestream.yml

Vérifiez ensuite manuellement l’aller-retour. Cette forme ignore le fichier de configuration et réplique une base de données vers un chemin unique.

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

La commande s’exécute au premier plan et continue de fonctionner. Dans un second shell, écrivez une ligne, puis restaurez la replica dans un nouveau fichier.

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

Le compte inclut la nouvelle ligne. Si ce n’est pas le cas, la modification n’est pas encore synchronisée. Litestream effectue un envoi selon un sync-interval qui vaut 1 seconde par défaut. Attendez, puis recommencez la restauration. Cette seconde correspond également à votre point de récupération. Un crash entraîne au maximum la perte des écritures effectuées pendant le dernier intervalle de synchronisation. Aucune configuration ne peut réduire cette valeur à zéro.

Pour utiliser un stockage réel, remplacez le bloc de replica par une URL S3. Cela fonctionne avec Amazon S3 et avec les object storages compatibles S3 d’autres fournisseurs.

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

snapshot:
  interval: 24h
  retention: 24h

Ne stockez pas les identifiants dans ce fichier. Litestream lit LITESTREAM_ACCESS_KEY_ID et LITESTREAM_SECRET_ACCESS_KEY depuis l’environnement. Placez-les donc dans un drop-in systemd appartenant à root, avec le mode 600.

Les valeurs de snapshot ci-dessus sont les valeurs par défaut, et la valeur de rétention surprend souvent. La rétention indique combien de temps Litestream conserve les snapshots et les fichiers qui leur sont associés. Elle détermine donc aussi jusqu’où vous pouvez remonter dans le temps lors d’une restauration. Vingt-quatre heures signifie qu’une migration défectueuse détectée mercredi matin est déjà irrécupérable à partir de l’état de lundi. Définissez retention: 168h sur une semaine et prévoyez le coût du stockage supplémentaire.

Validez la restauration avant d’en avoir besoin

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

À partir d’un chemin de base de données, litestream restore recherche la réplique correspondante dans /etc/litestream.yml et la récupère. PRAGMA integrity_check affiche ok pour un fichier sain. Toute autre sortie signifie que la copie restaurée n’est pas utilisable. Exécutez cette vérification régulièrement avec un service et un timer systemd, puis lisez la sortie. Tant que vous n’avez pas restauré une sauvegarde au moins une fois, vous ne savez pas si elle fonctionne.

Exécuter Litestream avec systemd

Le paquet Debian installe une unité litestream qui lit /etc/litestream.yml.

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

Une sortie normale nomme chaque base de données de la configuration, puis reste silencieuse, à l’exception des lignes de synchronisation périodiques. Une erreur no such file or directory concernant le chemin de votre base de données signifie que le chemin indiqué dans la configuration est incorrect ou que le processus ne peut pas le lire. L’unité s’exécute par défaut en tant que root, ce qui accorde plus de privilèges que nécessaire pour cette tâche. Litestream doit pouvoir lire et écrire dans la base de données et dans le répertoire qui la contient, car il utilise les fichiers -wal et -shm situés à côté de votre base de données. Utilisez donc le compte que votre application utilise déjà.

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

Appliquez ces modifications avec sudo systemctl daemon-reload et sudo systemctl restart litestream. La création d’un compte de service dédié avec le moins de privilèges possible prend quelques minutes. Elle fait la différence entre un agent de sauvegarde et un second processus root sur la machine.

Un point concernant l’ordre d’exécution est important si vous devez reconstruire entièrement la machine. Vous devez restaurer la base de données avant de démarrer l’application. litestream restore accepte -if-db-not-exists, qui renvoie 0 lorsque le fichier existe déjà. Vous pouvez donc l’exécuter à chaque démarrage. Ajoutez-le dans une ligne ExecStartPre de l’unité de votre application. Ainsi, un VPS neuf récupère la base de données, tandis qu’un VPS existant ne fait rien. litestream replicate dispose d’une option -restore-if-db-not-exists correspondante si vous préférez conserver cette configuration au même endroit.

Quand SQLite atteint ses limites sur un VPS

Systèmes de fichiers réseau. C'est la limite que vous ne pouvez pas contourner par la configuration. Le mode WAL exige que chaque processus utilisant la base de données partage une petite zone de mémoire, fournie par le fichier -shm. La documentation de SQLite énonce cette règle sans exception :

Tous les processus utilisant une base de données doivent se trouver sur le même ordinateur hôte ; WAL ne fonctionne pas sur un système de fichiers réseau.

Une base de données située sur un partage NFS (network file system) monté ou sur un partage SMB peut donc être corrompue, et aucun pragma ne peut l'empêcher. Une distinction est souvent oubliée. Un périphérique block réseau, que la plupart des fournisseurs de VPS attachent comme stockage supplémentaire, apparaît sous Linux comme un disque ordinaire avec un système de fichiers ordinaire. Cela ne pose aucun problème. Un file share monté est différent.

Un deuxième serveur d'application. Aucun paramètre ne permet ce fonctionnement. Dès que deux machines doivent servir les mêmes données, vous avez besoin d'une base de données qui communique sur le réseau. Prenez cette décision tant que vous avez encore le temps de la planifier.

Charges de travail avec beaucoup d'écritures. Un seul writer à la fois est une propriété du format de fichier, et non un paramètre réglable. Les écritures courtes sont peu coûteuses, car chaque commit est ajouté à la fin du WAL. Le débit dépend donc davantage de la latence de votre disque pour les petites écritures que de votre CPU. Consultez NVMe ou stockage SSD SATA sur un VPS pour voir la différence. Les transactions longues sont le véritable problème, car elles mettent en file d'attente tous les autres writers.

Requêtes analytiques. SQLite est un row store conçu pour les transactions. Un dashboard qui parcourt cent millions de lignes correspond à une autre charge de travail et nécessite un autre outil. La page DuckDB comparé à SQLite pour une utilisation serveur explique où se situe cette limite.

VACUUM pendant la réplication. Un VACUUM complet réécrit l'intégralité du fichier de base de données. Litestream doit donc le téléverser entièrement à nouveau, et sa documentation déconseille de l'exécuter sur place pendant la réplication. Arrêtez le replicator, exécutez vacuum, puis redémarrez-le. Prévoyez un nouveau snapshot complet.

Deux replicators sur une même base de données. N'exécutez jamais deux processus Litestream sur la même base de données ou vers la même destination de réplication. La documentation précise que vous devez empêcher cette situation. Sinon, vous obtiendrez une replica impossible à restaurer.

Ce que Litestream ne couvre pas

Litestream protège le fichier de base de données, et rien d’autre. Les fichiers téléversés, la configuration de l’application, les certificats TLS (transport layer security) et les fichiers d’unité restent à votre charge. Associez-le à des sauvegardes chiffrées hors machine avec restic planifiées pour couvrir les deux volets. Si la machine est neuve, les dix premières minutes sur un nouveau VPS couvrent la création du compte utilisateur et la configuration du pare-feu, que ce guide suppose déjà effectuées.

FAQ

SQLite est-il suffisamment adapté à une application en production ?

Pour une application sur un seul serveur, oui, à condition d’activer le mode WAL, de définir un délai d’attente busy_timeout et d’effectuer des sauvegardes en continu. Les limites importantes sont structurelles : un seul processus d’écriture à la fois et une seule machine hôte. Une application qui respecte ces limites dispose d’une base de données sans saut réseau et sans processus distinct à monitorer. Une application qui dépasse ces limites a besoin d’une base de données client-serveur. Aucun réglage ne change cela.

Pourquoi database is locked s’affiche-t-il encore après la définition de busy_timeout ?

Parce que SQLite ignore le busy handler lorsque l’attente pourrait provoquer un deadlock. Une transaction qui commence par un simple BEGIN est différée : un SELECT initial la place dans une transaction de lecture, puis une écriture ultérieure doit la faire évoluer. Si une autre connexion a écrit entre-temps, SQLite renvoie immédiatement SQLITE_BUSY au lieu d’appeler votre busy handler, car votre instantané de lecture est déjà obsolète. Commencez toute transaction qui effectuera une écriture par BEGIN IMMEDIATE afin que le verrou d’écriture soit pris immédiatement et que le délai d’attente s’applique.

Puis-je conserver ma base de données SQLite sur un stockage réseau ?

Pas sur un système de fichiers réseau tel que NFS ou SMB. Le mode WAL nécessite que tous les processus partagent la mémoire via le fichier -shm, et la documentation de SQLite indique que chaque processus utilisant la base de données doit se trouver sur le même ordinateur hôte. Un périphérique bloc réseau attaché par votre fournisseur est différent : Linux voit un disque normal avec un système de fichiers normal, et SQLite fonctionne dessus.

Ai-je besoin de Litestream si j’effectue déjà des sauvegardes nocturnes ?

Cela dépend de la quantité de données que vous pouvez vous permettre de perdre. Une tâche nocturne peut entraîner la perte de jusqu’à vingt-quatre heures d’écritures. Litestream synchronise environ une fois par seconde. En cas de panne, vous perdez donc environ la dernière seconde. Il est également plus sûr que la copie du fichier de base de données avec cp, qui peut capturer une base de données pendant une écriture. Litestream couvre uniquement la base de données. Maintenez donc une sauvegarde générale des fichiers en parallèle.

#sqlite#wal#litestream#sauvegardes#production