Backup und Wiederherstellung
Ein Backup des Nusszopf besteht aus drei Dateien und ist mit einem Skript in wenigen Sekunden gemacht, während die Seite weiterläuft. Genauso wichtig ist das Gegenstück: Ein Backup, das Du nie zurückgespielt hast, ist keins. Das Zurückspielen auf einem leeren Server ist geprüft (mit dem Skript und den Befehlen dieser Seite, unverändert ausgeführt); übe es trotzdem gelegentlich auf einem Ersatzrechner.
Was gesichert wird
| Datei | Inhalt | Warum |
|---|---|---|
postgres.dump |
Die ganze Datenbank: Konten mit Passwort-Hashes, Projekte, Gesuche, Aufrufzähler, Newsletter-Anmeldungen samt Einwilligung, offene Passwort-Links, fehlgeschlagene Aufträge | Das sind die Daten der Anwendung. |
storage.tar.gz |
Das Volume laravel-storage: die Avatare und temporäre Uploads |
Hochgeladene Dateien gibt es sonst nirgends. |
installation.tar.gz |
Das Installationsverzeichnis: .env, docker-compose.yaml, legal/, eine compose.override.yaml und was sonst dort liegt |
Die .env enthält den APP_KEY (ohne ihn funktionieren verschickte Links nicht mehr) und die Passwörter, mit denen die Datenbank angelegt wurde. NUSSZOPF_VERSION darin sagt, zu welcher Version der Dump gehört. |
Bewusst nicht gesichert:
- Der Suchindex (
meilisearch-data) ist abgeleitet. Die Wiederherstellung baut ihn mitsearch:reindexneu auf. - Redis (
redis-data): Sitzungen, wartende Aufträge, Cache. Nach einer Wiederherstellung müssen sich alle neu anmelden, und ein Auftrag, der beim Backup noch wartete (etwa eine Mail), läuft nicht. Eine fehlgeschlagene Mail steht infailed_jobs, also in der Datenbank. - Die Docker-Images. Sie werden für die Version aus der
.envneu geladen. - Dein Reverse Proxy und seine Zertifikate. Sichere sie mit dem Proxy.
Das Backup-Skript
Speichere es als /usr/local/bin/nusszopf-backup.sh und mache es mit chmod 700 ausführbar. Liegt Deine Installation nicht
in /opt/nusszopf, ändere NUSSZOPF_DIR.
#!/bin/sh
# Nusszopf backup: the database, the uploaded files and the installation directory (Handbuch: Backup und Wiederherstellung).
set -eu
NUSSZOPF_DIR=/opt/nusszopf # your installation: docker-compose.yaml and .env
BACKUP_ROOT=/opt/nusszopf-backups
KEEP_DAYS=30
umask 077 # a backup holds .env, with APP_KEY and the passwords
cd "$NUSSZOPF_DIR" # cron starts elsewhere, and docker compose must run here
BACKUP_DIR="$BACKUP_ROOT/$(date +%Y-%m-%d_%H-%M-%S)"
mkdir -p "$BACKUP_DIR.incomplete"
# The database first: a file uploaded while the rest runs is then an unused extra, never a missing avatar.
docker compose exec -T postgres sh -c 'pg_dump --format=custom -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > "$BACKUP_DIR.incomplete/postgres.dump"
docker compose exec -T postgres pg_restore --list < "$BACKUP_DIR.incomplete/postgres.dump" > /dev/null
docker compose run --rm --no-deps -T --entrypoint tar php-fpm czf - -C /var/www/storage/app . > "$BACKUP_DIR.incomplete/storage.tar.gz"
tar tzf "$BACKUP_DIR.incomplete/storage.tar.gz" > /dev/null
tar czf "$BACKUP_DIR.incomplete/installation.tar.gz" .
mv "$BACKUP_DIR.incomplete" "$BACKUP_DIR"
find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +"$KEEP_DAYS" -exec rm -rf {} +
echo "Backup written to $BACKUP_DIR"Starte es täglich per cron. Öffne die Crontab von root mit crontab -e und trage zum Beispiel ein, dass es jede Nacht um
3 Uhr läuft:
0 3 * * * /usr/local/bin/nusszopf-backup.sh >> /var/log/nusszopf-backup.log 2>&1Was Du wissen solltest:
- Das Skript braucht den laufenden Stack, denn es fragt PostgreSQL nach dem Dump.
- Ein fehlgeschlagenes Backup ist sichtbar: Das Skript bricht beim ersten Fehler mit einem Exit-Code ungleich null ab, das
Log nennt den Grund, und der Ordner behält die Endung
.incomplete. Nur ein Ordner ohne diese Endung ist ein vollständiges Backup. Schau ab und zu ins Log und in die Ordnerliste, denn von allein warnt niemand. - Kopiere die Backups auf einen anderen Rechner. Ein Backup auf derselben Platte überlebt den Verlust des Servers nicht.
Kopiere
/opt/nusszopf-backupszum Beispiel perrsyncaus der Crontab eines anderen Rechners. Die Ordner sind nur für root lesbar; behandle auch die Kopien als geheim, denn sie enthalten die.env. - Der Dump ist in sich konsistent. Die Dateien werden einen Moment später kopiert; ein dazwischen hochgeladener Avatar ist im Backup, wird aber nicht verwendet.
- Aufbewahrt werden 30 Tage (
KEEP_DAYS).
Wiederherstellen
Die Befehle stellen Datenbank, Dateien und Installationsverzeichnis genau so wieder her, wie sie zum Zeitpunkt des Backups waren, und bauen den Suchindex neu auf. Alles, was danach geschrieben wurde, geht verloren. Dieselben Befehle dienen auch als Rollback nach einem Deployment.
1. Vorbereiten.
- Auf einem neuen Server: Installiere Docker Engine mit dem Compose-Plugin (wie bei der Installation),
kopiere den Backup-Ordner dorthin und lege das Verzeichnis an:
mkdir -p /opt/nusszopf. - Auf der bestehenden Installation (Rollback oder ein älteres Backup): Stoppe sie mit
docker compose down, ohne-v.
2. Backup wählen. Ein vollständiger Ordner, also ohne .incomplete:
RESTORE_DIR=/opt/nusszopf-backups/2026-09-25_03-00-003. Zurückspielen, als root:
cd /opt/nusszopf
tar xzf "$RESTORE_DIR/installation.tar.gz"
docker compose pull
docker compose up -d --wait postgres redis
docker compose exec -T redis redis-cli FLUSHALL
docker compose exec -T postgres sh -c 'dropdb --if-exists -U "$POSTGRES_USER" "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
docker compose exec -T postgres sh -c 'pg_restore --exit-on-error -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < "$RESTORE_DIR/postgres.dump"
docker compose run --rm --no-deps -T --entrypoint sh php-fpm -c 'find /var/www/storage/app -mindepth 1 -delete && tar xzf - -C /var/www/storage/app' < "$RESTORE_DIR/storage.tar.gz"
docker compose up -d --wait
docker compose exec php-fpm php artisan search:reindex
docker compose exec php-fpm php artisan nusszopf:healthWas die Zeilen tun:
tar xzf …installation.tar.gzbringt.env,legal/, Deinecompose.override.yamlund diedocker-compose.yamlder Version zurück, mit der das Backup entstand.docker compose pullholt dann deren Images, damit Code und Datenbank zusammenpassen.- Nur PostgreSQL und Redis laufen, während die Daten zurückkehren, damit nichts dazwischen schreibt.
FLUSHALLleert Redis. Aufträge nach dem Backup gehören zu Daten, die es nicht mehr gibt, und Sitzungen werden abgemeldet.dropdb/createdbbeginnen mit einer leeren Datenbank. Nimm nichtpg_restore --cleanstattdessen: Nach einem Upgrade hat die Datenbank Tabellen, die das Backup nicht kennt.--cleanscheitert dann auf halbem Weg und hinterlässt ein Gemisch aus zwei Versionen, das sich nicht mehr upgraden lässt.- Das Storage-Volume wird geleert und über das
php-fpm-Image gefüllt, damit die Dateien ihren Besitzer behalten. up -d --waitstartet alles und kehrt zurück, sobald jeder Dienst gesund ist (etwa eine Minute).php-fpmmigriert nichts, denn der Code ist der des Backups.search:reindexfüllt den Suchindex über den Worker in Sekunden bis Minuten neu.
Scheitert eine Zeile, behebe die Ursache (zum Beispiel braucht docker compose pull Netz) und führe alle Zeilen von vorn
aus. Jede lässt sich wiederholen.
Danach gilt (so geprüft): Die Anmeldung mit den Passwörtern aus dem Backup funktioniert, aber alle müssen sich neu anmelden. Vor dem Backup verschickte Links funktionieren weiter (Passwort zurücksetzen, E-Mail bestätigen, Newsletter bestätigen). Öffentliche Projekte werden wieder gefunden, private nicht. Die Avatare werden ausgeliefert. Worker und Scheduler melden sich binnen einer Minute gesund.
Willst Du danach auf eine neuere Version, spiele sie wie gewohnt ein: Deployment.
Warum das Skript so aussieht
Frühere Fassungen dieses Skripts scheiterten, ohne es zu sagen: Die Dump-Zeile las $DB_DATABASE aus einer Shell, die es nicht
hat, und unter cron, das nicht im Installationsverzeichnis startet, fand jedes docker compose keine Datei und schrieb einen
leeren Dump, während das Skript mit Erfolg endete. Die Fassung oben behebt beides (set -eu, cd "$NUSSZOPF_DIR", ein
pg_restore --list als Test jedes Dumps). Kopiere deshalb keine älteren Varianten und ändere die Prüfzeilen nicht.