Skip to content

Deploying.es

Doug Blank edited this page Oct 5, 2026 · 3 revisions

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

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.

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.

Clone this wiki locally