Deploying.zh_CN - dsblank/gramps-connect GitHub Wiki

🌐 English · Deutsch · Español · Français · Nederlands

部署

deploy/ 是 Gramps Connect 真正的多用户形态:容器化的 app/ 前端 + gramps-web-api 后端,以真正的 Postgres 为后端存储(通过 SharedPostgreSQL 插件),前面由 Caddy 提供 TLS,用于真正托管在某处——真实的密钥、真实的域名/证书,以及每人各有登录账号的多个用户。它也是亲眼看到实时协作的唯一方式;独立桌面版在设计上就是单用户的,因此看不到别人的编辑出现。

后端是官方的、未经修改的 dmstraub/gramps-webapi 镜像——也就是 gramps-project/gramps-web-api 自己的 CI 在每次发布时发布的镜像,gramps-project/gramps-web 本身也基于它构建——而不是由本仓库维护的源码构建。这让本部署保持为一个普通的标准 gramps-web-api 实例,任何兼容的客户端都可以与之通信,而不仅仅是 Gramps Connect 自己的前端。SharedPostgreSQL/PostgreSQL/FilterRules/JSON 插件、多家谱支持和已编译的翻译都已经包含在该镜像中;deploy/Dockerfile 唯一添加的就是 app/ 的前端,以静态文件的形式叠加在上面。代价是:该上游镜像基于 gramps-web-base 构建(约 4.3GB——torch、sentence-transformers、opencv、45 个 tesseract 语言包),无条件安装了 AI 附加组件,因为没有可以改用的官方精简版本。

前端和后端默认共享一个容器/源(gramps_webapi 通过 STATIC_PATH 提供构建好的 SPA,并在同一进程中处理 /api/*),因此 Gramps Connect 本身不需要任何 CORS 配置——只有在另一个单独托管的前端从外部调用时才需要(这种情况下在 deploy/.env 中设置 GRAMPSWEB_CORS_ORIGINS)。

服务:caddy(TLS 终止,唯一公开的入口)、app(gunicorn,提供前端 + /api/*)、worker(Celery,运行导入/媒体/搜索重建索引任务)、postgres(通过 SharedPostgreSQL 存储家谱数据)、redis(Celery 消息代理)。

在本地运行

cp deploy/.env.example deploy/.env    # 然后编辑它——见文件中的注释
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build

然后访问 https://localhost(Caddy 会自动为自己签发一个本地自签名证书——接受浏览器的警告即可)。

在真实主机上运行

在 GitHub 的 runner 上而不是在主机上构建一次(gh workflow run build-docker.yml,或在 Actions 标签页中触发),它会推送 ghcr.io/<owner>/gramps-connect:latest,然后在主机上:

cp deploy/.env.example deploy/.env    # 这次使用真实的密钥/域名
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

把一个域名指向该主机,并编辑 deploy/Caddyfile(见下文 TLS),即可获得真正的证书来代替自签名证书。

示例:在 DigitalOcean droplet 上托管

从一个空账号到一个在小型云服务器(DigitalOcean 称之为“droplet”)上正式运行的站点的完整步骤。只要您有一台装了 Docker 的 Linux 服务器,任何 VPS 提供商的做法都一样。

1. 创建 droplet

在 DigitalOcean 控制台中,选择 Create → Droplets:

  • Image:Marketplace → Docker on Ubuntu(预装 Docker)。
  • Size:Basic,1 vCPU / 2 GB 内存(s-1vcpu-2gb)是能良好运行的最小规格。后端镜像很大(约 4 GB,见上文),所以不要选更小的。
  • Region:离您的用户最近的区域。
  • Authentication:SSH 密钥(比密码更安全)。

然后在 Networking → Firewalls → Create Firewall 中:只允许入站的 22、80 和 443 端口,并把它应用到该 droplet。

2. 添加交换空间

在导入和启动期间,2 GB 内存比较紧张,所以请添加一个 2 GB 的交换文件(以 root 身份通过 SSH 登录):

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

3. 把一个域名指向它

这一步是可选的,但推荐这样做,因为有了域名才能获得真正的 HTTPS 证书。免费的 DuckDNS 名称就可以,您自己域名上的一条 A 记录也可以。无论哪种,都把它指向 droplet 的公网 IP。

4. 安装并启动 Gramps Connect

git clone https://github.com/dsblank/gramps-connect /opt/gramps-connect
cd /opt/gramps-connect/deploy
cp .env.example .env
# 编辑 .env:GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD、
# PUBLIC_URL=https://your.domain,以及电子邮件设置(见下文)
docker compose pull
docker compose up -d

这会拉取预先构建好的公开镜像 ghcr.io/dsblank/gramps-connect:latest,所以不需要在 droplet 上构建任何东西。

5. 启用 HTTPS

按照 TLS 中的说明编辑 deploy/Caddyfile:把 :443 { tls internal ... } 块替换为 your.domain { reverse_proxy app:5000 },并删除 :80 块,然后再次运行 docker compose up -d。Caddy 会自行申请并续期 Let's Encrypt 证书。

6. 创建第一棵家谱

按照下文的首次设置和导入数据操作,用 https://your.domain 代替 https://localhost。

电子邮件(重置密码、邀请)

  • 新的 DigitalOcean 账号会屏蔽出站 SMTP。请使用 587 端口上的中继服务(例如 Brevo、SendGrid、Mailgun);如果邮件仍然发不出去,请向 DigitalOcean 支持提交工单(类别 Networking),要求解除屏蔽。
  • GRAMPSWEB_EMAIL_USE_STARTTLS 必须是小写的 true。True 会被悄悄当作字符串读取,导致连接失败。
  • Brevo 的 SMTP 登录名是一个生成的地址(类似 [email protected]),而不是您注册时使用的电子邮件地址。

以后更新

见下文的更新托管服务器。

更新托管服务器

这些步骤适用于任何运行 deploy/ 栈的主机,无论是不是 droplet。命令假定仓库克隆在 /opt/gramps-connect,并且您位于其 deploy/ 目录中。

1. 备份

cd /opt/gramps-connect/deploy
# 家谱数据(Postgres)
docker compose exec -T postgres sh -c 'pg_dumpall -U "$POSTGRES_USER"' > backup-$(date +%F).sql
# 用户账号(sqlite)和媒体文件
docker compose cp app:/app/users  ./backup-users-$(date +%F)
docker compose cp app:/app/media  ./backup-media-$(date +%F)

更新可能会在启动时运行无法回退的数据库迁移,所以备份是完全撤销一次更新的唯一办法。

2. 确认新镜像已经构建好

服务器拉取的是 ghcr.io/dsblank/gramps-connect:latest,它只有在 build-docker.yml 工作流运行时才会改变。刚给一个版本打上标签后,请先确认 Docker 构建已经完成(仓库的 Actions 标签页)再更新,否则拉取到的会是上一个版本。

3. 更新

cd /opt/gramps-connect && git pull       # 更新后的 deploy/ 配置
cd deploy
docker compose pull app                  # 新镜像
docker compose up -d                     # 重新创建有变化的部分

只有 app 和 worker 会被重新创建。postgres、redis 和 caddy 保持运行,所有数据都保留在 Docker 卷中。除非您确实想删除一切,否则永远不要给 docker compose down 加上 -v(见备注)。

如果 git pull 因本地修改而拒绝执行:使用真实域名的主机有一个编辑过的 deploy/Caddyfile(见 TLS),它从不会被提交,所以一次修改了仓库中该文件的更新会与它冲突。用以下命令保留您的修改:

git stash && git pull && git stash pop    # 然后解决 deploy/Caddyfile 中的冲突

deploy/.env 已被 gitignore,所以永远不会受影响。

4. 检查新增设置

diff .env.example .env

把 .env.example 中新增的设置复制到 .env,如果有改动,再运行一次 docker compose up -d。

5. 验证

docker compose ps                  # app 和 worker 刚刚创建,状态为 "Up"
docker compose logs --tail 50 app  # gunicorn 的 "Booting worker" 行,没有 traceback

然后打开站点并登录。每个 gunicorn worker 出现一行 Gtk-CRITICAL ... gtk_icon_theme_get_for_screen 是正常且无害的:服务器装了 GTK,但没有显示器。

回滚

每个镜像还会打上构建它的提交的标签,即 ghcr.io/dsblank/gramps-connect:<commit-sha>(列表见仓库的 Packages 页面)。要回滚,把 deploy/docker-compose.yml 顶部 x-app-image 下的 image: 行改为那个标签,然后运行:

docker compose up -d

如果较新的版本迁移过数据库,还要恢复第 1 步中的备份。

运行您自己的分支

分支(fork)会发布它自己的镜像。在您的分支中运行 build-docker.yml(推送 ghcr.io/<your-user>/gramps-connect),并把上面的 image: 行设为它;或者用 docker compose up -d --build 在服务器上构建。在服务器上构建需要比上面 2 GB 的 droplet 更多的磁盘和内存,因为仅基础镜像就约有 4 GB。

Docker 命令

以下所有命令都假设您位于仓库根目录。docker compose 是插件形式;如果您的 Docker 安装只有独立的二进制文件,请改用 docker-compose(两种方式的参数相同)。

# 全部五个服务的状态
docker compose -f deploy/docker-compose.yml ps

# 日志(加 -f 持续跟踪,加 --tail=100 限制行数)
docker compose -f deploy/docker-compose.yml logs app
docker compose -f deploy/docker-compose.yml logs worker

# 重启一个服务(例如修改了 deploy/.env 中的环境变量之后)
docker compose -f deploy/docker-compose.yml up -d
# ^ 重新创建配置(镜像、环境变量、卷)发生变化的任何服务;
#   如果编辑了 deploy/Dockerfile 或 app/后端源码,请先加上 --build。

# 停止所有服务,保留数据(卷会保留)
docker compose -f deploy/docker-compose.yml down

# 停止并删除所有数据(小心——会删除 Postgres、媒体、用户等)
docker compose -f deploy/docker-compose.yml down -v

# 进入一个正在运行的容器的 shell
docker compose -f deploy/docker-compose.yml exec app sh

登录凭据:本部署与独立版的区别

这个 docker 部署没有默认密码——在首次启动前,您需要自己在 deploy/.env 中设置 GRAMPSWEB_ADMIN_USER/GRAMPSWEB_ADMIN_PASSWORD,入口脚本会据此创建账号。没有任何东西会为您生成或打印密码。

这与 gramps-connect-desktop(单用户的 PyInstaller 演示版,与这个 docker 部署无关)不同:它总是创建一个固定的 admin/admin 账号,这在那里没有问题,因为它只是一个用完即弃的本地演示,而不是暴露在真实服务器上的东西。

首次设置:预置的管理员

首次启动时(app-users/app-db 卷第一次为空时),入口脚本会:

  • 如果 GRAMPSWEB_SECRET_KEY 留空,则生成并持久化一个 Flask 密钥
  • 运行用户数据库迁移
  • 根据 GRAMPSWEB_ADMIN_USER / GRAMPSWEB_ADMIN_PASSWORD(两者在 deploy/.env 中都是必需的——没有 admin/admin 回退)创建一个站点管理员用户(不属于任何家谱,角色 5)

不属于任何家谱的站点管理员可以通过 API 创建/列出/删除家谱,但是——由于 app/ 的前端没有选择家谱的界面,并且总是期望已登录用户的 JWT 中已经带有一棵家谱——在被分配到某棵家谱之前,无法浏览家谱数据。多家谱模式下没有用于创建家谱的 CLI 命令,所以请通过 API 创建第一棵家谱(只需一次):

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"

然后,要么把站点管理员本身分配到这棵家谱,要么(推荐——让站点管理和家谱所有权保持分离)创建一个专门属于该家谱的用户:

# 方案 A:把现有的站点管理员分配到这棵家谱。
curl -sk -X PUT "https://localhost/api/users/<admin>/" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"tree\":\"$TREE_ID\"}"

# 方案 B:改为创建一个与该家谱绑定的独立普通用户。
# 即使不使用,email 和 full_name 也是模式要求的必填项。
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 是一个整数(gramps_webapi.auth.const):

角色 值 说明
ADMIN 5 站点管理员;唯一可以不属于任何家谱的角色
OWNER 4 完全控制自己的家谱
EDITOR 3 可以编辑家谱数据
CONTRIBUTOR 2 可以添加数据
MEMBER 1 读取权限
GUEST 0 最低限度的读取权限

然后以您刚刚创建的用户在 https://localhost/ 登录——在分配家谱之前签发的令牌不会带有家谱信息,所以如果您沿用了一个已经打开的会话,请退出后重新登录。

导入数据(例如带媒体的 example.gramps)

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. 导入 .gramps XML 文件。重要:这个端点读取原始请求体并直接写入磁盘
#    ——它不会解析 multipart/form-data,所以请使用 --data-binary,而不是
#    curl 的 -F/--form(multipart 上传会悄悄地用分隔符/头部字节破坏文件,
#    然后 Gramps 的导入器会以一个笼统、毫无帮助的 "Import failed" 失败,
#    没有任何更多细节)。
curl -sk -X POST https://localhost/api/importers/gramps/file \
  -H "Authorization: Bearer $TOKEN" --data-binary @example.gramps

# 响应是一个任务句柄(导入通过 Celery 在 `worker` 服务上运行)——
# 轮询它直到 "state":"SUCCESS"(或 "FAILURE"):
curl -sk https://localhost/api/tasks/<task_id> -H "Authorization: Bearer $TOKEN"

# 2. 媒体:把被引用的媒体文件打成 zip 包(任意子集/超集都可以——文件按
#    校验和匹配,未被引用的会被忽略),然后同样以原始请求体上传该压缩包:
curl -sk -X POST https://localhost/api/media/archive/upload/zip \
  -H "Authorization: Bearer $TOKEN" --data-binary @media.zip

# 3. 验证:已登录用户的家谱中的对象数量。
curl -sk https://localhost/api/metadata/ -H "Authorization: Bearer $TOKEN" \
  | python3 -m json.tool

已知陷阱:在 GRAMPSWEB_MEDIA_PREFIX_TREE=True(本 compose 文件中默认设置)的情况下,媒体存储在 MEDIA_BASE_DIR/<tree_id>/ 下,但没有任何东西会自动创建这个按家谱划分的子目录——POST /api/trees/ 不会,媒体压缩包上传任务也不会。在创建一次之前,为某棵家谱上传媒体会失败,提示 Directory /app/media/<tree_id> does not exist:

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

大型媒体上传超时:单文件(POST /api/media/)和 ZIP 压缩包(POST /api/media/archive/upload/zip)两个上传端点,都会在 app 的 gunicorn worker 中同步读取请求体,然后才交给 Celery,因此一个大文件或一条慢速连接可能会超过 gunicorn 的单请求超时——浏览器随后只会看到一个没有 JSON 正文的 Internal Server Error(来自 gunicorn/Caddy,而不是 Gramps)。deploy/docker-compose.yml 默认把 GUNICORN_TIMEOUT 设为 600 秒;如果上传仍然失败,可以在 deploy/.env 中通过 GUNICORN_TIMEOUT 进一步调高(见该文件中的注释)。

TLS

caddy 是唯一公开了端口(80/443)的服务,并在 app 前面终止 TLS。在没有配置域名的情况下,它会自动生成并持久化自己的本地 CA,并由它签发一个自签名证书(deploy/Caddyfile 中的 tls internal)——浏览器第一次会显示信任警告;接受即可继续(如果希望在没有真实域名的情况下消除警告,也可以把生成的 CA 加入系统/浏览器的信任存储)。80 端口会重定向到 443。

一旦有真实域名指向这台服务器,就编辑 deploy/Caddyfile:把 :443 { tls internal ... } 块替换为 example.com { reverse_proxy app:5000 },并删除 :80 块——对于真实域名,Caddy 会自动处理 ACME 证书签发、续期和 80 端口重定向,不需要 tls 指令。同时把 deploy/.env 中的 PUBLIC_URL 更新为真实的 https:// 域名,然后运行 docker compose -f deploy/docker-compose.yml up -d 使这两项更改生效。

与 Gramps Connect 并行运行 gramps-web

默认没有配置(docker-compose.yml 中没有 grampsweb 服务),但值得了解:由于后端是一个普通的、未经修改的 gramps-web-api 实例,gramps-project/gramps-web(另一个官方 Gramps 前端)也可以连接这个后端运行——相同的数据、相同的家谱/用户,只是在另一个端口上提供不同的界面。

gramps-web 正是为此发布了 ghcr.io/gramps-project/grampsjs:latest:只用 nginx 提供它的静态构建,不包含后端。它的 nginx 配置会在容器启动时把 /api 反向代理到 API_HOST 环境变量所指定的地址,因此从浏览器的角度看是同源的——专门针对这条路径不需要 GRAMPSWEB_CORS_ORIGINS(该设置用于托管在完全不同位置、跨源调用而不经过此代理的 gramps-web 实例)。

要添加它,可以在 docker-compose.yml 中加入一个类似这样的服务:

grampsweb:
  image: ghcr.io/gramps-project/grampsjs:latest
  environment:
    API_HOST: http://app:5000
    # Docker 内置的 DNS 解析器——default.conf.template 的 `resolver`
    # 指令要求显式设置它。
    NAME_SERVER: 127.0.0.11
  depends_on:
    - app
  restart: unless-stopped

并在它自己的端口上公开(要么直接公开,例如 ports: ["8081:80"],要么放在 Caddy 后面,在 deploy/Caddyfile 中添加第二个形如 :8443 { reverse_proxy grampsweb:80 } 的块,以便像 app 服务一样使用 TLS)。

备注

  • 数据保存在具名 Docker 卷中(app-db、app-media、app-indexdir、app-users、app-secret、app-cache、app-tmp、postgres-data、caddy-data、caddy-config)。docker compose down(不带 -v)会保留它们;docker compose down -v 会删除一切(包括生成的 CA——这意味着下次启动时浏览器会再次出现信任警告,而不只是数据丢失)。
  • redis(Celery 消息代理/结果后端)是后台任务(搜索索引、大型导入/导出任务)所必需的——不是可选项。worker 服务也是如此:一旦设置了 GRAMPSWEB_CELERY_CONFIG__*,导入、媒体压缩包上传和搜索重建索引都会通过 Celery 分派,如果没有东西消费队列,它们就会作为未完成的任务永远挂着。出于同样的原因,app-cache(/app/cache)必须是 app 和 worker 共享的卷:app 容器的请求处理程序把上传的文件写到那里,然后 worker 容器的 Celery 任务再把它读回来。
  • worker 服务以 --pool=solo(不 fork)运行,这是出于谨慎考虑:Celery 默认的 prefork 池会 fork 一个已经接触过真正 PyGObject/GTK 状态的进程(Gramps 的 gi.repository.GLib 导入)。单个 worker 实例并不需要 --pool=solo 所放弃的并发能力,但如果 worker 吞吐量将来成为瓶颈,值得重新审视这一点。
  • app/ 的浏览器端本地缓存(sql.js、OPFS)以每个视图固定的文件名为键,而不是以后端 URL 或家谱 ID 为键——这会导致的陷阱以及如何清除,见架构。
  • .github/workflows/build-docker.yml(手动触发——gh workflow run build-docker.yml,或 Actions 标签页)会构建 deploy/Dockerfile 并推送 ghcr.io/<owner>/gramps-connect:latest。docker compose -f deploy/docker-compose.yml --env-file deploy/.env pull 会拉取该镜像而不是在本地构建;up -d --build 仍然保留,用于在本地迭代修改 Dockerfile 本身。
⚠️ **GitHub.com Fallback** ⚠️