Docker 部署

新不可变内容边界:fresh PDF 与完整解析成员写独立 s3.bucket_content (默认 qatlas-content,必须区别于旧桶/corpus),本地后端使用同一 content/ 逻辑命名空间。 成员 create-only、持久SHA复核、manifest最后发布,PG成功后才切current;PG不可用不延期报成功。 同SHA可复用source ID;arXiv vN语义、source字节身份、S3VersionId物理版本、parse revision 不同。冻结导入别名不同字节409(overwrite也不替换),解析重传新revision。 旧三桶/layout/versioning/prune步骤保留为历史与旧资源运维,不得据此覆盖冻结原件; 新功能不bulk回填,不删除旧桶,不复制旧MD/JSON/images,只在内容/PDF访问懒冻结旧PDF。 无nightly/boot/入库自动解析,只有显式内容访问或管理员明确RunNow。 当前API/存储/错误与上传规则见论文内容契约和Upload API。

单 binary(qatlasd)的 docker 流派部署。自 v0.16.0 起正式支持,跟 systemd 流派并列;选哪个取决于你的运维偏好和数据规模,见下面表格。

何时选 docker / 何时选 systemd

维度

systemd(裸 binary)

docker compose

单进程极简部署

推荐

支持

一键起完整栈(含 PostgreSQL)

不支持(手动多服务)

推荐

多服务隔离 / k8s / 集群

不支持

推荐

升级 binary

install-qatlasd.sh 重跑 + systemctl restart

docker compose pull && docker compose up -d

配置 hot-reload

改 .env + systemctl restart

改 .env + 重建 container

资源占用

binary ~50 MB RSS

+ container overhead 5-10 MB / service

默认日志去向

journald (journalctl -u qatlasd)

stdout (docker logs qatlasd / log driver)

Caddy / nginx 反代

host 上跑(推荐)

host 上跑或者 compose 内 sidecar

备份

rsync ~/.local/share/qatlasd/(XDG)/ /var/lib/quantum-atlas/(FHS)

tar czf data.tgz data/(bind mount)

简单原则:

  • 个人 / 实验室 / 评估:docker compose 全家桶 — docker compose up -d 一行起 PostgreSQL + qatlasd(对象存储留外部,如 NAS 上的 RustFS)

  • 生产单边缘 / 多边缘 active-active:systemd binary + 外部 RustFS / PostgreSQL(mesh 共享)

  • k8s / Nomad / Swarm:docker image 作为 building block

  • 想随时切换:两种流派完全兼容数据(同一份 pb_data / RustFS bucket / PG database 可以今天 systemd 跑明天 compose 跑),不存在 lock-in

镜像与标签

服务端镜像由 GoReleaser dockers_v2 构建并发布到 GitHub Container Registry,默认平台为 linux/amd64 + linux/arm64。版本标签来自 .Tag 和 .Version;只有非 prerelease 且非 snapshot 时另更新 latest:

ghcr.io/iai-ustc-quantum/qatlasd:vX.Y.Z   # 带 v 前缀的精确版本
ghcr.io/iai-ustc-quantum/qatlasd:X.Y.Z    # 去 v 的版本
ghcr.io/iai-ustc-quantum/qatlasd:latest   # 每次非预发布、非快照的镜像发布时更新

例如 vX.Y.Z-rc.N 只更新两个版本标签,不更新 latest。latest 不按版本大小排序,也不等待外部 smoke;GitHub Latest 另由 GoReleaser/平台默认处理,两者不是原子发布。部署应固定已核验的 tag 或 digest,不能把流程成功或 latest 更新当作生产运行验证。

Dockerfile.goreleaser 复用已经内嵌 UI/文档的 GoReleaser 预编译二进制,不重复源码编译;原 Dockerfile 保留为本地源码构建路径。发布镜像仍基于 gcr.io/distroless/static-debian12:nonroot,无 shell、默认 UID 65532,保留现有运行参数。真实多架构 manifest、拉取与运行仍须单独验证。

后续采用新 workflow 的版本在 GoReleaser 发布后,由官方 actions/attest 读取 dist/digests.txt 生成签名构建证明,默认登记到 GitHub,不推送 registry bundle。对已核对的镜像 digest,可用 GitHub CLI 验证:

gh attestation verify 'oci://ghcr.io/iai-ustc-quantum/qatlasd@sha256:<digest>' \
  --repo IAI-USTC-Quantum/QuantumAtlas

来源证明关联构建身份与镜像摘要,不保证程序无 bug。已发布的 v0.35.0-rc.1 不补签新 GitHub 证明;其已验证的 BuildKit 双架构 SBOM/provenance 不受影响,两类证明不能混同。attestation 失败时 Release 和镜像可能已公开,应先核对实际状态,不直接重跑覆盖;稳定版推送时更新 latest 的条件不因此改变。

A. compose 全家桶(推荐起手式)

适合:单机评估、实验室、个人部署。

git clone https://github.com/IAI-USTC-Quantum/QuantumAtlas
cd QuantumAtlas/deploy
cp .env.docker.example .env
$EDITOR .env                      # 改密码 + GitHub OAuth client
chmod 0600 .env

# Distroless nonroot UID 65532 — 必须 chown 不然 server 写不了
mkdir -p data/{raw,pb_data}
sudo chown -R 65532:65532 data/{raw,pb_data}

docker compose up -d
docker compose logs -f qatlasd    # 看启动 log,Ctrl-C 退出
curl http://localhost:4200/api/health
# {"status":"healthy",...}  (degraded ok — S3 还没 bootstrap)

首次启动后:bootstrap RustFS svcacct + buckets

RustFS 启动时只有 root 凭据(.env 里的 RUSTFS_ROOT_*)。qatlasd 必须用service account key(最小权限原则;root key 不能用),先建 svcacct 才能让 qatlasd 真正 write 进去:

# 用一次性 mc 容器(distroless image 没 shell,没法 docker exec 进 rustfs)
docker run --rm --network=container:qatlas-rustfs minio/mc \
    mc alias set local http://localhost:9000 \
    "$RUSTFS_ROOT_ACCESS_KEY" "$RUSTFS_ROOT_SECRET_KEY"

# 建 svcacct(输出会含一对 access/secret,记下来)
docker run --rm --network=container:qatlas-rustfs minio/mc \
    mc admin user svcacct add local --name qatlasd "$RUSTFS_ROOT_ACCESS_KEY"

# 建三个 buckets
docker run --rm --network=container:qatlas-rustfs minio/mc \
    mc mb local/qatlas-pdf local/qatlas-md local/qatlas-images

把 svcacct 的 access/secret 填回 .env 的 QATLAS_S3_ACCESS_KEY_ID / QATLAS_S3_SECRET_ACCESS_KEY,然后重启 qatlasd:

docker compose up -d qatlasd      # 只重建 qatlasd container,其他不动
curl http://localhost:4200/api/health | jq .data.checks.raw_store

如果嫌 mc 步骤烦,可以用仓库自带的 scripts/rustfs_bootstrap.sh 一键搞定(指向 http://localhost:9000)。

完整 .env 字段

参见 deploy/.env.docker.example,每个字段都带 inline 注释。最容易踩的两个坑:

  • QATLAS_S3_* 必须是 svcacct 凭据,不要直接复制 RUSTFS_ROOT_*(root key 工作得了,但出事根本无法回滚)

  • QATLAS_ALLOWED_GITHUB_LOGINS 或 QATLAS_ADMIN_GITHUB_LOGINS 至少要填一个,否则 fail-closed 谁都登不了(这是刻意的;防止配置失误导致公网暴露)

B. compose standalone(qatlasd-only)

适合:已有外部 RustFS(NAS、公有云 S3、R2、…)和外部 PostgreSQL(专门 VPS、托管 RDS)的场景。

cd QuantumAtlas/deploy
cp .env.docker.example .env
$EDITOR .env                      # 填 QATLAS_S3_ENDPOINT / QATLAS_POSTGRES_DSN 等
chmod 0600 .env

mkdir -p data/{pb_data,raw}
sudo chown -R 65532:65532 data/

docker compose -f docker-compose.standalone.yml up -d

QATLAS_S3_ENDPOINT / QATLAS_POSTGRES_DSN 在 standalone compose 里都是必填(没有默认会让它意外回到全家桶模式)。

C. docker run 单 image(k8s / Nomad / 自定义编排)

如果你不用 compose(kubernetes deployment / Nomad job / cap1ystone),直接 pull image:

docker run -d --name qatlasd \
    -p 127.0.0.1:4200:4200 \
    -v /srv/qatlas/pb_data:/data/pb_data \
    -v /srv/qatlas/raw:/data/raw \
    -e QATLAS_S3_ENDPOINT=https://rustfs.example.com \
    -e QATLAS_S3_BUCKET_PDF=qatlas-pdf \
    -e QATLAS_S3_BUCKET_MD=qatlas-md \
    -e QATLAS_S3_BUCKET_IMAGES=qatlas-images \
    -e QATLAS_S3_ACCESS_KEY_ID=... \
    -e QATLAS_S3_SECRET_ACCESS_KEY=... \
    -e QATLAS_POSTGRES_DSN=postgres://qatlas:secret@pg.example.com:5432/qatlas?sslmode=disable \
    -e GITHUB_CLIENT_ID=... \
    -e GITHUB_CLIENT_SECRET=... \
    -e QATLAS_ALLOWED_GITHUB_LOGINS=your-login \
    ghcr.io/iai-ustc-quantum/qatlasd:latest

或者用 --env-file:

docker run --rm --env-file /path/to/qatlasd.env \
    -v /srv/qatlas:/data \
    -p 127.0.0.1:4200:4200 \
    ghcr.io/iai-ustc-quantum/qatlasd:latest

健康检查:

docker run --rm ghcr.io/iai-ustc-quantum/qatlasd:latest --version
# qatlasd version X.Y.Z

完整 env 列表参考 docs/deployment/server-config.md。

加 Caddy 反代(可选)

compose 默认只把 qatlasd 4200 端口 bind 在 127.0.0.1 —— 公网入口靠反代。最简方案是把 caddy 加进同一个 compose 文件:

# 加在 deploy/docker-compose.yml 的 services 块里
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data           # LE certs / OCSP cache,重启不丢
      - caddy_config:/config

volumes:
  caddy_data: {}
  caddy_config: {}

对应的 ./Caddyfile:

atlas.your-domain.com {
    reverse_proxy qatlasd:4200      # service name in compose network
}

Caddy 自动跟 Let's Encrypt 申请证书(前提:80/443 公网可达 + 域名 A 记录指过来)。

更复杂的拓扑(多边缘 / SSO / 双 endpoint)见 反向代理。

加 fluent-bit / RustFS audit notify(可选)

T10 审计 sink 用 fluent-bit 中继 RustFS notify webhook 到独立 audit bucket。完整 compose 片段见 deploy/nas-rustfs-compose.example.yaml,可以整段复用进 docker-compose.yml。

备份 / 升级 / 回滚

备份

bind mount 模式备份就是 tar:

docker compose stop                       # 短暂停一致性 snapshot
tar czf qatlas-backup-$(date +%F).tgz data/
docker compose start

或者只挑 critical state(pb_data + postgres 的 named volume;raw 资产可从 S3 重建 registry):

tar czf qatlas-critical-$(date +%F).tgz data/pb_data   # + docker volume 里的 pgdata

升级 qatlasd

# 升 image tag
$EDITOR .env                              # 改 QATLAS_VERSION=v0.17.0
docker compose pull qatlasd
docker compose up -d qatlasd              # 只重建 qatlasd,postgres 不重启

# 或者跟 latest 滚动
docker compose pull
docker compose up -d

回滚

# 紧急回滚到上个 version
QATLAS_VERSION=v0.15.0 docker compose up -d qatlasd

回滚前先检查目标版本的 GitHub Release 和对应版本的迁移/部署文档。现行 Release 正文由 GoReleaser 从 Git 提交生成,不等于完整的 schema 兼容性说明。换回旧镜像只回滚程序,不会逆转 PocketBase/goose 数据迁移,即使 patch 版本也不能保证回滚安全。升级前备份数据库与相关状态;必要时停止服务并从备份恢复,确认版本守卫允许后再启动。

升级 RustFS / PostgreSQL

各自有 changelog 看。RustFS 主要看 RUSTFS_DRIVE_TIMEOUT_PROFILE 这种 hot path 兼容;PostgreSQL 跨 major 升级前先做 pg_dump,并遵循官方升级指引(registry 表结构由 qatlasd 内的 goose migrations 管理,启动时自动 apply)。

常见坑

"permission denied" on ./data/*

distroless nonroot 用户 UID 是 65532。mkdir data/ 是当前用户拥有的,container 写不进去:

ls -ln data/raw
# drwxrwxr-x 2 1000 1000 ...    # 1000 = host user, not 65532

sudo chown -R 65532:65532 data/{raw,pb_data}
docker compose up -d qatlasd

postgres container 不受影响(official image 自己处理数据目录权限)。

GitHub OAuth callback URL 必须是公网 URL

OAuth app 注册时填 https://atlas.your-domain.com/api/oauth2-redirect,不能填 http://localhost:4200/...。GitHub 不接受 localhost callback;最低门槛是带 TLS 的真域名。本地测:用 caddy 反代 + 自签 cert + 127.0.0.1 atlas.local.test 写 hosts 文件。

docker exec -it qatlas-rustfs sh 不行

distroless 没 shell。RustFS 用的是 rustfs/rustfs:latest image(不是 distroless),有 shell;但 qatlasd image 是 distroless static,不能 exec 进去 debug。要 debug 时:

# 替换 entrypoint 拉一次性 image with shell(不能附在 running qatlasd 上)
docker run --rm -it --entrypoint=/qatlasd \
    ghcr.io/iai-ustc-quantum/qatlasd:latest \
    config show              # 任意 subcommand,看 env 配置

或者 sidecar 模式跑个 busybox/alpine mount 同样的 volume 做文件检查。

mc 不能从 host 连 rustfs container

rustfs service 默认只在 compose 内部网络可见(没 publish 9000)。host 上的 mc 连不上 localhost:9000。两条路:

  • 在 compose 文件给 rustfs 加 ports: ["127.0.0.1:9000:9000"](仅 dev / 受信网络)

  • 用 --network=container:qatlas-rustfs 跑一次性 mc container(共享 rustfs 的 network namespace,连 localhost:9000 就是连 rustfs 自己)—— 上面 bootstrap 步骤就用了这招

想看 server log

docker compose logs -f qatlasd
# 或保留最近 100 行:
docker compose logs --tail 100 qatlasd

journald 不存 — log 直接走 stdout 给 docker 的 log driver。生产可以挂 json-file driver + log rotate,或者推到外部 loki / ELK / cloudwatch。