Skip to content

自托管 AFFiNE:镜像源、digest、和那个"迁移任务已退出" ​

AFFiNE 是个开源知识库(文档 + 白板一体)。自托管它比装一个单容器应用麻烦一点:它是多容器的(应用 + Postgres + Redis + 一个一次性的迁移任务)。

这篇记录四件事:镜像怎么拉到、为什么要把镜像 pin 到 digest、externalUrl 不配对会怎样、以及那个"迁移任务 Exited"为什么是正常的。

一、镜像:ghcr 的两个坑 ​

AFFiNE 只发布在 GitHub Container Registry(ghcr.io),不在 Docker Hub。而 ghcr.io 在国内直连实测掉到 ~5KB/s,基本等于拉不下来。

两个代理源,实测结果差别很大:

源结果
ghcr.m.daocloud.io❌ 有白名单,affine 不在名单里,直接拒绝
ghcr.nju.edu.cn✅ 快,无白名单限制
bash
docker pull ghcr.nju.edu.cn/toeverything/affine:stable
docker tag  ghcr.nju.edu.cn/toeverything/affine:stable ghcr.io/toeverything/affine:stable

教训:镜像加速源不是"随便哪个都能用"。有的只代理白名单里的镜像(给自家产品用),有的全量代理。报错信息通常很含蓄——不是 403,而是 "not in the allowlist" 或者直接 404。

二、pin 到 digest(这条官方专门提醒过) ​

AFFiNE 的 stable tag 会滚动更新,而新版本可能包含数据库迁移。自动拉新版 = 某天重启容器,数据库被单向迁移了,回退不了。

所以把镜像钉死在 digest:

yaml
services:
  affine:
    image: ghcr.nju.edu.cn/toeverything/affine@sha256:b649f5ce2384ffdf13c23bccf81d759e15973d59d0b0058af65d895a84373099

⚠️ 注意 digest 前面要带 repo 前缀,而且必须和你本地 RepoDigests 里的那个前缀一致:

bash
docker inspect ghcr.io/toeverything/affine:stable --format '{{json .RepoDigests}}'

如果本地记录的是 ghcr.nju.edu.cn/...@sha256:...,compose 里就写 nju 前缀。写错了会变成"引用不存在",docker 会去拉,然后失败。

好处:compose 直接用本地镜像、不触发任何拉取;日后真要重拉也还走快源。

三、externalUrl 必须是最终对外地址 ​

AFFiNE 用 config.json 配置:

json
{
  "server": {
    "name": "AFFiNE",
    "externalUrl": "https://notes.example.com"
  },
  "auth": {
    "allowSignup": false
  }
}
  • externalUrl 写错域名 → 登录回调、邮件里的链接、分享链接全会指到错的地方。它必须是浏览器最终访问的那个 https://...,不能写 http://localhost:3010
  • allowSignup 默认是开的 → 意味着任何知道网址的人都能注册,而第一个注册的人就是管理员。部署完先注册自己,然后立刻关掉
bash
# 改完重启生效
docker compose up -d affine

⚠️ 这个文件里可能有 SMTP 密码之类的凭据,权限设 600:

bash
chmod 600 config.json

四、Postgres 数据目录的 uid ​

pgvector/pgvector:pg16 这个镜像里,Postgres 以 uid 999 运行。如果你用 bind mount 挂宿主机目录:

bash
mkdir -p data/postgres
chown -R 999:999 data/postgres     # 必须做

不做的话 Postgres 启动失败,日志报 could not open directory ... Permission denied 或者 initdb: could not change permissions of directory。

这是所有"官方 postgres 镜像 + bind mount"的通用坑,跟 AFFiNE 无关。

五、"迁移任务已退出" 是正常的 ​

docker compose ps 会看到一个容器死了:

affine_migration_job   Exited (0)

Exited (0) = 成功退出。 它是个 one-shot 任务:跑数据库迁移,跑完就该退出。不是崩溃。

判断标准永远是退出码,不是"容器还在不在跑":

bash
docker inspect affine_migration_job --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
# 期望:0 false

顺带一句:真正需要担心的是 OOMKilled=true(内存不够被杀),这个在 NAS 上跑 Postgres + Node 应用时并不罕见。

六、验证:从内网到公网两段走 ​

bash
# 内网:应该 302 到 /admin/setup(说明应用起来了,还没初始化)
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://127.0.0.1:3010/

# 公网:应该 200
curl -s -o /dev/null -w '%{http_code}\n' https://notes.example.com/

两段分开测的价值:内网不通 = 应用/容器问题;内网通但公网不通 = 隧道/域名问题。 一眼分开,不用猜。

七、备份 ​

三个东西,缺一不可:

  1. Postgres 数据 —— 用 pg_dump,别直接拷数据目录(可能不一致)
  2. config.json —— 里面有配置和凭据
  3. data/storage —— 用户上传的附件/图片
bash
docker exec affine_postgres pg_dump -U <user> <db> | gzip > /tmp/affine-$(date +%F).sql.gz

异地备份要加密(里面有用户内容和凭据)。云挂载点不能直写的坑见这篇。

检查清单 ​

  1. 镜像走 ghcr.nju.edu.cn(别用有白名单的代理)
  2. pin 到 digest,并且前缀和本地 RepoDigests 一致
  3. externalUrl = 最终对外 HTTPS 地址
  4. 部署完立刻注册管理员 + allowSignup: false
  5. postgres 数据目录 chown 999:999
  6. 迁移任务 Exited(0) 属正常,看退出码
  7. 验证分内网/公网两段
  8. 备份三件:pg_dump + config + storage

一句话版本:AFFiNE 的坑集中在"版本"和"地址"两件事上——镜像要 pin digest 防自动迁移,externalUrl 要写最终域名否则链接全错。