Backup und Wiederherstellung
Ein Backup von Notarium ist ein eingebauter Befehl des Images und kein Kopieren von Dateien von außen. notarium backup baut ein logisches ZIP, während der Dienst weiterarbeitet: Lesen bleibt durchgehend möglich, Schreibvorgänge werden nur für zwei kurze Checkpoints angehalten. Du brauchst nur Docker und einen laufenden Container — den Dienst anzuhalten ist nicht nötig.
meta.db im laufenden Betriebcp /data/meta.db ist kein Backup. Die Metadaten-Datenbank läuft im WAL-Modus: Bereits committete Zeilen können noch in meta.db-wal liegen, und einzeln kopierte Dateien ergeben keinen Stand zu einem einzigen Zeitpunkt. Eine aus so einer Kopie wiederhergestellte Instanz verliert Daten — stillschweigend.
Ein Backup erstellen
docker compose exec -T notarium backup > notarium-20260731.zip
Das ist ein vollwertiges Backup und keine Probe nach dem Motto „läuft es überhaupt?“: Der Befehl baut das Archiv, prüft es mit seinem eigenen Verifizierer und gibt erst dann die Bytes aus. Das Flag -T ist Pflicht: Sonst weist Compose ein Pseudoterminal zu, und ein Binärstrom übersteht das nicht unversehrt — das Archiv wird beschädigt. Ein einfaches docker exec weist standardmäßig kein Pseudoterminal zu, dort brauchst du das Flag also nicht. Die Standardausgabe ist strikt für die ZIP-Bytes reserviert; Fortschritt und die abschließende Zusammenfassung gehen nach stderr und verunreinigen das Archiv nicht.
Für einen regelmäßigen Job
Die Umleitung mit > hat eine Tücke: Die Datei unter ihrem endgültigen Namen entsteht, bevor der Befehl irgendetwas getan hat. Stürzt Docker oder das Backup mittendrin ab, bleibt eine korrekt benannte Datei mit unvollständigem Inhalt zurück. Unten steht eine Veröffentlichung, die auch im Fehlerfall sicher ist: erst in eine temporäre Datei schreiben, sie auf die Platte durchschreiben und dann per atomarem Hardlink veröffentlichen.
backup="notarium-$(date -u +%Y%m%dT%H%M%SZ).zip"
partial="${backup}.partial.$$"
set -eu
umask 077
committed=0
cleanup() { test "$committed" -eq 1 || rm -f "$partial"; }
trap cleanup EXIT
docker compose exec -T notarium backup > "$partial"
sync -f "$partial"
sync -f "$(dirname "$backup")"
committed=1
ln "$partial" "$backup"
if sync -f "$(dirname "$backup")"; then
if rm "$partial"; then
sync -f "$(dirname "$backup")" ||
echo "backup warning: final is durable; partial cleanup fsync failed" >&2
else
echo "backup warning: final is durable; retaining recovery partial $partial" >&2
fi
else
echo "backup warning: final is visible; retaining durable recovery partial $partial" >&2
fi
trap - EXIT
set -e verhindert, dass das Archiv veröffentlicht wird, wenn Docker oder das Backup einen Fehler zurückgegeben haben. Der temporäre Name enthält die PID, deshalb streiten sich zwei gleichzeitig laufende Jobs nicht um dieselbe Datei. Die temporäre Datei und ihr Verzeichnis werden vor dem Veröffentlichungspunkt auf die Platte durchgeschrieben, und die Veröffentlichung selbst ist ein atomarer Hardlink, der niemals überschreibt: Zwei Jobs mit demselben Zielnamen können einander nicht überschreiben.
Fehler nach dem Veröffentlichungspunkt werden als Warnungen gemeldet, nicht als fälschlicher Fehlschlag: Die endgültige Datei liegt an ihrem Platz, und in Zweifelsfällen bleibt die temporäre Datei als Reservekopie erhalten. Selbst ein mehrdeutiger Exit-Code ungleich null von ln bewahrt sie auf — der Link kann angelegt worden sein, kurz bevor der Prozess abgebrochen wurde. umask 077 macht das Archiv nur für seinen Eigentümer lesbar. Halte die temporäre und die endgültige Datei auf demselben Dateisystem.
Für einen Container, der mit einfachem docker run unter dem Namen notarium gestartet wurde, gilt alles Obige unverändert — nur eine Zeile ist anders:
docker exec notarium backup > "$partial"
Den Wrapper oben braucht es, weil der stdout-Transport eine Datei nicht selbst veröffentlichen kann. Ist das Verzeichnis für die Archive bereits eingehängt — ein Backup-Share, NFS, ein Scratch-Volume bei read-only gemountetem Root —, erledigt backup --output /path/archive.zip dieselbe Arbeit: Es schreibt in eine temporäre Datei neben dem Ziel, schreibt sie auf die Platte durch, verifiziert sie und veröffentlicht per atomarem Hardlink, der niemals überschreibt; im Fehlerfall bleibt unter dem Zielnamen nichts zurück. Einen Shell-Wrapper brauchst du dann nicht, und nach stdout geht eine einzige JSON-Zusammenfassung.
Der Befehl wird per docker exec in dem Container ausgeführt, in dem der Dienst bereits läuft — nicht als eigener Container: Für einen konsistenten Stand muss er sich mit der laufenden Anwendung abstimmen.
Wann ein Backup scheitern kann
Das Backup ist so gebaut, dass du niemals unbemerkt ein inkonsistentes Archiv bekommst: Lässt sich der Stand nicht sauber erfassen, bricht der Befehl mit einem Fehler ab und veröffentlicht nichts. Zwei Fälle, in denen das passiert:
- Ein ununterbrochener Strom von Änderungen. Während das Archiv gebaut wird, müssen die Daten stillhalten; eine überlappende Schreiboperation löst einen erneuten Versuch aus, und bei einem endlosen Strom von Änderungen gibt der Befehl mit einem Fehler auf. In der Praxis begegnet dir das auf einer stark genutzten Instanz — versuch es einfach später noch einmal.
- Ein langer Import oder Export läuft. Das Backup braucht zwei sehr kurze Schreibpausen, und eine lang laufende Aufgabe passt da nicht hinein. Lege ein Backup nicht in dasselbe Zeitfenster wie einen Massenimport.
In beiden Fällen leidet der Dienst nicht: Die Schreibwarteschlange wird sofort freigegeben, und der Operator-Befehl hält die Anwendung nicht auf. Normales Lesen bleibt in jedem Fall über die gesamte Dauer eines Backups verfügbar.
Was im Archiv steckt
| Pfad im Archiv | Was es ist |
|---|---|
data/meta.db | Konten, Sessions, Mitgliedschaften, stabile Identifikatoren, Versionsverlauf und Job-Zustand. |
data/spaces/ | Die Markdown-Quelle der Wahrheit, samt Agenten-Gedächtnis und Projekt-Markerdateien. |
data/jobs/ | Fertige Artefakte und dauerhafte Import-Uploads. |
manifest.json | Formatversion, Zeitstempel, der exakte Satz an Verzeichnissen sowie Größe, mtime und SHA-256 jeder einzelnen Datei. |
Das abgeleitete Verzeichnis data/engine/ ist nicht enthalten: Die Indizes werden nach einer Wiederherstellung aus den Dateien neu aufgebaut. Von den unfertigen Dateien werden nur die internen übersprungen — die temporären Dateien atomarer Notiz-Schreibvorgänge, nicht zu Ende hochgeladene Importe und Teilstücke von Export-Artefakten. Gewöhnliche Benutzerdateien, deren Name auf .part endet, bleiben im Archiv — das ist legitim.
Das ZIP enthält Konten und Session-Zustand aus der Metadaten-Datenbank. Behandle es wie ein Secret: umask 077 im Snippet oben macht ein neues Archiv nur für seinen Eigentümer lesbar.
Verifizierung
Die Verifizierung ändert nichts und gehört in jeden Backup-Job:
docker compose exec -T notarium backup verify < notarium-20260722.zip
# für einfaches docker run:
docker exec -i notarium backup verify < notarium-20260722.zip
Bei Erfolg gibt der Befehl eine einzige JSON-Zusammenfassung aus und beendet sich mit Exit-Code 0. Abgelehnt werden: unsichere und doppelte Pfade, im Manifest nicht erfasste Dateien, ein nicht exakt passender Satz an Verzeichnissen, Abweichungen bei Größen und Hashes, fehlerhafte Zeit-Metadaten, überschrittene Limits und eine nicht bestandene SQLite-Integritätsprüfung. Das aktive Datenverzeichnis wird dabei weder gelesen noch verändert.
Hashes fangen zufällige Beschädigungen ab, schützen aber nicht vor Manipulation: Wer sowohl den Inhalt als auch das Manifest austauschen kann, besteht die Prüfung. Betrachte den Backup-Speicher als vertrauenswürdigen Zustand mit eingeschränktem Zugriff — oder ergänze Signatur oder Verschlüsselung in der Schicht, die das Archiv transportiert.
Wiederherstellung
Die Wiederherstellung ist eine Offline-Notfalloperation. Sie akzeptiert nur ein sauberes, leeres Datenverzeichnis; sie führt niemals mit einer bestehenden Instanz zusammen und überschreibt sie auch nicht.
Lege ein frisches Volume an und stelle den Dienst vor dem Start darauf um:
set -eu
docker compose stop notarium
# altes Volume beiseiteschieben; in Compose ein leeres /data einhängen
docker compose run --rm --no-deps -T notarium restore \
< notarium-20260722.zip
docker compose up -d --force-recreate --no-deps notarium
Der Container muss danach wirklich neu erstellt und nicht bloß gestartet werden: docker compose start würde den alten Container mit seiner alten Mount-Konfiguration hochfahren. Behalte das alte Volume, bis du die wiederhergestellte Instanz geprüft hast.
Die Wiederherstellung verifiziert das gesamte Archiv, bevor sie irgendetwas einspielt. Wird der Vorgang mitten im Einspielen abgebrochen, bleibt ein ausdrücklicher Marker zurück: Behandle dieses Ziel als verbraucht und stelle in ein neues, leeres Ziel wieder her, statt den Vorgang durchzudrücken oder zusammenzuführen.
Was du nach einer Wiederherstellung prüfen solltest:
- Melde dich mit einem Konto aus dem Backup an.
- Öffne ein paar Spaces und überzeuge dich, dass Adressen und Identifikatoren erhalten sind.
- Öffne eine Notiz, die du bearbeitet hattest, und sieh dir ihren Verlauf an.
- Prüfe die Import- und Export-Jobs, deren Uploads oder Artefakte dir wichtig sind.
Die wiederherzustellende Metadaten-Datenbank muss ein Migrationsjournal mitbringen, das der Ziel-Build akzeptiert. Bei einer nicht leeren DB ohne Journal bricht die Wiederherstellung kontrolliert ab (fail closed) — sie rät ihre Version nicht und trägt auch von sich aus keine ein. Siehe Datenbank.
Grenzen
Der eingebaute Befehl unterstützt nur das kanonische Layout: ein einziges Datenverzeichnis und eine Metadaten-Datenbank in einer SQLite-Datei. Zeigt META_DB_URL auf Postgres oder liegen Notizen außerhalb von DATA_DIR, bricht der Befehl kontrolliert ab, statt dir ein unvollständiges Archiv unterzuschieben — nimm in diesem Fall die Bordmittel deiner Datenbank plus Snapshots der eingehängten Verzeichnisse.
Die Zwischendateien von Backup und Verifizierung liegen standardmäßig in /tmp. Ein Streaming-Backup verifiziert sich vor der Veröffentlichung selbst und kann deshalb zeitweise Platz für das Archiv plus zwei entpackte Stufen benötigen; eine eigenständige Verifizierung braucht das Archiv plus eine. Die Wiederherstellung puffert den eingehenden Stream im Scratch-Verzeichnis, entpackt das Archiv aber direkt in das frische Datenverzeichnis selbst. Ist das Root-Dateisystem des Containers nur lesend eingehängt oder ist dein Datenbestand groß, verweise NOTARIUM_BACKUP_TMPDIR auf ein eingehängtes, beschreibbares Verzeichnis.
Komprimierte und entpackte Eingabe sind auf 64 GiB und eine Million Einträge begrenzt; Namen, die internen ZIP-Strukturen und manifest.json haben ein eigenes Speicherlimit von 32 MiB. Vertrauenswürdige große Installationen können NOTARIUM_BACKUP_MAX_BYTES, NOTARIUM_BACKUP_MAX_ENTRIES und NOTARIUM_BACKUP_MAX_METADATA_BYTES anheben — siehe Umgebungsvariablen.
Weiter
- Image-CLI — der vollständige Kontrakt über Befehle, Streams und Exit-Codes.
- Datenbank — was die Metadaten-Datenbank genau enthält und warum sie in keinem Backup fehlen darf.
- Produktion — Reverse-Proxy, die Single-Instance-Invariante, der laufende Betrieb.