rqlite mit Docker Compose: verteilte SQLite-Datenbank im Cluster betreiben
rqlite repliziert SQLite per Raft über mehrere Knoten. Die Anleitung zeigt Einzelknoten und Cluster mit Docker Compose, Authentifizierung per auth.json, Backup und echten Restore nach Totalverlust, das Update von 10.6.4 auf 10.6.5 sowie das Verhalten bei Knotenausfall.
Geprüft am 25.10.2026 · für rqlite 10.6.5
Mit KI erstellt – redaktionelle Prüfung ausstehend
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

rqlite legt eine Raft-Replikation vor SQLite: Jeder Schreibzugriff geht über den Leader, eine Mehrheit der Knoten bestätigt ihn, und fällt ein Rechner aus, übernimmt ein anderer. Das Go-Programm steht unter MIT-Lizenz und hat auf GitHub knapp 18.000 Sterne. Aktuell ist sie im Gespräch, weil Entwickler Philip O'Toole zum zwölften Geburtstag des Projekts einen vielbeachteten Rückblick auf Hacker News gebracht hat. Für kleine und mittlere Unternehmen ist rqlite interessant, wenn eine kleine relationale Datenbank ausfallsicher sein soll, ohne gleich einen PostgreSQL-Cluster mit Patroni zu betreiben.
Diese Anleitung zeigt einen abgesicherten Einzelknoten, Sicherung und Rücksicherung, das Update und einen Cluster aus drei Knoten mit Ausfalltest. Alle Ausgaben stammen aus unserem Testlauf mit rqlite 10.6.4 und 10.6.5.
Voraussetzungen
rqlite ist sparsam. Für einen Einzelknoten reichen 1 bis 2 CPU-Kerne und 1 GB RAM, der Bedarf wächst mit der Datenbank. Für echte Ausfallsicherheit gehören die drei Knoten auf drei getrennte Server oder VMs. Sie brauchen:
- einen Linux-Server mit Docker Engine und Docker Compose Plugin, etwa Debian 12 oder Ubuntu 24.04, eingerichtet wie in unserer Grundlagen-Anleitung zu Docker Compose,
curlfür die API-Aufrufe und optionalsqlite3zum Prüfen von Sicherungen,- für den Cluster drei Hosts, die sich über Port 4002 erreichen. Port 4002 darf nur zwischen den Knoten offen sein, Port 4001 nur für Ihre Anwendungen.
| Eckdatum | Wert |
|---|---|
| Image | rqlite/rqlite:10.6.5 (Docker Hub, Release vom 24.10.2026) |
| Ports | 4001 HTTP-API und Weboberfläche, 4002 Raft zwischen den Knoten |
| Volume | /rqlite/file (Raft-Log, Snapshots, SQLite-Datei) |
| Umgebungsvariablen | NODE_ID, HTTP_ADDR, RAFT_ADDR, optional SQLITE_EXTENSIONS |
| Lizenz | MIT |
Schritt 1: Einzelknoten mit Authentifizierung anlegen
Die offiziellen Compose-Beispiele starten rqlite ohne Anmeldung und mit latest. Ohne -auth darf aber jeder im Netz Tabellen löschen, und latest macht Updates unkontrollierbar. Legen Sie daher einen Ordner /opt/rqlite mit drei Dateien an. Die .env legt die Version fest; für den späteren Update-Test beginnen wir bewusst eine Version zurück:
mkdir -p /opt/rqlite && cd /opt/rqlite
echo "RQLITE_TAG=10.6.4" > .env
Die compose.yaml bindet die API nur an 127.0.0.1. Den Raft-Port veröffentlicht ein Einzelknoten gar nicht, er wird nur im Cluster gebraucht:
services:
rqlite:
image: rqlite/rqlite:${RQLITE_TAG:-10.6.5}
hostname: rqlite-1
restart: unless-stopped
command: -auth /config/auth.json
environment:
NODE_ID: rqlite-1
HTTP_ADDR: rqlite-1:4001
RAFT_ADDR: rqlite-1:4002
volumes:
- rqlite-data:/rqlite/file
- ./auth.json:/config/auth.json:ro
ports:
- "127.0.0.1:4001:4001"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://rqlite-1:4001/readyz"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
rqlite-data:
Das Entrypoint-Skript baut aus den Umgebungsvariablen die Startparameter und hängt die Optionen aus command an. Wie Sie Healthchecks und Secrets allgemein härten, zeigt unsere Anleitung zu Healthchecks, Secrets und Non-Root.
Die auth.json enthält Benutzer, Passwörter und Rechte. Erzeugen Sie die Passwörter zufällig, etwa mit openssl rand -base64 24, und setzen Sie die Datei auf chmod 600:
[
{"username": "admin", "password": "LANGES-ZUFALLSPASSWORT-1", "perms": ["all"]},
{"username": "app", "password": "LANGES-ZUFALLSPASSWORT-2", "perms": ["execute", "query"]},
{"username": "backup", "password": "LANGES-ZUFALLSPASSWORT-3", "perms": ["backup"]},
{"username": "*", "perms": ["status", "ready"]}
]
admin darf alles, die Anwendung nur execute und query, das Sicherungsskript nur backup. Der Sonderbenutzer * steht für anonyme Anfragen, hier nur Status und Bereitschaft für das Monitoring. Starten Sie den Dienst:
docker compose up -d
docker compose logs --no-log-prefix | grep "rqlited starting"
[rqlited] 2026/10/25 00:28:49 rqlited starting, version v10.6.4, SQLite 3.53.4, commit 64145717224d620809174cb2235ac86ec604aac5, compiler (toolchain) gc, compiler (command) musl-gcc
Verifizieren: docker compose ps zeigt den Dienst nach wenigen Sekunden als healthy. Ein anonymer Aufruf von curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:4001/status liefert 200, weil * das Recht status hat. Ein Schreibversuch ohne Anmeldung muss dagegen 401 liefern.
Schritt 2: Daten über die HTTP-API schreiben und lesen
Schreibende Befehle gehen an /db/execute, Abfragen an /db/query, jeweils als JSON-Array. Werte übergeben Sie als Parameter, das schützt vor SQL-Injection. Wir legen eine Marker-Tabelle an, die später die Wiederherstellung beweist:
APP="app:LANGES-ZUFALLSPASSWORT-2"
curl -s -u "$APP" -XPOST 'http://127.0.0.1:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["CREATE TABLE marker (id INTEGER PRIMARY KEY, text TEXT, created TEXT)",
["INSERT INTO marker(text, created) VALUES(?, datetime())", "restore-test-2026-10-25"]]'
curl -s -u "$APP" -G 'http://127.0.0.1:4001/db/query' --data-urlencode 'q=SELECT * FROM marker'
{"results":[{"columns":["id","text","created"],"types":["integer","text","text"],"values":[[1,"restore-test-2026-10-25","2026-10-25 00:28:52"]]}]}
Wichtig für Skripte: SQL-Fehler stehen im Feld error, der HTTP-Status bleibt 200. Ein Insert in eine fehlende Tabelle lieferte bei uns "error":"no such table: gibtsnicht". Schreibende SQL-Befehle über /db/query lehnt rqlite ab, weil dieser Endpunkt eine reine Leseverbindung nutzt:
{"results":[{"error":"attempt to change database via query operation","error_v2":{"message":"attempt to write a readonly database","sqlite":{"code":8,"extended_code":8}}}]}
Die eingebaute Weboberfläche erreichen Sie unter /console/. Sie verlangt einen Benutzer mit dem Recht ui oder all, anonym kam 401. Vom Arbeitsplatz genügt ein SSH-Tunnel (ssh -L 4001:127.0.0.1:4001 server), dann öffnen Sie http://localhost:4001/console/.


Verifizieren: Die Abfrage liefert den Marker restore-test-2026-10-25, und die Konsole zeigt unter Schema die Tabelle marker mit drei Spalten.
Schritt 3: Rechte und Fehlermeldungen prüfen
Prüfen Sie die Rechte auch negativ. Im Test lieferten alle drei Fälle HTTP 401: Schreiben ohne Anmeldung, Abfrage mit falschem Passwort und ein Sicherungsabruf durch den Benutzer app, dem das Recht backup fehlt.
curl -s -o /dev/null -w '%{http_code}\n' -u admin:falsch 'http://127.0.0.1:4001/db/query?q=SELECT%201'
curl -s -o /dev/null -w '%{http_code}\n' -u "$APP" http://127.0.0.1:4001/db/backup
401
401
Verlässt der Verkehr den Server, aktivieren Sie HTTPS mit -http-cert und -http-key, denn Basic Auth überträgt Passwörter sonst im Klartext.
Verifizieren: Alle drei Aufrufe liefern 401, der korrekte Aufruf mit dem Benutzer app liefert 200.
Schritt 4: Sicherung erstellen
rqlite sichert im laufenden Betrieb. /db/backup liefert eine vollständige SQLite-Datei, mit ?fmt=sql einen SQL-Textdump. Der Benutzer backup genügt dafür:
mkdir -p /opt/rqlite/backup
curl -s -u "backup:LANGES-ZUFALLSPASSWORT-3" http://127.0.0.1:4001/db/backup \
-o /opt/rqlite/backup/rqlite-$(date +%F).sqlite3 -w 'HTTP %{http_code} %{size_download} Bytes\n'
head -c 15 /opt/rqlite/backup/rqlite-*.sqlite3; echo
HTTP 200 8192 Bytes
SQLite format 3
Abbrüche meldet rqlite laut Dokumentation im Trailer-Header X-Stream-Error, prüfen Sie Sicherungen daher zusätzlich mit sqlite3 datei "PRAGMA integrity_check". Die Konsole bietet dieselbe Funktion unter Backup.

Verifizieren: Die Datei beginnt mit SQLite format 3, und curl ... /db/backup?fmt=sql zeigt die Zeile INSERT INTO "marker" VALUES(1,'restore-test-2026-10-25',...).
Schritt 5: Update auf 10.6.5 einspielen
Mit festem Tag ist das Update eine Zeile in der .env, vorher sichern Sie wie in Schritt 4:
sed -i 's/RQLITE_TAG=10.6.4/RQLITE_TAG=10.6.5/' .env
docker compose pull && docker compose up -d
curl -s -u "admin:LANGES-ZUFALLSPASSWORT-1" http://127.0.0.1:4001/status | jq -r .build.version
v10.6.5
Der Container wurde neu erstellt, der Marker war danach sofort abrufbar. Im Cluster aktualisieren Sie die Knoten nacheinander und warten jeweils, bis /nodes wieder alle Knoten als erreichbar meldet.
Verifizieren: /status meldet v10.6.5, das Image-Label org.opencontainers.image.version ebenfalls, und die Marker-Abfrage liefert weiterhin eine Zeile.
Schritt 6: Wiederherstellung nach Totalverlust
Eine Sicherung ist erst etwas wert, wenn die Rücksicherung klappt. Wir haben den Ernstfall durchgespielt und das Volume komplett gelöscht:
docker compose down -v
docker compose up -d
curl -s -u "$APP" -G http://127.0.0.1:4001/db/query --data-urlencode 'q=SELECT text FROM marker'
{"results":[{"error":"no such table: marker", ...}]}
Die leere Datenbank laden Sie mit /db/load aus der SQLite-Datei. Das Recht load steckt in all, daher nutzen wir den Admin:
curl -s -u "admin:LANGES-ZUFALLSPASSWORT-1" -XPOST http://127.0.0.1:4001/db/load \
-H 'Content-type: application/octet-stream' \
--data-binary @/opt/rqlite/backup/rqlite-2026-10-25.sqlite3 -w ' HTTP %{http_code}\n'
curl -s -u "$APP" -G http://127.0.0.1:4001/db/query --data-urlencode 'q=SELECT * FROM marker'
{"results":[]} HTTP 200
{"results":[{"columns":["id","text","created"],...,"values":[[1,"restore-test-2026-10-25","2026-10-25 00:28:52"]]}]}
Der Marker samt Zeitstempel ist zurück. Laut Dokumentation eignet sich /db/load bis etwa 100 MB, größere Dateien spielen Sie über /boot in einen Einzelknoten ein.
Verifizieren: Vor dem Load meldet die Abfrage no such table: marker, danach liefert sie den ursprünglichen Datensatz mit unverändertem created-Wert.
Schritt 7: Cluster aus drei Knoten aufbauen
Raft verlangt eine Mehrheit: Bei drei Knoten müssen zwei laufen. Die folgende Datei startet drei Knoten auf einem Host zum Ausprobieren; in Produktion verteilen Sie sie auf drei Server mit erreichbaren Adressen. Knoten 2 und 3 treten mit -join bei und melden sich dabei als Benutzer cluster an:
x-rqlite: &rqlite
image: rqlite/rqlite:${RQLITE_TAG:-10.6.5}
restart: unless-stopped
services:
rqlite-1:
<<: *rqlite
hostname: rqlite-1
command: -auth /config/auth.json
environment:
NODE_ID: rqlite-1
HTTP_ADDR: rqlite-1:4001
RAFT_ADDR: rqlite-1:4002
volumes:
- ./auth.json:/config/auth.json:ro
- data-1:/rqlite/file
ports:
- "127.0.0.1:4001:4001"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://rqlite-1:4001/readyz?noleader"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
rqlite-2:
<<: *rqlite
hostname: rqlite-2
command: -auth /config/auth.json -join rqlite-1:4002 -join-as cluster
environment:
NODE_ID: rqlite-2
HTTP_ADDR: rqlite-2:4001
RAFT_ADDR: rqlite-2:4002
volumes:
- ./auth.json:/config/auth.json:ro
- data-2:/rqlite/file
ports:
- "127.0.0.1:4011:4001"
depends_on:
rqlite-1:
condition: service_healthy
rqlite-3:
<<: *rqlite
hostname: rqlite-3
command: -auth /config/auth.json -join rqlite-1:4002 -join-as cluster
environment:
NODE_ID: rqlite-3
HTTP_ADDR: rqlite-3:4001
RAFT_ADDR: rqlite-3:4002
volumes:
- ./auth.json:/config/auth.json:ro
- data-3:/rqlite/file
ports:
- "127.0.0.1:4021:4001"
depends_on:
rqlite-1:
condition: service_healthy
volumes:
data-1:
data-2:
data-3:
Die auth.json des Clusters braucht zusätzlich {"username": "cluster", "password": "...", "perms": ["join", "remove"]}. Der Healthcheck fragt /readyz?noleader ab, den Grund nennt das Troubleshooting.
Verifizieren: curl -s -u admin:... http://127.0.0.1:4001/nodes?pretty listet drei Knoten mit "voter": true und "reachable": true, einer davon mit "leader": true. In der Konsole zeigt die Topologie den Leader mit Stern.

Schritt 8: Knotenausfall und Leader-Wechsel beobachten
Ein Schreibzugriff über den Follower 2 wurde an den Leader weitergeleitet. Danach haben wir den Leader rqlite-1 gestoppt. Gestoppt um 00:37:09.431, um 00:37:10.093 meldete rqlite-2 sich als neuer Leader. Schreibzugriffe über Knoten 2 und 3 liefen sofort weiter (HTTP 200). /nodes zeigte den fehlenden Knoten so:
"rqlite-1": {"voter": true, "reachable": false, "leader": false,
"error": "dial tcp: lookup rqlite-1 on 127.0.0.11:53: server misbehaving"}
Anschließend haben wir den zweiten Knoten gestoppt. Der verbliebene Knoten hat kein Quorum mehr und verweigert Schreibzugriffe und Standard-Abfragen:
leader not found HTTP 503
[+]node ok
[+]leader does not exist HTTP 503
Mit level=none liest der Knoten lokal und lieferte alle drei Zeilen mit HTTP 200. Die Daten können dabei veraltet sein. Nach dem Neustart aller Knoten waren wieder drei Knoten erreichbar und alle Zeilen vorhanden.
Verifizieren: Nach dem Stopp eines Knotens zeigt /status unter store.leader.node_id einen neuen Leader, Inserts liefern 200. Bei zwei fehlenden Knoten antwortet /readyz mit 503.
Troubleshooting
| Meldung | Ursache | Lösung |
|---|---|---|
dependency failed to start: container ...-rqlite-1-1 is unhealthy | Healthcheck auf /readyz: Nach Stopp aller Knoten wartet Knoten 1 auf einen Leader, Knoten 2 und 3 auf Knoten 1. | /readyz?noleader nutzen oder docker compose up -d --no-deps rqlite-2 rqlite-3 |
leader not found, HTTP 503 | Weniger als die Hälfte plus eins der Knoten läuft | Ausgefallene Knoten starten, für Notlesen level=none |
| HTTP 401 | Falsches Passwort oder fehlendes Recht in auth.json | Benutzer und perms prüfen, Konsole braucht ui |
attempt to change database via query operation | Schreibender SQL-Befehl an /db/query | /db/execute verwenden |
no such table: ... mit HTTP 200 | SQL-Fehler stehen im JSON, nicht im Statuscode | Feld error in Skripten auswerten |
Den Kaltstartfehler haben wir mit dem Healthcheck der offiziellen Beispieldatei selbst ausgelöst: /readyz meldete [+]leader does not exist mit HTTP 503, /readyz?noleader dagegen 200.
Häufige Fragen
Ersetzt rqlite PostgreSQL oder MariaDB?
Nein. rqlite passt zu kleinen bis mittleren Datenmengen, bei denen Ausfallsicherheit wichtiger ist als Schreibdurchsatz, denn jeder Schreibzugriff wartet auf die Mehrheit. Anwendungen sprechen HTTP oder nutzen Client-Bibliotheken.
Wie viele Knoten brauche ich?
Drei Knoten überstehen einen Ausfall, fünf zwei. Leseknoten ohne Stimmrecht hängen Sie als Read Replicas an.
Kann ich die Raft-Ports offen ins Netz stellen?
Nein. Port 4002 gehört nur zwischen die Knoten. Beschränken Sie ihn per Firewall, etwa mit ufw wie in unserer Anleitung zu ufw und Fail2ban. Für Verschlüsselung zwischen den Knoten bietet rqlite -node-cert und -node-key.
Gibt es automatische Sicherungen?
Ja. Mit -auto-backup lädt der Leader regelmäßig eine komprimierte Kopie in einen S3-kompatiblen Speicher oder ins Dateisystem. Alternativ rufen Sie /db/backup aus Schritt 4 per Cronjob auf.
Fazit
rqlite ist schnell eingerichtet und verhält sich im Ausfall wie versprochen: Der Leader-Wechsel dauerte im Test unter einer Sekunde, ohne Quorum gibt es klare 503-Meldungen. Sicherungen sind normale SQLite-Dateien. Achten Sie auf drei Punkte: Authentifizierung von Anfang an aktivieren, Versionen fest vorgeben und beim Cluster den Healthcheck mit noleader verwenden, sonst bleibt der Kaltstart hängen.
Weiterführende Anleitungen und Quellen
- Docker Compose unter Linux: Grundlage für Selfhosting
- Docker Compose absichern: Secrets, Healthchecks, Non-Root
- DbGate mit Docker Compose: Datenbank-Manager absichern
- SQLite 3.54: Neuerungen in der Shell
- rqlite auf GitHub und Release-Notizen
- rqlite-Dokumentation: Security, Backup and Restore und Clustering
- Offizielle Docker-Compose-Beispiele
- Philip O'Toole: Twelve years of distributed SQLite


