Deploying.es - dsblank/gramps-connect GitHub Wiki

🌐 English · Deutsch · Français · Nederlands · 简体中文

Despliegue

deploy/ es la forma multiusuario real de Gramps Connect: un frontend app/ + backend gramps-web-api en contenedores, con un Postgres real (mediante el complemento SharedPostgreSQL), detrás de Caddy para TLS, pensado para alojarse de verdad en algún sitio: secretos reales, un dominio/certificado real y varios usuarios, cada uno con su propio inicio de sesión. También es la única forma de ver en acción la colaboración en vivo; la versión de escritorio independiente es de un solo usuario por diseño, así que no hay ediciones de nadie más que ver aparecer.

El backend es la imagen oficial y sin modificar dmstraub/gramps-webapi (la misma que publica la CI de gramps-project/gramps-web-api en cada versión, y sobre la que se construye el propio gramps-project/gramps-web), no una compilación desde el código fuente mantenida por este repositorio. Así este despliegue sigue siendo una instancia estándar y normal de gramps-web-api con la que puede hablar cualquier cliente compatible, no solo el frontend de Gramps Connect. Los complementos SharedPostgreSQL/PostgreSQL/FilterRules/JSON, el soporte de varios árboles y las traducciones compiladas ya vienen en esa imagen; lo único que añade deploy/Dockerfile es el frontend de app/, colocado encima como archivos estáticos. La contrapartida: esa imagen original se construye sobre gramps-web-base (~4,3 GB: torch, sentence-transformers, opencv, 45 paquetes de idioma de tesseract) con los extras de IA instalados incondicionalmente, ya que no hay ninguna variante ligera oficial que usar en su lugar.

El frontend y el backend comparten por defecto un contenedor/origen (gramps_webapi sirve la SPA compilada mediante STATIC_PATH y atiende /api/* en el mismo proceso), así que Gramps Connect no necesita ninguna configuración de CORS; eso solo importa para un frontend distinto, alojado aparte, que haga llamadas desde fuera (defina GRAMPSWEB_CORS_ORIGINS en deploy/.env en ese caso).

Servicios: caddy (terminación TLS, el único punto de entrada publicado), app (gunicorn, sirve el frontend + /api/*), worker (Celery, ejecuta las tareas de importación/multimedia/reindexado de búsqueda), postgres (datos del árbol mediante SharedPostgreSQL), redis (intermediario de Celery).

Ejecutarlo localmente

cp deploy/.env.example deploy/.env    # luego edítelo -- véanse los comentarios del archivo
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build

Después visite https://localhost (Caddy se emite automáticamente un certificado local autofirmado; acepte la advertencia del navegador).

Ejecutarlo en un servidor real

Compile una vez en los runners de GitHub en lugar de en el servidor (gh workflow run build-docker.yml, o lánzelo desde la pestaña Actions), lo que sube ghcr.io/<owner>/gramps-connect:latest; luego, en el servidor:

cp deploy/.env.example deploy/.env    # esta vez con secretos/dominio reales
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

Apunte un dominio al servidor y edite deploy/Caddyfile (véase TLS más abajo) para obtener un certificado real en lugar del autofirmado.

Ejemplo: alojamiento en un droplet de DigitalOcean

Un recorrido desde una cuenta vacía hasta un sitio en funcionamiento en un pequeño servidor en la nube (DigitalOcean los llama «droplets»). Cualquier proveedor de VPS funciona igual una vez que tiene un servidor Linux con Docker.

1. Crear el droplet

En la consola de DigitalOcean, Create → Droplets:

  • Image: Marketplace → Docker on Ubuntu (con Docker preinstalado).
  • Size: Basic, 1 vCPU / 2 GB de RAM (s-1vcpu-2gb) es el más pequeño que funciona bien. La imagen del backend es grande (~4 GB, véase más arriba), así que no elija uno más pequeño.
  • Region: la más cercana a sus usuarios.
  • Authentication: una clave SSH (más segura que una contraseña).

Después, Networking → Firewalls → Create Firewall: permita solo el tráfico entrante por los puertos 22, 80 y 443, y aplíquelo al droplet.

2. Añadir swap

2 GB de RAM se quedan cortos durante las importaciones y el arranque, así que añada un archivo de swap de 2 GB (conéctese por SSH como root):

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

3. Apuntar un dominio hacia él

Es opcional pero recomendable, ya que es lo que le permite obtener un certificado HTTPS real. Sirve un nombre gratuito de DuckDNS, igual que un registro A en un dominio propio. En cualquier caso, apúntelo a la IP pública del droplet.

4. Instalar e iniciar Gramps Connect

git clone https://github.com/dsblank/gramps-connect /opt/gramps-connect
cd /opt/gramps-connect/deploy
cp .env.example .env
# edite .env: GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD,
# PUBLIC_URL=https://su.dominio y la configuración de correo (véase abajo)
docker compose pull
docker compose up -d

Esto descarga la imagen pública ya compilada ghcr.io/dsblank/gramps-connect:latest, así que no se compila nada en el propio droplet.

5. Activar HTTPS

Edite deploy/Caddyfile como se describe en TLS: sustituya el bloque :443 { tls internal ... } por su.dominio { reverse_proxy app:5000 } y elimine el bloque :80; después ejecute de nuevo docker compose up -d. Caddy solicita y renueva por sí mismo un certificado de Let's Encrypt.

6. Crear el primer árbol

Siga Configuración inicial e Importar datos más abajo, usando https://su.dominio en lugar de https://localhost.

Correo electrónico (restablecer contraseñas, invitaciones)

  • Las cuentas nuevas de DigitalOcean bloquean el SMTP saliente. Use un relé en el puerto 587 (p. ej. Brevo, SendGrid, Mailgun) y, si el correo sigue sin salir, abra una incidencia con el soporte de DigitalOcean (categoría Networking) para que levanten el bloqueo.
  • GRAMPSWEB_EMAIL_USE_STARTTLS debe ser true en minúsculas. True se lee silenciosamente como una cadena y la conexión falla.
  • El usuario SMTP de Brevo es una dirección generada (como [email protected]), no la dirección de correo con la que se registró.

Actualizar más adelante

Véase Actualizar un servidor alojado más abajo.

Actualizar un servidor alojado

Estos pasos se aplican a cualquier servidor que ejecute la pila de deploy/, sea un droplet o no. Los comandos suponen que el repositorio está clonado en /opt/gramps-connect y que usted está en su directorio deploy/.

1. Hacer una copia de seguridad

cd /opt/gramps-connect/deploy
# Datos del árbol (Postgres)
docker compose exec -T postgres sh -c 'pg_dumpall -U "$POSTGRES_USER"' > backup-$(date +%F).sql
# Cuentas de usuario (sqlite) y archivos multimedia
docker compose cp app:/app/users  ./backup-users-$(date +%F)
docker compose cp app:/app/media  ./backup-media-$(date +%F)

Las actualizaciones pueden ejecutar al arrancar migraciones de la base de datos que no se pueden revertir, así que una copia de seguridad es la única forma de deshacer una por completo.

2. Comprobar que la nueva imagen está compilada

El servidor descarga ghcr.io/dsblank/gramps-connect:latest, que solo cambia cuando se ejecuta el flujo de trabajo build-docker.yml. Justo después de etiquetar una versión, compruebe que la compilación de Docker ha terminado (pestaña Actions del repositorio) antes de actualizar, o descargará la versión anterior.

3. Actualizar

cd /opt/gramps-connect && git pull       # configuración de deploy/ actualizada
cd deploy
docker compose pull app                  # la nueva imagen
docker compose up -d                     # recrea lo que haya cambiado

Solo se recrean app y worker. postgres, redis y caddy siguen funcionando, y todos los datos permanecen en volúmenes de Docker. No añada nunca -v a docker compose down a menos que quiera borrarlo todo (véase Notas).

Si git pull se niega por cambios locales: un servidor con dominio real tiene un deploy/Caddyfile editado (véase TLS) que nunca se confirma, así que una actualización que cambie la copia del repositorio entra en conflicto con él. Conserve sus cambios con:

git stash && git pull && git stash pop    # luego resuelva cualquier conflicto en deploy/Caddyfile

deploy/.env está en gitignore, así que nunca se ve afectado.

4. Buscar ajustes nuevos

diff .env.example .env

Copie cualquier ajuste nuevo de .env.example a .env y, si cambió algo, ejecute de nuevo docker compose up -d.

5. Verificar

docker compose ps                  # app y worker creados recientemente, "Up"
docker compose logs --tail 50 app  # líneas "Booting worker" de gunicorn, sin tracebacks

Después cargue el sitio e inicie sesión. Una línea Gtk-CRITICAL ... gtk_icon_theme_get_for_screen por cada worker de gunicorn es normal e inofensiva: el servidor tiene GTK instalado pero no tiene pantalla.

Volver atrás

Cada imagen se etiqueta también con el commit a partir del cual se compiló, ghcr.io/dsblank/gramps-connect:<commit-sha> (la lista está en la página Packages del repositorio). Para volver atrás, cambie la línea image: bajo x-app-image, al principio de deploy/docker-compose.yml, a esa etiqueta y después ejecute:

docker compose up -d

Si la versión más nueva migró la base de datos, restaure también la copia de seguridad del paso 1.

Ejecutar su propio fork

Un fork publica su propia imagen. Ejecute build-docker.yml en su fork (publica ghcr.io/<su-usuario>/gramps-connect) y apunte a ella la línea image: anterior, o compile en el servidor con docker compose up -d --build. Compilar en el servidor necesita más disco y memoria que el droplet de 2 GB de arriba, ya que solo la imagen base ocupa unos 4 GB.

Comandos de Docker

Todos los comandos siguientes suponen que está en la raíz del repositorio. docker compose es la forma de plugin; si su instalación de Docker solo tiene el binario independiente, use docker-compose (con las mismas opciones en ambos casos).

# Estado de los cinco servicios
docker compose -f deploy/docker-compose.yml ps

# Registros (añada -f para seguirlos, --tail=100 para limitarlos)
docker compose -f deploy/docker-compose.yml logs app
docker compose -f deploy/docker-compose.yml logs worker

# Reiniciar un servicio (p. ej. tras un cambio de entorno en deploy/.env)
docker compose -f deploy/docker-compose.yml up -d
# ^ recrea cualquier servicio cuya configuración (imagen, entorno, volúmenes) haya cambiado;
#   añada antes --build si editó deploy/Dockerfile o el código de app/backend.

# Detenerlo todo, conservando los datos (los volúmenes sobreviven)
docker compose -f deploy/docker-compose.yml down

# Detener y borrar todos los datos (cuidado -- elimina Postgres, multimedia, usuarios, etc.)
docker compose -f deploy/docker-compose.yml down -v

# Abrir una shell en un contenedor en ejecución
docker compose -f deploy/docker-compose.yml exec app sh

Credenciales de inicio de sesión: este despliegue frente a la versión independiente

Este despliegue con Docker no tiene contraseña por defecto: usted define GRAMPSWEB_ADMIN_USER/GRAMPSWEB_ADMIN_PASSWORD en deploy/.env antes del primer arranque, y eso es lo que crea el punto de entrada. Nada genera ni muestra una contraseña por usted.

Eso es distinto de gramps-connect-desktop (la versión de demostración de un solo usuario con PyInstaller, sin relación con este despliegue con Docker): siempre crea una cuenta fija admin/admin, lo cual allí está bien porque es una demostración local desechable, no algo expuesto en un servidor real.

Configuración inicial: el administrador creado

En el primer arranque (la primera vez que los volúmenes app-users/app-db están vacíos), el punto de entrada:

  • genera y guarda una clave secreta de Flask si GRAMPSWEB_SECRET_KEY se dejó en blanco
  • ejecuta las migraciones de la base de datos de usuarios
  • crea un usuario administrador del sitio (sin árbol, rol 5) a partir de GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD (ambos obligatorios en deploy/.env; no hay alternativa admin/admin)

Un administrador del sitio sin árbol puede crear/listar/eliminar árboles mediante la API, pero, como el frontend de app/ no tiene interfaz para elegir árbol y siempre espera que el JWT del usuario conectado ya lleve un árbol, no puede ver los datos de un árbol hasta que se le asigne uno. No hay ningún comando de CLI para crear árboles en modo de varios árboles, así que cree el primer árbol mediante la API (una sola vez):

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

echo "$TREE_ID"

Después, asigne el propio administrador del sitio a ese árbol o (recomendado: mantiene separadas la administración del sitio y la propiedad del árbol) cree un usuario dedicado, limitado al árbol:

# Opción A: asignar el administrador del sitio existente al árbol.
curl -sk -X PUT "https://localhost/api/users/<admin>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"tree\":\"$TREE_ID\"}"

# Opción B: crear en su lugar un usuario normal aparte, ligado al árbol.
# email y full_name son obligatorios en el esquema aunque no se usen.
curl -sk -X POST "https://localhost/api/users/<username>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"email\":\"<email>\",\"full_name\":\"<full name>\",\"password\":\"<password>\",\"role\":4,\"tree\":\"$TREE_ID\"}"

role es un entero (gramps_webapi.auth.const):

Rol Valor Notas
ADMIN 5 Administrador del sitio; único rol que puede no tener árbol
OWNER 4 Control total de su árbol
EDITOR 3 Puede editar los datos del árbol
CONTRIBUTOR 2 Puede añadir datos
MEMBER 1 Acceso de lectura
GUEST 0 Acceso de lectura mínimo

Luego inicie sesión en https://localhost/ con el usuario que acaba de crear; los tokens emitidos antes de asignar un árbol no lo llevan, así que cierre sesión y vuelva a iniciarla si reutilizó una sesión ya abierta.

Importar datos (p. ej. example.gramps, con multimedia)

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

# 1. Importar el archivo XML .gramps. IMPORTANTE: este endpoint lee el cuerpo
#    bruto de la petición y lo escribe directamente en disco -- NO analiza
#    multipart/form-data, así que use --data-binary, no -F/--form de curl (una
#    subida multipart corrompe el archivo sin avisar con bytes de
#    separadores/cabeceras, y el importador de Gramps falla entonces con un
#    genérico e inútil "Import failed" sin más detalle).
curl -sk -X POST https://localhost/api/importers/gramps/file \
  -H "Authorization: Bearer $TOKEN" --data-binary @example.gramps

# La respuesta es un identificador de tarea (la importación se ejecuta en el
# servicio `worker` mediante Celery) -- consúltelo hasta "state":"SUCCESS" (o "FAILURE"):
curl -sk https://localhost/api/tasks/<task_id> -H "Authorization: Bearer $TOKEN"

# 2. Multimedia: comprima en zip los archivos multimedia referenciados (sirve
#    cualquier subconjunto/superconjunto -- los archivos se emparejan por suma
#    de comprobación y los no referenciados se ignoran) y suba el archivo,
#    de nuevo como cuerpo bruto:
curl -sk -X POST https://localhost/api/media/archive/upload/zip \
  -H "Authorization: Bearer $TOKEN" --data-binary @media.zip

# 3. Verificar: recuentos de objetos del árbol del usuario conectado.
curl -sk https://localhost/api/metadata/ -H "Authorization: Bearer $TOKEN" \
  | python3 -m json.tool

Trampa conocida: con GRAMPSWEB_MEDIA_PREFIX_TREE=True (definido por defecto en este archivo compose), los objetos multimedia se guardan en MEDIA_BASE_DIR/<tree_id>/, pero nada crea automáticamente ese subdirectorio por árbol: ni POST /api/trees/ ni la tarea de subida del archivo multimedia. Subir multimedia para un árbol falla con Directory /app/media/<tree_id> does not exist hasta que se crea una vez:

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

Subidas grandes de multimedia que agotan el tiempo: tanto el endpoint de subida de un solo archivo (POST /api/media/) como el de archivo ZIP (POST /api/media/archive/upload/zip) leen el cuerpo de la petición de forma síncrona dentro del worker de gunicorn de app antes de pasarlo a Celery, así que un archivo grande o una conexión lenta pueden superar el tiempo de espera por petición de gunicorn; el navegador ve entonces un simple Internal Server Error sin cuerpo JSON (de gunicorn/Caddy, no de Gramps). deploy/docker-compose.yml fija GUNICORN_TIMEOUT en 600 s por defecto; auméntelo más mediante GUNICORN_TIMEOUT en deploy/.env (véanse los comentarios de ese archivo) si las subidas siguen fallando.

TLS

caddy es el único servicio con puertos publicados (80/443) y termina TLS delante de app. Sin ningún dominio configurado, genera y guarda su propia CA local y emite automáticamente a partir de ella un certificado autofirmado (tls internal en deploy/Caddyfile); los navegadores mostrarán una advertencia de confianza la primera vez; acéptela para continuar (o añada la CA generada al almacén de confianza de su sistema/navegador si quiere quitar la advertencia sin un dominio real). El puerto 80 redirige al 443.

En cuanto un dominio real apunte a este servidor, edite deploy/Caddyfile: sustituya el bloque :443 { tls internal ... } por example.com { reverse_proxy app:5000 } y elimine el bloque :80; Caddy gestiona automáticamente la emisión ACME, la renovación y la redirección del puerto 80 para un dominio real, sin necesidad de directiva tls. Actualice también PUBLIC_URL en deploy/.env con el dominio https:// real, y luego ejecute docker compose -f deploy/docker-compose.yml up -d para aplicar ambos cambios.

Ejecutar gramps-web junto a Gramps Connect

No viene configurado por defecto (no hay un servicio grampsweb en docker-compose.yml), pero conviene saberlo: como el backend es una instancia normal y sin modificar de gramps-web-api, gramps-project/gramps-web (el otro frontend oficial de Gramps) también puede ejecutarse contra este mismo backend: los mismos datos, los mismos árboles/usuarios, solo una interfaz distinta en otro puerto.

gramps-web publica ghcr.io/gramps-project/grampsjs:latest precisamente para esto: nginx sirviendo solo su compilación estática, sin backend incluido. Su configuración de nginx hace de proxy inverso de /api hacia una variable de entorno API_HOST al arrancar el contenedor, así que desde el punto de vista del navegador es del mismo origen: no hace falta GRAMPSWEB_CORS_ORIGINS específicamente para esta ruta (ese ajuste es para una instancia de gramps-web alojada en otro sitio completamente distinto, que llama desde otro origen en lugar de a través de este proxy).

Para añadirlo, un servicio de docker-compose.yml parecido a este:

grampsweb:
  image: ghcr.io/gramps-project/grampsjs:latest
  environment:
    API_HOST: http://app:5000
    # El resolvedor DNS integrado de Docker -- la directiva `resolver` de
    # default.conf.template exige que se defina explícitamente.
    NAME_SERVER: 127.0.0.11
  depends_on:
    - app
  restart: unless-stopped

publicado en su propio puerto (directamente, p. ej. ports: ["8081:80"], o detrás de Caddy con un segundo bloque del estilo :8443 { reverse_proxy grampsweb:80 } en deploy/Caddyfile para tener TLS igual que el servicio app).

Notas

  • Los datos persisten en volúmenes con nombre de Docker (app-db, app-media, app-indexdir, app-users, app-secret, app-cache, app-tmp, postgres-data, caddy-data, caddy-config). docker compose down (sin -v) los conserva; docker compose down -v lo borra todo (incluida la CA generada: eso supone una nueva advertencia de confianza del navegador en el siguiente arranque, no solo pérdida de datos).
  • redis (intermediario/almacén de resultados de Celery) es necesario para las tareas en segundo plano (indexado de búsqueda, trabajos grandes de importación/exportación); no es opcional. Tampoco lo es el servicio worker: la importación, la subida de archivos multimedia y el reindexado de búsqueda se despachan a través de Celery en cuanto se define GRAMPSWEB_CELERY_CONFIG__*, y se quedan para siempre como una tarea sin atender si nada consume la cola. app-cache (/app/cache) debe ser un volumen compartido entre app y worker por el mismo motivo: el gestor de peticiones del contenedor app escribe allí el archivo subido, y luego la tarea de Celery del contenedor worker lo lee.
  • El servicio worker se ejecuta con --pool=solo (sin fork), por precaución ante el pool prefork por defecto de Celery, que haría fork de un proceso que ya ha tocado estado real de PyGObject/GTK (la importación de gi.repository.GLib de Gramps). Una sola instancia de worker no necesita la concurrencia a la que renuncia --pool=solo, pero conviene revisarlo si el rendimiento del worker llega a ser un cuello de botella.
  • La caché local del navegador de app/ (sql.js, OPFS) se identifica por un nombre de archivo fijo por vista, no por la URL del backend ni por el ID del árbol; consulte Arquitectura para ver la trampa que esto provoca y cómo borrarla.
  • .github/workflows/build-docker.yml (activación manual: gh workflow run build-docker.yml, o la pestaña Actions) compila deploy/Dockerfile y sube ghcr.io/<owner>/gramps-connect:latest. docker compose -f deploy/docker-compose.yml --env-file deploy/.env pull descarga esa imagen en lugar de compilarla localmente; up -d --build sigue ahí para iterar localmente sobre el propio Dockerfile.
⚠️ **GitHub.com Fallback** ⚠️