Skip to content

Deploying.fr

Doug Blank edited this page Sep 22, 2026 · 1 revision

🌐 English · Deutsch

Déploiement

deploy/ est la véritable forme multi-utilisateur de Gramps Connect : un frontend app/ conteneurisé + backend gramps-web-api, adossé à un véritable Postgres (via l'extension SharedPostgreSQL), devant lequel se trouve Caddy pour le TLS, destiné à être réellement hébergé quelque part — de vrais secrets, un vrai domaine/certificat, et plusieurs utilisateurs ayant chacun leur propre identifiant. C'est aussi le seul moyen de voir la collaboration en direct en action ; la version autonome pour ordinateur est conçue pour un seul utilisateur, il n'y a donc personne d'autre dont regarder apparaître les modifications.

Le backend est l'image officielle et non modifiée dmstraub/gramps-webapi — la même que la propre CI de gramps-project/gramps-web-api publie à chaque version, et sur laquelle gramps-project/gramps-web lui-même s'appuie — pas un build depuis les sources que ce dépôt maintiendrait. Cela garde ce déploiement comme une instance gramps-web-api standard et ordinaire, à laquelle tout client compatible peut parler, pas seulement le propre frontend de Gramps Connect. Les extensions SharedPostgreSQL/PostgreSQL/FilterRules/JSON, le support multi-arbres, et les traductions compilées viennent tous déjà de cette image ; la seule chose que deploy/Dockerfile ajoute est le frontend de app/, superposé par-dessus comme des fichiers statiques. Le compromis : cette image en amont est construite sur gramps-web-base (~4,3 Go — torch, sentence-transformers, opencv, 45 paquets de langue tesseract) avec des extras d'IA installés sans condition, puisqu'il n'existe pas de variante allégée officielle à récupérer à la place.

Frontend et backend partagent un seul conteneur/origine par défaut (gramps_webapi sert la SPA construite via STATIC_PATH et traite /api/* dans le même processus), Gramps Connect lui-même n'a donc besoin d'aucune configuration CORS — cela n'est pertinent que pour un frontend différent, hébergé séparément, qui appelle depuis l'extérieur (dans ce cas, définir GRAMPSWEB_CORS_ORIGINS dans deploy/.env).

Services : caddy (terminaison TLS, le seul point d'entrée publié), app (gunicorn, sert le frontend + /api/*), worker (Celery, exécute les tâches d'import/média/réindexation de recherche), postgres (données d'arbre via SharedPostgreSQL), redis (broker Celery).

L'exécuter localement

cp deploy/.env.example deploy/.env    # puis le modifier -- voir les commentaires du fichier
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build

Puis visiter https://localhost (Caddy s'émet automatiquement un certificat local auto-signé — accepter l'avertissement du navigateur).

L'exécuter sur un vrai hôte

Construire une fois sur les runners de GitHub plutôt que sur l'hôte (gh workflow run build-docker.yml, ou le déclencher depuis l'onglet Actions), ce qui pousse ghcr.io/<owner>/gramps-connect:latest, puis sur l'hôte :

cp deploy/.env.example deploy/.env    # cette fois avec de vrais secrets/domaine
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

Faire pointer un domaine vers l'hôte et modifier deploy/Caddyfile (voir TLS ci-dessous) pour obtenir un vrai certificat au lieu de l'auto-signé.

Commandes Docker

Toutes les commandes ci-dessous supposent d'être à la racine du dépôt. docker compose est la forme plugin ; si votre installation Docker n'a que le binaire autonome, utiliser docker-compose à la place (mêmes options dans les deux cas).

# État des cinq services
docker compose -f deploy/docker-compose.yml ps

# Logs (ajouter -f pour suivre, --tail=100 pour limiter)
docker compose -f deploy/docker-compose.yml logs app
docker compose -f deploy/docker-compose.yml logs worker

# Redémarrer un service (par ex. après un changement dans deploy/.env)
docker compose -f deploy/docker-compose.yml up -d
# ^ recrée tout service dont la configuration (image, environnement, volumes)
#   a changé ; ajouter d'abord --build si deploy/Dockerfile ou le code
#   source du backend dans app/ a été modifié.

# Tout arrêter, garder les données (les volumes survivent)
docker compose -f deploy/docker-compose.yml down

# Arrêter et supprimer toutes les données (attention -- supprime Postgres, médias, utilisateurs, etc.)
docker compose -f deploy/docker-compose.yml down -v

# Ouvrir un shell dans un conteneur en cours d'exécution
docker compose -f deploy/docker-compose.yml exec app sh

Identifiants de connexion : ce déploiement vs. la version autonome

Ce déploiement Docker n'a pas de mot de passe par défaut — vous définissez vous-même GRAMPSWEB_ADMIN_USER/GRAMPSWEB_ADMIN_PASSWORD dans deploy/.env avant le premier démarrage, et c'est ce que le point d'entrée sème. Rien ne génère ou n'affiche de mot de passe pour vous.

C'est différent de gramps-connect-desktop (le build de démonstration PyInstaller mono-utilisateur, sans rapport avec ce déploiement Docker) : il sème toujours un compte fixe admin/admin, ce qui est acceptable là puisque c'est une démo locale jetable, pas quelque chose exposé sur un vrai serveur.

Configuration initiale : l'admin semé

Au premier démarrage (la première fois que les volumes app-users/app-db sont vides), le point d'entrée :

  • génère et conserve durablement une clé secrète Flask si GRAMPSWEB_SECRET_KEY a été laissé vide
  • exécute les migrations de la base de données utilisateurs
  • sème un utilisateur administrateur du site (sans arbre, rôle 5) à partir de GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD (les deux requis dans deploy/.env — pas de repli admin/admin)

Un administrateur du site sans arbre peut créer/lister/supprimer des arbres via l'API, mais — puisque le frontend de app/ n'a pas d'interface de sélection d'arbre et s'attend toujours à ce que le JWT de l'utilisateur connecté porte déjà un arbre — ne peut pas parcourir les données d'un arbre tant qu'il n'y est pas affecté. Il n'y a pas de commande CLI pour créer un arbre en mode multi-arbres, donc créer le premier arbre via l'API (une seule fois) :

TOKEN=$(curl -sk -X POST https://localhost/api/token/ \
  -H 'Content-Type: application/json' \
  -d '{"username":"<admin>","password":"<mot de passe admin>"}' \
  | 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":"Mon arbre familial"}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")

echo "$TREE_ID"

Ensuite, soit affecter l'administrateur du site lui-même à cet arbre, soit (recommandé — garde l'administration du site et la propriété de l'arbre séparées) créer plutôt un utilisateur dédié, rattaché à l'arbre :

# Option A : affecter l'administrateur du site existant à l'arbre.
curl -sk -X PUT "https://localhost/api/users/<admin>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"tree\":\"$TREE_ID\"}"

# Option B : créer à la place un utilisateur ordinaire séparé, rattaché
# à l'arbre. email et full_name sont requis par le schéma même si non
# utilisés.
curl -sk -X POST "https://localhost/api/users/<username>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"email\":\"<email>\",\"full_name\":\"<nom complet>\",\"password\":\"<mot de passe>\",\"role\":4,\"tree\":\"$TREE_ID\"}"

role est un entier (gramps_webapi.auth.const) :

Rôle Valeur Remarques
ADMIN 5 Administrateur du site ; seul rôle pouvant être sans arbre
OWNER 4 Contrôle complet de son arbre
EDITOR 3 Peut modifier les données de l'arbre
CONTRIBUTOR 2 Peut ajouter des données
MEMBER 1 Accès en lecture
GUEST 0 Accès en lecture minimal

Ensuite, se connecter sur https://localhost/ avec l'utilisateur qui vient d'être créé — les jetons existants émis avant une affectation à un arbre ne le porteront pas, donc se déconnecter/reconnecter si une session déjà ouverte a été réutilisée.

Importer des données (par ex. example.gramps, avec médias)

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

# 1. Importer le fichier XML .gramps. IMPORTANT : cet endpoint lit le
#    corps brut de la requête et l'écrit directement sur le disque -- il
#    ne parse PAS multipart/form-data, donc utiliser --data-binary, pas
#    -F/--form de curl (un envoi multipart corrompt silencieusement le
#    fichier avec des octets de délimiteur/en-tête, et l'importateur de
#    Gramps échoue alors avec un générique et peu utile "Import failed"
#    sans plus de détail).
curl -sk -X POST https://localhost/api/importers/gramps/file \
  -H "Authorization: Bearer $TOKEN" --data-binary @example.gramps

# La réponse est un identifiant de tâche (l'import s'exécute sur le
# service worker via Celery) -- l'interroger jusqu'à "state":"SUCCESS"
# (ou "FAILURE") :
curl -sk https://localhost/api/tasks/<task_id> -H "Authorization: Bearer $TOKEN"

# 2. Médias : compresser en zip les fichiers médias référencés (tout
#    sous-ensemble/sur-ensemble convient -- les fichiers sont associés
#    par somme de contrôle, ceux non référencés sont ignorés) et
#    envoyer l'archive, à nouveau comme corps brut :
curl -sk -X POST https://localhost/api/media/archive/upload/zip \
  -H "Authorization: Bearer $TOKEN" --data-binary @media.zip

# 3. Vérifier : nombre d'objets pour l'arbre de l'utilisateur connecté.
curl -sk https://localhost/api/metadata/ -H "Authorization: Bearer $TOKEN" \
  | python3 -m json.tool

Piège connu : avec GRAMPSWEB_MEDIA_PREFIX_TREE=True (défini par défaut dans ce fichier compose), les médias sont stockés sous MEDIA_BASE_DIR/<tree_id>/, mais rien ne crée automatiquement ce sous-répertoire par arbre — ni POST /api/trees/, ni la tâche d'envoi d'archive média. Envoyer des médias pour un arbre échoue avec Directory /app/media/<tree_id> does not exist tant qu'il n'a pas été créé une fois :

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

Envois de médias volumineux qui expirent : les endpoints d'envoi aussi bien du fichier unique (POST /api/media/) que de l'archive ZIP (POST /api/media/archive/upload/zip) lisent le corps de la requête de façon synchrone à l'intérieur du worker gunicorn de app avant de transmettre à Celery, donc un fichier volumineux ou une connexion lente peut dépasser le délai d'expiration par requête de gunicorn — le navigateur voit alors une simple Internal Server Error sans corps JSON (venant de gunicorn/Caddy, pas de Gramps). deploy/docker-compose.yml fixe GUNICORN_TIMEOUT à 600s par défaut ; l'augmenter encore via GUNICORN_TIMEOUT dans deploy/.env (voir les commentaires de ce fichier) si les envois échouent toujours.

TLS

caddy est le seul service avec des ports publiés (80/443) et termine le TLS devant app. Sans domaine configuré, il génère et conserve sa propre AC locale et en émet automatiquement un certificat auto-signé (tls internal de deploy/Caddyfile) — les navigateurs afficheront un avertissement de confiance la première fois ; l'accepter pour continuer (ou ajouter l'AC générée au magasin de confiance du système/navigateur si vous voulez que l'avertissement disparaisse sans vrai domaine). Le port 80 redirige vers le 443.

Une fois qu'un vrai domaine pointe vers ce serveur, modifier deploy/Caddyfile : remplacer le bloc :443 { tls internal ... } par example.com { reverse_proxy app:5000 } et supprimer le bloc :80 — Caddy gère l'émission ACME, le renouvellement et la redirection du port 80 automatiquement pour un vrai domaine, aucune directive tls nécessaire. Mettre aussi à jour PUBLIC_URL dans deploy/.env avec le vrai domaine https://, puis docker compose -f deploy/docker-compose.yml up -d pour prendre en compte les deux changements.

Exécuter gramps-web à côté de Gramps Connect

Non branché par défaut (pas de service grampsweb dans docker-compose.yml), mais utile à savoir : puisque le backend est une instance gramps-web-api ordinaire et non modifiée, gramps-project/gramps-web (l'autre frontend officiel de Gramps) peut aussi tourner contre ce même backend — mêmes données, mêmes arbres/utilisateurs, juste une interface différente sur un port différent.

gramps-web publie ghcr.io/gramps-project/grampsjs:latest exactement pour cela : nginx servant uniquement son propre build statique, aucun backend intégré. Sa configuration nginx redirige /api en reverse-proxy vers une variable d'environnement API_HOST au démarrage du conteneur, donc du point de vue du navigateur c'est same-origin — pas besoin de GRAMPSWEB_CORS_ORIGINS pour ce cas précis (ce réglage est pour une instance gramps-web hébergée ailleurs complètement, qui appelle en cross-origin plutôt qu'à travers ce proxy).

Pour l'ajouter, un service docker-compose.yml dans ce genre :

grampsweb:
  image: ghcr.io/gramps-project/grampsjs:latest
  environment:
    API_HOST: http://app:5000
    # Le résolveur DNS intégré de Docker -- la directive resolver de
    # default.conf.template exige que cela soit défini explicitement.
    NAME_SERVER: 127.0.0.11
  depends_on:
    - app
  restart: unless-stopped

publié sur son propre port (soit directement, par ex. ports: ["8081:80"], soit devant Caddy avec un second bloc dans le style :8443 { reverse_proxy grampsweb:80 } dans deploy/Caddyfile pour un TLS assorti au service app).

Remarques

  • Les données persistent dans des volumes Docker nommés (app-db, app-media, app-indexdir, app-users, app-secret, app-cache, app-tmp, postgres-data, caddy-data, caddy-config). docker compose down (sans -v) les conserve ; docker compose down -v supprime tout (y compris l'AC générée — c'est un nouvel avertissement de confiance du navigateur au prochain démarrage, pas seulement une perte de données).
  • redis (broker/backend de résultats Celery) est requis pour les tâches en arrière-plan (indexation de recherche, gros travaux d'import/export) — pas optionnel. Il en va de même pour le service worker : import, envoi d'archive média et réindexation de recherche passent tous par Celery une fois GRAMPSWEB_CELERY_CONFIG__* défini, et restent pour toujours comme une tâche non satisfaite sans quelque chose qui consomme la file d'attente. app-cache (/app/cache) doit être un volume partagé entre app et worker pour la même raison : le gestionnaire de requêtes du conteneur app y écrit le fichier envoyé, puis la tâche Celery du conteneur worker le relit.
  • Le service worker tourne avec --pool=solo (pas de forking), par précaution face au pool prefork par défaut de Celery qui forkerait un processus ayant déjà touché un véritable état PyGObject/GTK (l'import gi.repository.GLib de Gramps). Une seule instance de worker n'a pas besoin de la concurrence à laquelle --pool=solo renonce, mais cela vaut la peine d'être reconsidéré si le débit du worker devient un jour un goulot d'étranglement.
  • Le cache local côté navigateur de app/ (sql.js, OPFS) est indexé par un nom de fichier fixe par vue, pas par URL de backend ou ID d'arbre — voir Architecture pour le piège que cela cause et comment le résoudre.
  • .github/workflows/build-docker.yml (déclenchement manuel — gh workflow run build-docker.yml, ou l'onglet Actions) construit deploy/Dockerfile et pousse ghcr.io/<owner>/gramps-connect:latest. docker compose -f deploy/docker-compose.yml --env-file deploy/.env pull récupère cela au lieu de construire localement ; up -d --build reste utile pour l'itération locale sur le Dockerfile lui-même.

Clone this wiki locally