Repository navigation
Deploying.zh_CN
🌐 English · Deutsch · Español · Français
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),即可获得真正的证书来代替自签名证书。
以下所有命令都假设您位于仓库根目录。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/ 登录——在分配家谱之前签发的令牌不会带有家谱信息,所以如果您沿用了一个已经打开的会话,请退出后重新登录。
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 进一步调高(见该文件中的注释)。
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 使这两项更改生效。
默认没有配置(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 本身。
Gramps Connect is part of the family of Gramps-based software.
Using the app
- Overview
- Installing
- Deploying
- Messaging
- GOQL (advanced search)
- Gramplets & Add-on Store
- Data Model & Editing
- FAQ
Building & contributing