Deploying.es - dsblank/gramps-connect GitHub Wiki
🌐 English · Deutsch · Français · Nederlands · 简体中文
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).
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 --buildDespués visite https://localhost (Caddy se emite automáticamente un
certificado local autofirmado; acepte la advertencia del navegador).
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 -dApunte un dominio al servidor y edite deploy/Caddyfile (véase TLS
más abajo) para obtener un certificado real en lugar del autofirmado.
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.
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 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/fstabEs 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.
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 -dEsto descarga la imagen pública ya compilada
ghcr.io/dsblank/gramps-connect:latest, así que no se compila nada en el
propio droplet.
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.
Siga Configuración inicial e
Importar datos más abajo,
usando https://su.dominio en lugar de https://localhost.
- 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_STARTTLSdebe sertrueen minúsculas.Truese 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ó.
Véase Actualizar un servidor alojado más abajo.
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/.
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.
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.
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 cambiadoSolo 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/Caddyfiledeploy/.env está en gitignore, así que nunca se ve afectado.
diff .env.example .envCopie cualquier ajuste nuevo de .env.example a .env y, si cambió
algo, ejecute de nuevo docker compose up -d.
docker compose ps # app y worker creados recientemente, "Up"
docker compose logs --tail 50 app # líneas "Booting worker" de gunicorn, sin tracebacksDespué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.
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 -dSi la versión más nueva migró la base de datos, restaure también la copia de seguridad del paso 1.
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.
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 shEste 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.
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_KEYse 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 endeploy/.env; no hay alternativaadmin/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.
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.toolTrampa 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.
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.
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-stoppedpublicado 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).
- 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 -vlo 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 servicioworker: la importación, la subida de archivos multimedia y el reindexado de búsqueda se despachan a través de Celery en cuanto se defineGRAMPSWEB_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 entreappyworkerpor 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
workerse 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 degi.repository.GLibde 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) compiladeploy/Dockerfiley subeghcr.io/<owner>/gramps-connect:latest.docker compose -f deploy/docker-compose.yml --env-file deploy/.env pulldescarga esa imagen en lugar de compilarla localmente;up -d --buildsigue ahí para iterar localmente sobre el propio Dockerfile.