-
Notifications
You must be signed in to change notification settings - Fork 1
Deploying.de
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).
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 --buildDann https://localhost aufrufen (Caddy stellt sich automatisch selbst
ein lokales, selbstsigniertes Zertifikat aus — die Browser-Warnung
akzeptieren).
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 -dEine Domain auf den Host zeigen lassen und deploy/Caddyfile bearbeiten
(siehe TLS unten), um statt des selbstsignierten ein echtes
Zertifikat zu bekommen.
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 shDiese 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.
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_KEYleer 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 indeploy/.enverforderlich — keinadmin/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.
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.toolBekannte 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).
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.
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-stoppedauf 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).
- 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 -vlö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 derworker-Dienst: Import, Medien-Archiv-Upload und Suchindex-Neuaufbau werden alle über Celery verteilt, sobaldGRAMPSWEB_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 zwischenappundworkergeteiltes 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 vongi.repository.GLib). Eine einzelne Worker-Instanz braucht die Parallelität nicht, die--pool=soloaufgibt, 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) bautdeploy/Dockerfileund veröffentlichtghcr.io/<owner>/gramps-connect:latest.docker compose -f deploy/docker-compose.yml --env-file deploy/.env pullholt dieses stattdessen statt lokal zu bauen;up -d --buildbleibt für lokales Iterieren am Dockerfile selbst bestehen.
Gramps Connect is part of the family of Gramps-based software.
Using the app
- Overview
- Installing
- Deploying
- Messaging
- GOQL (advanced search)
- Gramplets & Add-on Store
- Data Model & Editing
- FAQ
Building & contributing