SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-07

SQLite en production sur un VPS : bon choix ?

Découvrez quand SQLite convient sur un VPS, avec WAL, busy_timeout et Litestream, puis les limites précises qui imposent de migrer vers PostgreSQL.

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, pour une raison simple : un processus sur une 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 dans le pare-feu, aucun mot de passe à renouveler et aucune deuxième machine à maintenir en fonctionnement. Une requête est un appel de fonction plutôt qu’un aller-retour réseau. Une page qui exécute quarante requêtes coûte donc quarante appels de fonction.

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

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 qui commence par 3., suivie d’une date de build et d’un hash du code source. Ubuntu 24.04 fournit SQLite 3.45.1 en juillet 2026. 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 le premier réglage à 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 la base de données sur place. Pour garantir l’intégrité de l’opération, il prend un verrou exclusif sur l’ensemble du fichier. Tous les lecteurs attendent donc pendant toute écriture. Sur un ordinateur portable, cela passe généralement inaperçu. Sur un serveur web, une écriture lente bloque chaque requête qui utilise la base de données.

Le mode WAL (write-ahead log) inverse cet ordre. Un processus d’écriture ajoute les nouvelles pages à un fichier -wal distinct et ne modifie pas la base de données principale. Les lecteurs continuent à lire le fichier principal dans l’état correspondant au snapshot au moment où ils ont commencé. Les lecteurs ne bloquent donc pas le processus d’écriture, et celui-ci ne bloque pas les lecteurs. Ensuite, un checkpoint recopie les pages accumulées dans le fichier WAL vers la base de données principale. Cette seule modification suffit à rendre SQLite réellement utilisable derrière une application web.

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

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

La commande affiche wal. Cette sortie n’est pas décorative. PRAGMA journal_mode renvoie le mode dans lequel la base de données se trouve réellement. 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’activez 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 examinez les fichiers créés 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/

Trois fichiers sont maintenant présents : app.db, app.db-wal et app.db-shm. Le fichier -wal contient les pages validées qui n’ont pas encore été checkpointées. Le fichier -shm est un index de mémoire partagée que chaque connexion mappe afin que toutes s’accordent sur le 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 toutes les validations récentes. Si vous supprimez app.db et laissez les deux autres fichiers en place, SQLite appliquera ces pages WAL obsolètes à tout nouveau fichier créé sous ce nom. C’est ainsi qu’une base de données vide est souvent corrompue lors d’une tentative de réinitialisation.

Réglages de connexion indispensables pour toute application en production

Seul journal_mode est stocké dans la base de données. Tous les autres réglages ci-dessous s’appliquent à chaque connexion. Votre application doit donc les exécuter sur chaque connexion qu’elle ouvre, y compris celles qu’un pool crée en arrière-plan.

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

busy_timeout = 5000 indique à SQLite de réessayer pendant 5000 millisecondes au maximum lorsqu’une base de données est verrouillée, avant de renvoyer database is locked. La valeur par défaut est 0. SQLite échoue donc immédiatement lorsque deux écritures se chevauchent pour la première fois. Ce réglage suffit à supprimer la plupart des erreurs de verrouillage souvent attribuées à SQLite lui-même.

synchronous = NORMAL est le bon réglage en mode WAL, mais il faut comprendre le compromis. Avec FULL, SQLite appelle fsync sur le WAL à chaque commit. Avec NORMAL, la synchronisation a lieu lors des checkpoints. La documentation de SQLite est explicite sur ce que vous perdez : les transactions ne sont plus durables après une panne 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 fsync est retiré du chemin de chaque écriture.

foreign_keys = ON est désactivé par défaut pour des raisons de compatibilité ascendante, et ce réglage s’applique à chaque connexion. Un schéma rempli de clauses REFERENCES n’impose absolument aucune contrainte tant que chaque connexion n’a pas activé ce réglage.

Un autre réglage devient important plus tard. SQLite déclenche automatiquement un checkpoint lorsque le WAL dépasse 1000 pages. L’opération est effectuée par la connexion qui termine une transaction à ce moment-là. Ce comportement ne pose pas de problème en lui-même. 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 la configuration de busy_timeout

C’est l’erreur qui pousse les 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 qu’il évite se produit lors de la promotion d’une transaction. Dans SQLite, un BEGIN seul signifie BEGIN DEFERRED. Si la première instruction qui 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 promeuvent la transaction en transaction d’écriture si possible, ou renvoient SQLITE_BUSY.

Votre délai de 5000 millisecondes n’est jamais consulté. L’erreur arrive immédiatement. C’est pourquoi la configuration 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 donc pas de promotion ni d’interblocage à éviter. Le gestionnaire busy s’applique et la connexion attend son tour au lieu d’échouer. Laissez les transactions en lecture seule en mode différé. Toute transaction qui contient une écriture doit être immédiate.

La deuxième cause des erreurs de verrouillage est plus difficile à repérer : garder 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 tous les autres writers 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. De plus, l’exécution de cp sur une base de données SQLite active peut produire une copie qui ne s’ouvrira 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 stockage d’objets. 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 comme auparavant, et Litestream lit le WAL puis téléverse 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 pour Linux en juillet 2026. v0.5.15 est sortie le 21 juillet 2026. Remplacez la version dans les deux lignes par 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 par une replica locale. Vous pourrez ainsi vérifier l’ensemble du fonctionnement sans avoir besoin d’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 maintenant au démarrage. De nombreux guides tiers affichent encore l’ancien tableau. Reprenez donc la structure ci-dessus plutôt que le premier exemple trouvé dans les résultats de 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 seul chemin.

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 deuxième 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 nombre inclut la nouvelle ligne. Si ce n’est pas le cas, la modification n’est pas encore synchronisée. Litestream envoie les modifications selon un sync-interval de 1 seconde par défaut. Attendez, puis restaurez à nouveau. Cette seconde correspond également à votre point de récupération. En cas de crash, vous perdez au maximum les écritures de ce dernier intervalle de synchronisation. Aucune configuration ne permet de réduire cette perte à 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 stockages d’objets 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 mettez pas les identifiants dans ce fichier. Litestream lit LITESTREAM_ACCESS_KEY_ID et LITESTREAM_SECRET_ACCESS_KEY dans 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 durée de rétention par défaut peut surprendre. La rétention correspond à la durée pendant laquelle Litestream conserve les snapshots et les fichiers qui leur sont associés. Elle détermine donc aussi jusqu’à quel moment vous pouvez restaurer les données. Une durée de 24 heures signifie qu’une migration défectueuse détectée mercredi matin est déjà irrécupérable depuis l’état de lundi. Définissez retention: 168h sur une semaine et prévoyez le coût du stockage supplémentaire.

Prouvez 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 le replica correspondant dans /etc/litestream.yml et le télécharge. 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 consultez 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 indique chaque base de données définie dans 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 ce chemin est incorrect dans la configuration ou que le processus ne peut pas le lire. Par défaut, l’unité s’exécute avec 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. Attribuez donc l’unité au compte déjà utilisé par votre application.

# /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 principe du moindre privilège prend quelques minutes. Elle fait la différence entre un agent de sauvegarde et un second processus root sur le serveur.

Un point concernant l’ordre de démarrage est important si vous devez reconstruire la machine à partir de zéro. La base de données doit être restaurée avant le démarrage de 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’un indicateur -restore-if-db-not-exists correspondant 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 mémoire, fournie par le fichier -shm. La documentation SQLite énonce cette règle sans réserve :

Tous les processus utilisant une base de données doivent s’exécuter 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) ou SMB monté peut donc être corrompue, et aucun pragma ne l’empêche. Une distinction importante est souvent oubliée. Un périphérique block réseau, utilisé par la plupart des fournisseurs de VPS pour ajouter du stockage, apparaît dans Linux comme un disque ordinaire contenant un système de fichiers ordinaire. Cela ne pose pas de problème. Un file share monté est différent.

Un deuxième serveur d’application. Aucun réglage ne permet de faire fonctionner ce modèle. 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 pendant que vous avez encore le temps de la planifier.

Charges fortement orientées écriture. Le fait de n’autoriser qu’un 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é au WAL. Le débit dépend donc davantage de la latence de votre disque lors des petites écritures que de votre CPU. Consultez NVMe ou stockage SSD SATA sur un VPS pour voir concrètement cette différence. Les transactions longues sont le vrai problème, car elles mettent en 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 à un autre besoin et nécessite un autre outil. La section DuckDB comparé à SQLite pour les traitements serveur précise où se situe cette limite.

VACUUM avec la réplication. Un VACUUM complet réécrit la totalité du fichier de base de données. Litestream doit donc le téléverser intégralement à nouveau. La documentation Litestream déconseille de l’exécuter sur place pendant que la réplication est active. Arrêtez le replicator, exécutez vacuum, puis redémarrez-le. Prévoyez la création d’un nouvel instantané complet.

Deux replicators pour 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 replica. La documentation indique clairement qu’il vous revient d’empêcher cette situation. Sinon, vous obtenez un 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 importé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 sur une autre machine avec restic planifiées pour couvrir les deux aspects. Si la machine est neuve, les dix premières minutes sur un nouveau VPS couvrent la configuration du compte utilisateur et du firewall, que ce guide suppose déjà effectuée.

FAQ

SQLite est-il suffisant pour 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 continues. Les limites importantes sont structurelles : un seul writer à la fois et une seule machine hôte. Une application qui respecte ces limites bénéficie d’une base de données sans saut réseau ni processus séparé à monitorer. Une application qui les dépasse a besoin d’une base de données client-serveur. Aucun réglage ne change cela.

Pourquoi est-ce que j’obtiens toujours database is locked après avoir défini busy_timeout ?

Parce que SQLite ignore le busy handler lorsque l’attente risque de provoquer un deadlock. Une transaction qui commence par un BEGIN simple est différée : un premier SELECT l’ouvre en 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 snapshot de lecture est déjà obsolète. Commencez toute transaction qui effectuera une écriture par BEGIN IMMEDIATE afin que le write lock soit obtenu dès le début 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 doit permettre à tous les processus de partager la mémoire via le fichier -shm, et la documentation SQLite précise que chaque processus qui utilise la base de données doit se trouver sur le même ordinateur hôte. Un périphérique de stockage en mode bloc attaché par votre fournisseur est différent : Linux voit un disque normal avec un système de fichiers normal, et SQLite fonctionne dessus.

Litestream est-il nécessaire si j’effectue déjà des sauvegardes chaque nuit ?

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 crash, 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 en pleine écriture. Litestream ne couvre que la base de données. Maintenez donc en parallèle une sauvegarde générale des fichiers.

#sqlite#wal#litestream#sauvegardes#production