Deploying.fr - dsblank/gramps-connect GitHub Wiki

🌐 English · Deutsch · Español · Nederlands · 简体中文

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é.

Exemple : hébergement sur un droplet DigitalOcean

Un pas-à-pas depuis un compte vide jusqu'à un site en ligne sur un petit serveur cloud (DigitalOcean les appelle des « droplets »). N'importe quel fournisseur de VPS fonctionne de la même façon dès que vous avez un serveur Linux avec Docker.

1. Créer le droplet

Dans la console DigitalOcean, Create → Droplets :

  • Image : Marketplace → Docker on Ubuntu (Docker préinstallé).
  • Size : Basic, 1 vCPU / 2 Go de RAM (s-1vcpu-2gb) est le plus petit qui fonctionne bien. L'image du backend est volumineuse (~4 Go, voir plus haut), donc ne descendez pas plus bas.
  • Region : la plus proche de vos utilisateurs.
  • Authentication : une clé SSH (plus sûre qu'un mot de passe).

Ensuite, Networking → Firewalls → Create Firewall : n'autorisez en entrée que les ports 22, 80 et 443, et appliquez-le au droplet.

2. Ajouter du swap

2 Go de RAM, c'est juste pendant les imports et le démarrage, donc ajoutez un fichier de swap de 2 Go (connectez-vous en SSH en tant que root) :

fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

3. Faire pointer un domaine vers lui

Facultatif mais recommandé, puisque c'est ce qui vous permet d'obtenir un vrai certificat HTTPS. Un nom gratuit DuckDNS convient, tout comme un enregistrement A sur un domaine qui vous appartient. Dans les deux cas, faites-le pointer vers l'IP publique du droplet.

4. Installer et démarrer Gramps Connect

git clone https://github.com/dsblank/gramps-connect /opt/gramps-connect
cd /opt/gramps-connect/deploy
cp .env.example .env
# modifier .env : GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD,
# PUBLIC_URL=https://votre.domaine, et les réglages e-mail (voir ci-dessous)
docker compose pull
docker compose up -d

Cela télécharge l'image publique précompilée ghcr.io/dsblank/gramps-connect:latest, donc rien n'est compilé sur le droplet lui-même.

5. Activer HTTPS

Modifiez deploy/Caddyfile comme décrit dans TLS : remplacez le bloc :443 { tls internal ... } par votre.domaine { reverse_proxy app:5000 } et supprimez le bloc :80, puis relancez docker compose up -d. Caddy demande et renouvelle tout seul un certificat Let's Encrypt.

6. Créer le premier arbre

Suivez Configuration initiale et Importer des données ci-dessous, en utilisant https://votre.domaine à la place de https://localhost.

E-mail (réinitialisation de mot de passe, invitations)

  • Les nouveaux comptes DigitalOcean bloquent le SMTP sortant. Utilisez un relais sur le port 587 (par ex. Brevo, SendGrid, Mailgun) et, si les e-mails ne partent toujours pas, ouvrez un ticket au support DigitalOcean (catégorie Networking) pour faire lever le blocage.
  • GRAMPSWEB_EMAIL_USE_STARTTLS doit valoir true en minuscules. True est lu silencieusement comme une chaîne et la connexion échoue.
  • L'identifiant SMTP de Brevo est une adresse générée (comme [email protected]), pas l'adresse e-mail avec laquelle vous vous êtes inscrit.

Mettre à jour plus tard

Voir Mettre à jour un serveur hébergé ci-dessous.

Mettre à jour un serveur hébergé

Ces étapes s'appliquent à tout hôte qui exécute la pile deploy/, droplet ou non. Les commandes supposent que le dépôt est cloné dans /opt/gramps-connect et que vous êtes dans son répertoire deploy/.

1. Sauvegarder

cd /opt/gramps-connect/deploy
# Données de l'arbre (Postgres)
docker compose exec -T postgres sh -c 'pg_dumpall -U "$POSTGRES_USER"' > backup-$(date +%F).sql
# Comptes utilisateurs (sqlite) et fichiers médias
docker compose cp app:/app/users  ./backup-users-$(date +%F)
docker compose cp app:/app/media  ./backup-media-$(date +%F)

Les mises à jour peuvent exécuter au démarrage des migrations de base de données irréversibles, donc une sauvegarde est le seul moyen d'en annuler complètement une.

2. Vérifier que la nouvelle image est construite

Le serveur télécharge ghcr.io/dsblank/gramps-connect:latest, qui ne change que lorsque le workflow build-docker.yml s'exécute. Juste après l'étiquetage d'une version, vérifiez que la construction Docker est terminée (onglet Actions du dépôt) avant de mettre à jour, sinon vous téléchargerez la version précédente.

3. Mettre à jour

cd /opt/gramps-connect && git pull       # configuration deploy/ mise à jour
cd deploy
docker compose pull app                  # la nouvelle image
docker compose up -d                     # recrée ce qui a changé

Seuls app et worker sont recréés. postgres, redis et caddy continuent de tourner, et toutes les données restent dans des volumes Docker. N'ajoutez jamais -v à docker compose down sauf si vous voulez tout supprimer (voir Remarques).

Si git pull refuse à cause de modifications locales : un hôte avec un vrai domaine a un deploy/Caddyfile modifié (voir TLS) qui n'est jamais commité, donc une mise à jour qui change la copie du dépôt entre en conflit avec lui. Conservez vos modifications avec :

git stash && git pull && git stash pop    # puis résoudre tout conflit dans deploy/Caddyfile

deploy/.env est dans le gitignore, il n'est donc jamais affecté.

4. Vérifier les nouveaux réglages

diff .env.example .env

Copiez tout réglage nouvellement ajouté de .env.example vers .env, puis relancez docker compose up -d si vous avez changé quelque chose.

5. Vérifier

docker compose ps                  # app et worker récemment créés, "Up"
docker compose logs --tail 50 app  # lignes gunicorn "Booting worker", pas de tracebacks

Chargez ensuite le site et connectez-vous. Une ligne Gtk-CRITICAL ... gtk_icon_theme_get_for_screen par worker gunicorn est normale et sans danger : le serveur a GTK installé mais pas d'affichage.

Revenir en arrière

Chaque image est aussi étiquetée avec le commit à partir duquel elle a été construite, ghcr.io/dsblank/gramps-connect:<commit-sha> (voir la page Packages du dépôt pour la liste). Pour revenir en arrière, remplacez la ligne image: sous x-app-image, en haut de deploy/docker-compose.yml, par cette étiquette, puis :

docker compose up -d

Si la version plus récente a migré la base de données, restaurez aussi la sauvegarde de l'étape 1.

Utiliser votre propre fork

Un fork publie sa propre image. Lancez build-docker.yml dans votre fork (qui pousse ghcr.io/<votre-utilisateur>/gramps-connect) et faites pointer la ligne image: ci-dessus vers elle, ou construisez sur le serveur avec docker compose up -d --build. Construire sur le serveur demande plus de disque et de mémoire que le droplet de 2 Go ci-dessus, puisque l'image de base seule fait environ 4 Go.

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.
⚠️ **GitHub.com Fallback** ⚠️