Skip to content

Deploying.de

Doug Blank edited this page Sep 22, 2026 · 2 revisions

🌐 English · Français

Bereitstellung

deploy/ ist die echte Mehrbenutzer-Form von Gramps Connect: ein containerisiertes app/-Frontend + gramps-web-api- Backend, unterlegt mit echtem Postgres (über das SharedPostgreSQL- Add-on), vorgeschaltet mit Caddy für TLS, gedacht dafür, wirklich irgendwo gehostet zu werden — echte Geheimnisse, eine echte Domain/ein echtes Zertifikat und mehrere Benutzer mit jeweils eigenem Login. Es ist außerdem der einzige Weg, Live-Zusammenarbeit in Aktion zu sehen; die eigenständige Desktop-Version ist absichtlich für einen einzelnen Benutzer ausgelegt, es gibt dort also niemand sonst, dessen Änderungen man beim Erscheinen zusehen könnte.

Das Backend ist das offizielle, unveränderte dmstraub/gramps-webapi- Image — dasselbe, das die eigene CI von gramps-project/gramps-web-api bei jedem Release veröffentlicht, und auf dem gramps-project/gramps-web selbst aufbaut — kein von diesem Repository selbst aus dem Quellcode gebauter Build. Das hält diese Bereitstellung eine schlichte, standardmäßige gramps-web-api-Instanz, mit der jeder kompatible Client sprechen kann, nicht nur das eigene Frontend von Gramps Connect. Die Add-ons SharedPostgreSQL/PostgreSQL/FilterRules/JSON, Mehrstammbaum-Unterstützung und kompilierte Übersetzungen stammen alle bereits aus diesem Image; das Einzige, was deploy/Dockerfile hinzufügt, ist das Frontend von app/, obenauf als statische Dateien geschichtet. Der Kompromiss: dieses Upstream-Image basiert auf gramps-web-base (~4,3 GB — torch, sentence-transformers, opencv, 45 Tesseract-Sprachpakete) mit bedingungslos installierten KI-Extras, da es keine offizielle schlanke Variante zum Ziehen gibt.

Frontend und Backend teilen sich standardmäßig einen Container/Ursprung (gramps_webapi liefert die gebaute SPA über STATIC_PATH aus und behandelt /api/* im selben Prozess), Gramps Connect selbst braucht also keine CORS-Konfiguration — das ist nur relevant für ein anderes, separat gehostetes Frontend, das von außen hereinruft (in diesem Fall GRAMPSWEB_CORS_ORIGINS in deploy/.env setzen).

Dienste: caddy (TLS-Terminierung, der einzige veröffentlichte Einstiegspunkt), app (gunicorn, liefert das Frontend + /api/* aus), worker (Celery, führt Import-/Medien-/Suchindex-Neuaufbau-Jobs aus), postgres (Stammbaumdaten über SharedPostgreSQL), redis (Celery-Broker).

Lokal ausführen

cp deploy/.env.example deploy/.env    # dann bearbeiten -- siehe die Kommentare in der Datei
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build

Dann https://localhost aufrufen (Caddy stellt sich automatisch selbst ein lokales, selbstsigniertes Zertifikat aus — die Browser-Warnung akzeptieren).

Auf einem echten Host ausführen

Einmalig auf GitHubs Runnern bauen statt auf dem Host (gh workflow run build-docker.yml, oder im Actions-Tab auslösen), was ghcr.io/<owner>/gramps-connect:latest veröffentlicht, dann auf dem Host:

cp deploy/.env.example deploy/.env    # diesmal mit echten Geheimnissen/Domain
docker compose -f deploy/docker-compose.yml --env-file deploy/.env pull
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d

Eine Domain auf den Host zeigen lassen und deploy/Caddyfile bearbeiten (siehe TLS unten), um statt des selbstsignierten ein echtes Zertifikat zu bekommen.

Docker-Befehle

Alle folgenden Befehle setzen voraus, im Wurzelverzeichnis des Repositorys zu sein. docker compose ist die Plugin-Form; hat die eigene Docker-Installation nur das eigenständige Binary, stattdessen docker-compose verwenden (gleiche Optionen so oder so).

# Status aller fünf Dienste
docker compose -f deploy/docker-compose.yml ps

# Logs (-f zum Mitverfolgen hinzufügen, --tail=100 zum Begrenzen)
docker compose -f deploy/docker-compose.yml logs app
docker compose -f deploy/docker-compose.yml logs worker

# Einen Dienst neu starten (z. B. nach einer Änderung in deploy/.env)
docker compose -f deploy/docker-compose.yml up -d
# ^ erzeugt jeden Dienst neu, dessen Konfiguration (Image, Umgebung, Volumes)
#   sich geändert hat; zuerst --build hinzufügen, wenn deploy/Dockerfile
#   oder der Backend-Quellcode in app/ bearbeitet wurde.

# Alles stoppen, Daten behalten (Volumes bleiben erhalten)
docker compose -f deploy/docker-compose.yml down

# Stoppen und alle Daten löschen (Vorsicht -- löscht Postgres, Medien, Benutzer usw.)
docker compose -f deploy/docker-compose.yml down -v

# In einen laufenden Container hineinshellen
docker compose -f deploy/docker-compose.yml exec app sh

Anmeldedaten: diese Bereitstellung vs. die eigenständige Version

Diese Docker-Bereitstellung hat kein Standardpasswort — man setzt GRAMPSWEB_ADMIN_USER/GRAMPSWEB_ADMIN_PASSWORD selbst in deploy/.env vor dem ersten Start, und genau das sät der Entrypoint ein. Nichts erzeugt oder gibt ein Passwort für einen aus.

Das unterscheidet sich von gramps-connect-desktop (der Einzelbenutzer-PyInstaller-Demoversion, unabhängig von dieser Docker-Bereitstellung): dort wird immer ein festes Konto admin/admin eingesät, was dort in Ordnung ist, da es eine wegwerfbare lokale Demo ist, nichts, was auf einem echten Server exponiert wird.

Ersteinrichtung: der eingesäte Admin

Beim ersten Start (wenn die Volumes app-users/app-db erstmals leer sind) macht der Entrypoint Folgendes:

  • erzeugt und speichert einen Flask-Geheimschlüssel dauerhaft, falls GRAMPSWEB_SECRET_KEY leer gelassen wurde
  • führt Migrationen der Benutzerdatenbank aus
  • sät einen Site-Admin-Benutzer (stammbaumlos, Rolle 5) aus GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD (beide in deploy/.env erforderlich — kein admin/admin-Rückfall)

Ein stammbaumloser Site-Admin kann Stammbäume über die API erstellen/ auflisten/löschen, kann aber — da das Frontend von app/ keine Stammbaum-Auswahl-Oberfläche hat und immer erwartet, dass das JWT des angemeldeten Benutzers bereits einen Stammbaum trägt — die Daten eines Stammbaums erst durchsuchen, sobald einem zugewiesen. Es gibt keinen CLI-Befehl zum Anlegen eines Stammbaums im Mehrstammbaum-Modus, den ersten Stammbaum also einmalig über die API anlegen:

TOKEN=$(curl -sk -X POST https://localhost/api/token/ \
  -H 'Content-Type: application/json' \
  -d '{"username":"<admin>","password":"<admin-Passwort>"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")

TREE_ID=$(curl -sk -X POST https://localhost/api/trees/ \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Mein Familienstammbaum"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")

echo "$TREE_ID"

Dann entweder den Site-Admin selbst diesem Stammbaum zuweisen, oder (empfohlen — hält Site-Verwaltung und Stammbaum-Eigentümerschaft getrennt) stattdessen einen eigenen, stammbaumgebundenen Benutzer anlegen:

# Option A: den bestehenden Site-Admin dem Stammbaum zuweisen.
curl -sk -X PUT "https://localhost/api/users/<admin>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"tree\":\"$TREE_ID\"}"

# Option B: stattdessen einen separaten gewöhnlichen, an den Stammbaum
# gebundenen Benutzer anlegen. email und full_name sind vom Schema
# vorgeschrieben, auch wenn ungenutzt.
curl -sk -X POST "https://localhost/api/users/<username>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"email\":\"<email>\",\"full_name\":\"<vollständiger Name>\",\"password\":\"<Passwort>\",\"role\":4,\"tree\":\"$TREE_ID\"}"

role ist eine Ganzzahl (gramps_webapi.auth.const):

Rolle Wert Anmerkungen
ADMIN 5 Site-Admin; einzige Rolle, die stammbaumlos sein darf
OWNER 4 Volle Kontrolle über den eigenen Stammbaum
EDITOR 3 Kann Stammbaumdaten bearbeiten
CONTRIBUTOR 2 Kann Daten hinzufügen
MEMBER 1 Lesezugriff
GUEST 0 Minimaler Lesezugriff

Dann unter https://localhost/ mit dem gerade angelegten Benutzer anmelden — bereits vor einer Stammbaum-Zuweisung ausgestellte Tokens tragen sie nicht, also bei einer wiederverwendeten, bereits offenen Sitzung ab- und wieder anmelden.

Daten importieren (z. B. example.gramps, mit Medien)

TOKEN=$(curl -sk -X POST https://localhost/api/token/ \
  -H 'Content-Type: application/json' \
  -d '{"username":"<owner>","password":"<Passwort>"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")

# 1. Die .gramps-XML-Datei importieren. WICHTIG: dieser Endpunkt liest den
#    rohen Request-Body und schreibt ihn direkt auf die Platte -- er
#    parst KEIN multipart/form-data, also --data-binary verwenden, nicht
#    curls -F/--form (ein multipart-Upload beschädigt die Datei
#    stillschweigend mit Boundary-/Header-Bytes, und der Importer von
#    Gramps schlägt dann mit einem generischen, nicht hilfreichen
#    "Import failed" ohne weitere Details fehl).
curl -sk -X POST https://localhost/api/importers/gramps/file \
  -H "Authorization: Bearer $TOKEN" --data-binary @example.gramps

# Die Antwort ist ein Task-Handle (der Import läuft über den worker-Dienst
# via Celery) -- ihn abfragen, bis "state":"SUCCESS" (oder "FAILURE"):
curl -sk https://localhost/api/tasks/<task_id> -H "Authorization: Bearer $TOKEN"

# 2. Medien: die referenzierten Mediendateien in ein Zip packen (jede
#    Teilmenge/Übermenge ist in Ordnung -- Dateien werden per Prüfsumme
#    zugeordnet, nicht referenzierte werden ignoriert) und das Archiv
#    hochladen, wieder als roher Body:
curl -sk -X POST https://localhost/api/media/archive/upload/zip \
  -H "Authorization: Bearer $TOKEN" --data-binary @media.zip

# 3. Prüfen: Objektzahlen für den Stammbaum des angemeldeten Benutzers.
curl -sk https://localhost/api/metadata/ -H "Authorization: Bearer $TOKEN" \
  | python3 -m json.tool

Bekannte Falle: mit GRAMPSWEB_MEDIA_PREFIX_TREE=True (in dieser Compose-Datei standardmäßig gesetzt) werden Medien unter MEDIA_BASE_DIR/<tree_id>/ gespeichert, aber nichts legt dieses Unterverzeichnis pro Stammbaum automatisch an — weder POST /api/trees/ noch der Medien-Archiv-Upload-Task. Das Hochladen von Medien für einen Stammbaum schlägt mit Directory /app/media/<tree_id> does not exist fehl, bis es einmalig angelegt wird:

docker compose -f deploy/docker-compose.yml exec app mkdir -p /app/media/<tree_id>

Zeitüberschreitung bei großen Medien-Uploads: sowohl der Einzeldatei- (POST /api/media/) als auch der ZIP-Archiv-Upload- Endpunkt (POST /api/media/archive/upload/zip) lesen den Request-Body synchron innerhalb des gunicorn-Workers von app, bevor an Celery übergeben wird, sodass eine große Datei oder eine langsame Verbindung das Pro-Anfrage-Zeitlimit von gunicorn überdauern kann — der Browser sieht dann einen nackten Internal Server Error ohne JSON-Body (von gunicorn/Caddy, nicht von Gramps). deploy/docker-compose.yml setzt GUNICORN_TIMEOUT standardmäßig auf 600s; bei weiterhin fehlschlagenden Uploads über GUNICORN_TIMEOUT in deploy/.env weiter erhöhen (siehe die Kommentare in dieser Datei).

TLS

caddy ist der einzige Dienst mit veröffentlichten Ports (80/443) und terminiert TLS vor app. Ohne konfigurierte Domain erzeugt und speichert es automatisch eine eigene lokale CA und stellt daraus ein selbstsigniertes Zertifikat aus (tls internal in deploy/Caddyfile) — Browser zeigen beim ersten Mal eine Vertrauenswarnung; zum Fortfahren akzeptieren (oder die erzeugte CA zum System-/Browser-Vertrauensspeicher hinzufügen, wenn die Warnung ohne echte Domain verschwinden soll). Port 80 leitet auf 443 um.

Sobald eine echte Domain auf diesen Server zeigt, deploy/Caddyfile bearbeiten: den Block :443 { tls internal ... } durch example.com { reverse_proxy app:5000 } ersetzen und den :80-Block löschen — Caddy übernimmt ACME-Ausstellung, Erneuerung und die Port-80-Umleitung für eine echte Domain automatisch, keine tls-Direktive nötig. Außerdem PUBLIC_URL in deploy/.env auf die echte https://-Domain aktualisieren, dann docker compose -f deploy/docker-compose.yml up -d, um beide Änderungen zu übernehmen.

gramps-web neben Gramps Connect ausführen

Standardmäßig nicht eingebunden (kein grampsweb-Dienst in docker-compose.yml), aber gut zu wissen: da das Backend eine schlichte, unveränderte gramps-web-api-Instanz ist, kann gramps-project/gramps-web (das andere offizielle Gramps-Frontend) auch gegen dasselbe Backend laufen — dieselben Daten, dieselben Stammbäume/Benutzer, nur eine andere Oberfläche auf einem anderen Port.

gramps-web veröffentlicht ghcr.io/gramps-project/grampsjs:latest genau dafür: nginx, das nur den eigenen statischen Build ausliefert, kein eingebackenes Backend. Dessen nginx-Konfiguration leitet /api per Reverse-Proxy an eine Umgebungsvariable API_HOST beim Container-Start weiter, aus Sicht des Browsers ist es also same-origin — kein GRAMPSWEB_CORS_ORIGINS für diesen Weg speziell nötig (diese Einstellung ist für eine ganz woanders gehostete gramps-web-Instanz, die stattdessen cross-origin hereinruft, statt über diesen Proxy).

Um es hinzuzufügen, ein docker-compose.yml-Dienst in dieser Art:

grampsweb:
  image: ghcr.io/gramps-project/grampsjs:latest
  environment:
    API_HOST: http://app:5000
    # Dockers eingebetteter DNS-Resolver -- die resolver-Direktive von
    # default.conf.template erfordert, dass dies explizit gesetzt wird.
    NAME_SERVER: 127.0.0.11
  depends_on:
    - app
  restart: unless-stopped

auf einem eigenen Port veröffentlicht (entweder direkt, z. B. ports: ["8081:80"], oder vor Caddy mit einem zweiten Block :8443 { reverse_proxy grampsweb:80 } in deploy/Caddyfile für TLS passend zum app-Dienst).

Anmerkungen

  • Daten bleiben in benannten Docker-Volumes erhalten (app-db, app-media, app-indexdir, app-users, app-secret, app-cache, app-tmp, postgres-data, caddy-data, caddy-config). docker compose down (ohne -v) behält sie; docker compose down -v löscht alles (einschließlich der erzeugten CA — das ist beim nächsten Start eine neue Browser-Vertrauenswarnung, nicht nur Datenverlust).
  • redis (Celery-Broker/Ergebnis-Backend) ist für Hintergrundaufgaben erforderlich (Suchindexierung, große Import-/Export-Jobs) — nicht optional. Ebenso der worker-Dienst: Import, Medien-Archiv-Upload und Suchindex-Neuaufbau werden alle über Celery verteilt, sobald GRAMPSWEB_CELERY_CONFIG__* gesetzt ist, und bleiben für immer als unerfüllte Aufgabe liegen ohne etwas, das die Warteschlange abarbeitet. app-cache (/app/cache) muss aus demselben Grund ein zwischen app und worker geteiltes Volume sein: der Request-Handler des app-Containers schreibt die hochgeladene Datei dorthin, dann liest sie der Celery-Task des worker-Containers zurück.
  • Der worker-Dienst läuft mit --pool=solo (kein Forking), aus Vorsicht davor, dass Celerys Standard-Prefork-Pool einen Prozess forkt, der bereits echten PyGObject-/GTK-Zustand berührt hat (Gramps' Import von gi.repository.GLib). Eine einzelne Worker-Instanz braucht die Parallelität nicht, die --pool=solo aufgibt, aber das ist einen erneuten Blick wert, sollte der Worker-Durchsatz je zum Flaschenhals werden.
  • Der browserseitige lokale Cache von app/ (sql.js, OPFS) ist nach einem festen Dateinamen pro Ansicht geschlüsselt, nicht nach Backend-URL oder Stammbaum-ID — siehe Architektur für die Falle, die das verursacht, und wie man sie löst.
  • .github/workflows/build-docker.yml (manuell ausgelöst — gh workflow run build-docker.yml, oder über den Actions-Tab) baut deploy/Dockerfile und veröffentlicht ghcr.io/<owner>/gramps-connect:latest. docker compose -f deploy/docker-compose.yml --env-file deploy/.env pull holt dieses stattdessen statt lokal zu bauen; up -d --build bleibt für lokales Iterieren am Dockerfile selbst bestehen.

Clone this wiki locally