Développement Web

Déployer Turso libSQL self-hosted : tutoriel complet 2026

17 دقائق للقراءة

📍 Article principal : Litestream/Turso 2026 : guide pratique.

Turso = libSQL fork SQLite avec serveur HTTP, embedded replicas, edge distribution. Self-hosted sur VPS pour souveraineté complète.

Prérequis

  • VPS Linux avec Docker.
  • Domaine pour accès HTTPS.
  • Niveau : avancé.
  • Temps : 1h.

Pour cette installation Turso libSQL self-hosted, vous avez besoin d’un VPS avec 1 vCPU et 2 Go RAM minimum (Hetzner CX11 suffit largement). Docker et Docker Compose installés. Un nom de domaine avec DNS A pointant vers le VPS pour activer HTTPS via Let’s Encrypt. Comptez 15-30 minutes pour cette installation depuis un VPS vierge. La version libsql-server stable au moment de cet article est sqld v0.24.x, le binaire officiel de Turso open-source.

Étape 1 — Lancer libsql-server

docker run -d --name libsql-server \
  -p 8080:8080 \
  -v libsql-data:/var/lib/sqld \
  -e SQLD_NODE=primary \
  -e SQLD_HTTP_LISTEN_ADDR=0.0.0.0:8080 \
  -e SQLD_AUTH_JWT_KEY_FILE=/var/lib/sqld/jwt.pem \
  ghcr.io/tursodatabase/libsql-server:latest

Créez un fichier compose.yaml avec le service sqld officiel : image: ghcr.io/tursodatabase/libsql-server:latest. Mappez le port 8080 vers l’hôte (interne) et créez un volume pour /var/lib/sqld qui persiste les bases. Définissez la variable SQLD_NODE=primary pour le mode principal (mono-instance par défaut) et SQLD_HTTP_AUTH_JWT_KEY_FILE pour activer l’authentification. Lancez docker compose up -d et vérifiez les logs avec docker compose logs -f sqld pour confirmer le démarrage propre.

Étape 2 — Caddy reverse proxy

db.votre-app.com {
  reverse_proxy localhost:8080
}

Ne pas exposer sqld directement sur Internet. Utilisez Caddy comme reverse proxy qui gère HTTPS automatiquement via Let’s Encrypt. Le Caddyfile minimal ressemble à turso.example.com { reverse_proxy localhost:8080 }. Caddy obtient le certificat dans les 30 secondes suivant le démarrage si le DNS est correctement propagé. Le port 80 doit rester ouvert pour le challenge HTTP-01. Cette mise en place vous donne un endpoint TLS 1.3 propre sans gérer manuellement les certificats.

Étape 3 — Test API

curl https://db.votre-app.com/v2/pipeline \
  -d '{"requests":[{"type":"execute","stmt":{"sql":"SELECT 1"}}]}'

Avec curl, validez que l’API HTTP de libsql répond. curl https://turso.example.com -H 'Authorization: Bearer VOTRE_JWT' -d '{"statements":["SELECT 1"]}'. La réponse JSON doit contenir un tableau avec [{« results »:[{« columns »:[« 1″], »rows »:[[1]]}]}]. Si vous obtenez une 401, le JWT est mal généré ou expiré. Si vous obtenez 502, sqld n’est pas accessible depuis Caddy (vérifiez le mapping de ports docker).

Étape 4 — Client TypeScript

npm install @libsql/client

import { createClient } from '@libsql/client';
const db = createClient({
  url: 'https://db.votre-app.com',
  authToken: process.env.TURSO_TOKEN
});

await db.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)');
await db.execute({ sql: 'INSERT INTO users (email) VALUES (?)', args: ['test@example.com'] });
const r = await db.execute('SELECT * FROM users');

Le SDK officiel @libsql/client fonctionne avec un sqld self-hosted ou avec Turso Cloud sans changement de code. Installez avec npm i @libsql/client. Créez un client : createClient({ url: 'https://turso.example.com', authToken: process.env.TURSO_TOKEN }). Les méthodes execute() et batch() couvrent 95 % des besoins. Le SDK gère automatiquement la connexion HTTPS persistente et les retries exponentiels en cas d’erreur réseau. Pour un projet à Plateau qui démarre, ce pattern remplace bien Postgres pour les charges de lecture intensive.

Étape 5 — Embedded replicas (latence zéro)

const db = createClient({
  url: 'file:local-replica.db',
  syncUrl: 'https://db.votre-app.com',
  authToken: process.env.TURSO_TOKEN,
  syncInterval: 60
});

Reads = SQLite local, writes = forward server, sync auto.

Les embedded replicas sont la killer feature de libSQL. Le SDK télécharge une copie locale SQLite de la base sur le poste/serveur applicatif et la maintient synchronisée avec le primary via le protocole Hrana. Les lectures se font en mémoire (latence inférieure à 1 ms), les écritures sont relayées vers le primary. Pour un VPS applicatif à Bamako qui consulte une base hébergée à Falkenstein (Allemagne), la latence des SELECT chute de 200-300 ms à 1-3 ms. Activez avec createClient({ url: 'file:replica.db', syncUrl: 'https://turso.example.com', authToken: ... }).

Étape 6 — Drizzle ORM compatibility

import { drizzle } from 'drizzle-orm/libsql';
const db = drizzle(client, { schema });

Drizzle ORM supporte libSQL nativement via le driver drizzle-orm/libsql. Le schema TypeScript se définit comme pour SQLite classique. Drizzle Kit génère les migrations qui s’appliquent via le client libSQL. La compatibilité est totale : transactions, prepared statements, types JSON via les colonnes text avec mode json. Pour un dev solo qui veut un ORM type-safe sans la complexité de Prisma, cette combinaison est devenue très populaire en 2025-2026.

Étape 7 — JWT auth tokens

# Generate JWT key
docker exec libsql-server sqld admin gen-jwt-keys
# Outputs jwt.pem
# Generate token
docker exec libsql-server sqld admin gen-token --key /var/lib/sqld/jwt.pem

libsql utilise JWT signé par une clé EdDSA pour l’authentification. Générez la paire clé via openssl genpkey -algorithm Ed25519 -out private.pem puis dérivez la publique. Distribuez la publique au serveur (via SQLD_HTTP_AUTH_JWT_KEY_FILE), gardez la privée pour signer les tokens applicatifs. Chaque service applicatif reçoit un JWT avec un claim iss identifiant et une expiration courte (1-24h). Les rotations de clés se font sans downtime via la liste de clés acceptées.

Étape 8 — Replicas multi-region

Sur autre VPS :

docker run -d \
  -e SQLD_NODE=replica \
  -e SQLD_PRIMARY_URL=https://db.votre-app.com \
  ghcr.io/tursodatabase/libsql-server

Pour un trafic distribué entre l’Afrique de l’Ouest et l’Europe, déployez un primary en Europe et des replicas read-only en Afrique. Configurez SQLD_NODE=replica sur les VPS Africa avec SQLD_PRIMARY_URL pointant vers le primary. La synchronisation est asynchrone, les replicas servent les lectures avec latence locale. Les écritures sont automatiquement redirigées vers le primary par le SDK. Ce setup combine les avantages d’une base centralisée (cohérence) et d’une base distribuée (performance).

Étape 9 — Backups

Turso self-hosted : pg-style dump via libsql ou Litestream sur fichier underlying.

libsql stocke ses données dans /var/lib/sqld qui contient des fichiers SQLite standards. Sauvegardez quotidiennement avec Restic ou Borg vers un stockage S3 (Backblaze B2 à 6 USD/To/mois ou Hetzner Storage Box). La commande sqlite3 data.db .dump > backup.sql crée un dump SQL textuel restaurable sur n’importe quelle SQLite. Pour les bases volumineuses (>1 Go), préférez la commande VACUUM INTO qui crée un fichier compact prêt à compresser. Testez la restauration mensuellement.

Étape 10 — Monitoring

Endpoint /v2/health. Prometheus metrics via env SQLD_METRICS=1.

libsql expose des métriques Prometheus via l’endpoint /metrics (port admin séparé du port HTTP applicatif, généralement 8081). Scrapper avec Prometheus et visualiser dans Grafana avec un dashboard custom. Métriques clés : sqld_writes_total, sqld_replication_delay_seconds, sqld_connections_active. Alertez si replication_delay dépasse 10 secondes (dégradation de la cohérence) ou si writes/seconde s’effondre soudainement. Pour une équipe à Cocody qui pilote un service en production, ce monitoring détecte les pannes avant les utilisateurs.

Erreurs fréquentes

Erreur Cause Solution
JWT invalid Token expired Régénérer token
Replica out-of-sync Network issue Restart replica
Slow embedded sync syncInterval high 10-30s typique
HTTPS cert Caddy mal configuré Verify Caddyfile
OOM serveur Trop de connexions Augmenter VPS

Trois pannes courantes lors du démarrage initial. Premièrement, oublier le volume Docker ce qui efface les bases au redémarrage du container — créez toujours un volume nommé persistant. Deuxièmement, exposer sqld directement sur le port 8080 public sans authentification, ce qui ouvre la porte à n’importe qui — utilisez systématiquement Caddy ou Nginx en frontend. Troisièmement, mauvaise génération JWT (mauvais algorithme, clé corrompue), ce qui produit des 401 mystérieux — testez le token avec jwt.io avant de chercher ailleurs.

Application au contexte économique sous-régional

Trois précisions. Edge replicas Africa : déployer replica sur Africa Data Centres Lagos. Mobile embedded : SDK iOS/Android natif. Coût : 1 VPS primary + 2 replicas = 13,53 €/mois pour SaaS multi-region.

Pour une PME basée à Dakar ou Cotonou qui héberge une app SaaS, libsql self-hosted divise par 5-10 le coût comparé à Turso Cloud (à partir de 29 USD/mois pour Turso Pro vs 5 USD/mois pour un VPS Hetzner CX11). La latence reste excellente (Falkenstein à 50-80 ms de Dakar). Pour les charges critiques avec besoins de zero-downtime, restez sur le cloud Turso le temps de la phase de croissance, puis migrez sur self-hosted une fois la stack stabilisée. Cette progression économise 1 500-3 000 USD/an sur des projets matures.

Tutoriels frères

Cette installation libsql se complète bien avec d’autres briques self-hosted. Litestream pour la réplication continue d’une SQLite vers S3 (alternative simple aux embedded replicas pour les sites mono-région). Drizzle ORM pour la couche d’accès données type-safe. Caddy ou Traefik pour le reverse proxy HTTPS automatique. Backblaze B2 ou MinIO pour le stockage objet des sauvegardes. Cette stack cohérente évite la dispersion technologique et facilite la maintenance long terme.

FAQ

Turso Cloud vs self-hosted ? Cloud free tier 500 DB. Self-host illimité.

Compatibilité SQLite ? 99% compatible. Quelques extensions custom.

Multi-writer ? Primary + read replicas. Writes vers primary.

Cloudflare Workers ? Embedded replica via libSQL JavaScript.

Migration depuis SQLite ? Trivial. cp app.db + import via API.

Q : libsql est-il production-ready ? R : Oui, utilisé en production par Turso Cloud lui-même qui sert des millions de requêtes/jour. Q : Quelle différence avec SQLite classique ? R : libsql ajoute le protocole HTTP/Hrana pour les accès distants et les replicas, là où SQLite est strictement local. Q : Multi-tenant supporté ? R : Oui via le concept de databases (un sqld peut héberger des centaines de bases isolées). Q : Migration depuis Postgres ? R : Possible avec pgloader puis adaptation des types (JSONB devient TEXT JSON, INTERVAL devient TEXT ISO8601).

Dans la continuité

Démarrer en production sur un VPS

Pour passer du local à un serveur réellement accessible en ligne, Hostinger propose des plans VPS abordables avec sauvegarde automatique.

Voir les VPS →

Lien d affiliation. Si vous achetez via ce lien, le blog reçoit une petite commission sans surcoût pour vous.

Pourquoi self-hoster libSQL plutot que d’utiliser Turso Cloud

libSQL est le fork open source de SQLite maintenu par l’equipe Turso (ex-ChiselStrike). Il ajoute la replication serveur-client, des extensions vectorielles (libsql_vector pour le RAG), et un protocole HTTP/WebSocket nomme Hrana qui permet aux applications edge (Cloudflare Workers, Vercel Edge) d’interroger la base sans driver natif. Turso Cloud propose un free tier mais limite a 500 bases et 9 Go de stockage cumule. Pour un projet africain qui veut la souverainete des donnees ou simplement eviter la dependance a un SaaS, le binaire sqld permet d’auto-heberger libSQL en quelques minutes.

Sortie attendue : un serveur sqld accessible sur https://libsql.votredomaine.com avec authentification par token JWT, replication primary/replica entre Paris et Frankfurt si vous le souhaitez, et SDK officiels Node.js, Python, Rust qui s’y connectent.

Etape 1 : Installer sqld v0.24

sqld est distribue en binaire Rust. Sur Ubuntu 24.04 :

curl -sSf https://github.com/tursodatabase/libsql/releases/download/libsql-server-v0.24.32/libsql-server-x86_64-unknown-linux-gnu.tar.xz | tar -xJ
sudo mv sqld /usr/local/bin/
sqld --version

Résultat attendu : sqld 0.24.32. Si tar renvoie « unknown extension », verifiez que xz-utils est installe (sudo apt install xz-utils). Le binaire est statiquement lie, aucune dependance runtime supplementaire.

Etape 2 : Generer les cles JWT pour l’authentification

sqld accepte deux modes d’auth : token statique simple ou JWT signe. Privilegiez JWT pour la production. Generez une paire Ed25519 :

openssl genpkey -algorithm Ed25519 -out /etc/sqld/jwt-private.pem
openssl pkey -in /etc/sqld/jwt-private.pem -pubout -out /etc/sqld/jwt-public.pem

Vous devriez obtenir : deux fichiers PEM dans /etc/sqld/. Securisez les permissions avec chmod 600 /etc/sqld/jwt-private.pem et conservez la cle privee dans un coffre-fort (Vaultwarden self-hosted, par exemple). La cle publique sera fournie a sqld pour valider les tokens.

Etape 3 : Lancer sqld en mode primary

Creez le repertoire de donnees puis lancez sqld :

sudo mkdir -p /var/lib/sqld
sqld \
  --http-listen-addr 0.0.0.0:8080 \
  --grpc-listen-addr 0.0.0.0:5001 \
  --auth-jwt-key-file /etc/sqld/jwt-public.pem \
  --db-path /var/lib/sqld/data

Résultat attendu : listening on http://0.0.0.0:8080 et node role: primary. Si l’erreur est address already in use, un autre processus occupe le port 8080 ; verifiez avec ss -tlnp | grep 8080.

Etape 4 : Creer un service systemd

Pour la production, packagez sqld dans une unite systemd /etc/systemd/system/sqld.service :

[Unit]
Description=libSQL server
After=network.target

[Service]
ExecStart=/usr/local/bin/sqld --http-listen-addr 0.0.0.0:8080 --auth-jwt-key-file /etc/sqld/jwt-public.pem --db-path /var/lib/sqld/data
Restart=on-failure
User=sqld
Group=sqld

[Install]
WantedBy=multi-user.target

Activez avec sudo systemctl daemon-reload && sudo systemctl enable --now sqld. Ce que vous devez voir de systemctl status sqld : active (running) sans erreur dans les logs.

Etape 5 : Generer un token JWT pour le client

Utilisez le binaire jwt ou un script Node :

npm install -g jose-cli
echo '{"a":"rw"}' | jose-cli sign --alg EdDSA --key /etc/sqld/jwt-private.pem

Le claim "a":"rw" donne les permissions read+write. Pour un token read-only, utilisez "a":"ro". Sortie attendue : un token JWT de 200 a 250 caracteres. Stockez-le cote application dans une variable d’environnement TURSO_AUTH_TOKEN.

Etape 6 : Connexion depuis Node.js

Installez le SDK officiel et connectez-vous :

npm install @libsql/client
node -e "
const { createClient } = require('@libsql/client');
const c = createClient({
  url: 'https://libsql.votredomaine.com',
  authToken: process.env.TURSO_AUTH_TOKEN
});
c.execute('CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT)').then(console.log);
"

Vous devriez obtenir : un objet { rowsAffected: 0, lastInsertRowid: 0n, columns: [], rows: [] }. Si vous obtenez 401 Unauthorized, verifiez que le token n’est pas expire et que la cle publique cote serveur correspond a la cle privee qui a signe le token.

Etape 7 : Mettre en place une replication primary/replica

Lancez un second sqld en mode replica sur un VPS distant (Frankfurt, par exemple) :

sqld \
  --primary-grpc-url https://primary.votredomaine.com:5001 \
  --primary-grpc-tls true \
  --auth-jwt-key-file /etc/sqld/jwt-public.pem \
  --db-path /var/lib/sqld/replica

Résultat attendu cote replica : node role: replica et connected to primary. Les ecritures arrivees sur le primary sont propagees au replica avec une latence typique de 50 a 200 ms selon la distance reseau. Les lectures sur le replica sont eventually consistent.

Etape 8 : TLS et reverse proxy Caddy

Plutot que d’exposer sqld directement, placez Caddy 2.8 devant pour gerer le TLS automatique. Caddyfile minimal :

libsql.votredomaine.com {
  reverse_proxy localhost:8080
}

Caddy obtient un certificat Let’s Encrypt en moins de 30 secondes au premier demarrage. Verifiez avec curl -I https://libsql.votredomaine.com : output attendu HTTP/2 401 (sqld refuse une requete sans token, c’est le comportement correct).

Etape 9 : Sauvegardes et bench

Le repertoire /var/lib/sqld/data contient un fichier data.db SQLite standard. Vous pouvez y appliquer Litestream (voir notre tutoriel Litestream) ou un simple cron rsync vers Scaleway Object Storage. Bench typique sur un VPS Hetzner CX22 a Helsinki : 8 000 SELECT/seconde et 3 000 INSERT/seconde sur une base de 100 Mo, mesure avec libsql-bench.

Etape 10 : Quand choisir libSQL plutot que SQLite + Litestream

libSQL self-hoste est interessant si vos clients sont multiples et distants (web, mobile, edge functions) et ont besoin d’un endpoint HTTP plutot qu’un acces direct au fichier .db. Si vous avez une seule application monolithique sur le meme serveur que la base, SQLite + Litestream reste plus simple et plus performant. libSQL ne remplace pas PostgreSQL pour des charges OLTP a forte concurrence d’ecriture.

Dans la continuité : SQLite vs Postgres.

Etape 11 : Extension vectorielle pour RAG

libSQL inclut nativement les fonctions vectorielles (libsql_vector) sans extension externe. Creez une table avec une colonne F32_BLOB(384) pour stocker des embeddings de 384 dimensions (taille de all-MiniLM-L6-v2) :

CREATE TABLE docs(id INTEGER PRIMARY KEY, content TEXT, embedding F32_BLOB(384));
CREATE INDEX docs_idx ON docs(libsql_vector_idx(embedding));

Inserez ensuite un embedding genere par sentence-transformers ou OpenAI Embeddings, puis cherchez les voisins :

SELECT id, content FROM vector_top_k('docs_idx', vector32('[0.1,0.2,...]'), 5);

Résultat attendu : 5 lignes triees par similarite cosinus decroissante. Cas d’usage typique pour une PME senegalaise : moteur de recherche semantique sur la documentation interne, RAG pour un chatbot client en wolof+francais.

Etape 12 : Multi-tenant avec namespaces

sqld supporte le mode multi-tenant : une instance heberge plusieurs bases isolees, chacune accessible via un sous-domaine ou un header. Activez avec --enable-namespaces et creez une base avec curl -X POST https://libsql.votredomaine.com/v1/namespaces/clientA/create. Chaque namespace a son propre fichier .db et ses propres tokens JWT, ideal pour une architecture SaaS B2B ou chaque client a ses donnees isolees.

Etape 13 : Limites et roadmap

libSQL n’est pas un drop-in replacement parfait pour SQLite : certaines extensions (FTS5 avec triggers complexes) ont des comportements legerement differents en mode replication. La fonctionnalite « embedded replicas » (cache local synchronise depuis le serveur) est mature cote SDK Rust et JavaScript, encore experimentale en Python/Go. Surveillez le repo github.com/tursodatabase/libsql pour les annonces.

Etape 14 : Connexion depuis Python avec libsql_client

Le SDK Python officiel s’installe avec pip install libsql-client. Exemple de connexion async :

import asyncio, os
from libsql_client import create_client

async def main():
    client = create_client(
        url="https://libsql.votredomaine.com",
        auth_token=os.environ["TURSO_AUTH_TOKEN"]
    )
    rs = await client.execute("SELECT count(*) FROM notes")
    print(rs.rows)
    await client.close()

asyncio.run(main())

Ce que vous devez voir : [(0,)] si la table notes est vide, ou le nombre de lignes courant. Une exception LibsqlError: NOT_AUTHORIZED indique un token invalide ou expire.

Etape 15 : Embedded replicas pour applications mobiles

Le SDK @libsql/client v0.14+ supporte les embedded replicas : l’application embarque un fichier SQLite local synchronise periodiquement avec le primary distant. Configuration :

const client = createClient({
  url: 'file:local.db',
  syncUrl: 'https://libsql.votredomaine.com',
  authToken: process.env.TURSO_AUTH_TOKEN,
  syncInterval: 60
});

Les lectures sont locales (sub-milliseconde meme depuis Niamey ou Ouagadougou avec une 3G capricieuse), les ecritures sont propagees au serveur quand la connexion le permet. Ideal pour une app terrain (releve de compteurs, audit qualite, ONG sur le terrain) qui doit fonctionner offline.

Etape 16 : Cout et comparaison avec Turso Cloud

Self-hoste sur un VPS Hetzner CX22 a Helsinki : 4,50 EUR/mois (2 952 FCFA), capacite 40 Go SSD, illimite en nombre de bases si vous activez les namespaces. Turso Cloud Starter : gratuit jusqu’a 500 bases et 9 Go cumules, puis 29 USD/mois (≈ 17 700 FCFA) pour le plan Scaler. Le break-even depend de votre charge : pour des volumes modestes le free tier Cloud suffit, pour la souverainete des donnees ou plus de 9 Go, le self-hosting devient incontournable.

Etape 17 : Diagnostic des erreurs frequentes

Erreur STREAM_EXPIRED apres une connexion inactive de plus de 10 secondes : c’est le comportement Hrana par defaut. Le SDK officiel reconnecte automatiquement, mais si vous parsez le wire protocol manuellement, gerez ce cas en relancant une session. Erreur WAL_FRAMES_OUT_OF_SYNC sur un replica : redemarrez le replica avec --reset-replica pour forcer un resync complet depuis le primary.

Erreur SQLITE_BUSY sous forte charge : libSQL serialise les ecritures comme SQLite, augmentez le timeout cote client avec busy_timeout: 5000 dans les options de createClient. Si le probleme persiste, votre charge est probablement trop OLTP-heavy pour SQLite/libSQL et merite un Postgres.

Etape 18 : Ressources officielles a jour

Documentation a jour janvier 2026 : docs.turso.tech (couvre Cloud et self-hosted), github.com/tursodatabase/libsql (issues et release notes), discord.gg/turso (community). Pour les SDK : @libsql/client v0.14+ (Node, Bun, Deno, Cloudflare Workers), libsql-client v0.5+ (Python), libsql v0.5+ (Rust), libsql-go v0.4+ (Go). Verifiez toujours les changelogs avant un upgrade en production : la couche Hrana evolue rapidement.

Etape 19 : Integration avec un edge runtime

Cloudflare Workers et Vercel Edge Functions sont incompatibles avec les drivers SQL natifs (pas de TCP socket). libSQL contourne cela via HTTP/Hrana : le SDK @libsql/client fonctionne dans ces runtimes sans adapter. Exemple Cloudflare Worker qui sert une API JSON depuis votre sqld self-hoste : ajoutez les variables d’environnement TURSO_URL et TURSO_AUTH_TOKEN dans wrangler.toml, importez createClient et repondez en moins de 50 ms depuis le POP Cloudflare le plus proche du visiteur (Lagos, Johannesburg, Le Cap pour l’Afrique de l’Ouest et australe).

Etape 20 : Checklist de mise en production

Avant d’ouvrir votre serveur sqld au public, validez : firewall ufw qui ne laisse passer que 443 (Caddy) et 22 (SSH avec cle uniquement), backups Litestream actifs et restauration testee, monitoring Uptime Kuma qui ping /health, rotation des tokens JWT documentee (frequence 90 jours), alertes Grafana sur taux d’erreur 5xx superieur a 1 pour cent, et runbook de bascule sur replica. Sans ces verifications, votre stack reste fragile face au premier incident.

مشاركة