Paperclip 用 Docker 部署:四条路径、两个必生成的密钥和数据落在哪

2026-08-17

想用 Docker 把 Paperclip 跑起来的人,通常是冲着一句话来的:不想在本机装 Node 和 pnpm。官方文档开篇写的也正是这个意思——在 Docker 里运行 Paperclip,本地不需要装 Node 或 pnpm。

但真去翻文档会发现一件有点绕的事:Docker 相关内容分散在两个文件里,一个是文档站的部署页,一个是仓库 doc/DOCKER.md。两份都在讲 Docker,覆盖范围却不一样:部署页是精简版,只讲 compose quickstart 和手工 build;doc/DOCKER.md 是全量版,多出全栈 PostgreSQL compose、Podman Quadlet、认证模式的公开 URL 配置、以及一个专门模拟”干净 Ubuntu + npm”环境的冒烟测试脚本。如果你只看了其中一份,很容易漏掉真正影响生产可用性的那几项。

这篇就按官方原文,把四条路径和它们各自的前提条件摊开讲清楚。

四条路径分别解决什么问题

路径命令入口数据库官方给的定位
Compose quickstartdocker/docker-compose.quickstart.yml单容器内嵌,无外部数据库文档站标为 Recommended
手工 build + rundocker build + docker run同上(走 bind mount)需要自己控制参数时用
全栈 composedocker/docker-compose.yml独立 PostgreSQL 17 容器服务端 + 数据库分离
Podman Quadletdocker/quadlet/*.pod*.containerPostgreSQL 17,同 pod以 systemd 服务方式常驻

还有一条不算部署、但值得知道的:docker/docker-compose.untrusted-review.yml,官方用途是在隔离容器里用 Codex 或 Claude 审阅不可信的 pull request,不把宿主机暴露出去。完整流程官方指向 doc/UNTRUSTED-PR-REVIEW.md,这里不展开。

先说选型的现实依据。如果只是想在自己机器上看看 Paperclip 是怎么组织”公司—Agent—任务”这套东西的,quickstart 那条单容器路径最省事;如果要给团队长期用、要接外部数据库、要挂域名和认证,就得往全栈 compose 或 Quadlet 走。部署模式本身(是否开认证、暴露范围是 private 还是 public)是另一层选择,和容器化方式正交,详见 三种部署模式怎么选

Quickstart 的默认值,以及两份文档给的命令不一样

文档站给的 quickstart 命令是这一条:

docker compose -f docker/docker-compose.quickstart.yml up --build

起来之后访问 http://localhost:3100。默认值官方写得很明确:宿主机端口 3100,数据目录 ./data/docker-paperclip

要改端口和数据目录,用环境变量前置:

PAPERCLIP_PORT=3200 PAPERCLIP_DATA_DIR=../data/pc \
  docker compose -f docker/docker-compose.quickstart.yml up --build

这里有个特别容易踩的点,官方专门加了 Note:PAPERCLIP_DATA_DIR 是相对 compose 文件所在目录(也就是 docker/)解析的,所以写 ../data/pc 实际映射到的是项目根目录下的 data/pc。如果你按直觉当成”相对项目根”来填,数据会落到你以为的上一级去。

doc/DOCKER.md 里同样这条 quickstart,命令前面多带了两个环境变量:

BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=$(openssl rand -hex 32) \
  docker compose -f docker/docker-compose.quickstart.yml up --build

两份官方文档在同一条命令上给了不同写法,文档本身没有解释差异原因。稳妥的做法是按 doc/DOCKER.md 这版来——显式生成这两个 secret 不会有额外代价,而缺了它们的后果,文档没有说明。这两个变量的含义与其它环境变量一起,属于环境变量清单的范畴。

另外,doc/DOCKER.md 明确提醒:所有命令都假设你在项目根目录(含 package.json 的那个目录)执行,而不是在 docker/ 里面。这和上面 PAPERCLIP_DATA_DIR 的相对路径规则合在一起,是这块最集中的两个路径坑。

手工 build:UID/GID 是为 bind mount 准备的

手工构建镜像就一行:

docker build -t paperclip-local .

官方说明 Dockerfile 会装上一批常用的 agent 工具:gitghcurlwgetripgreppython3,以及 Claude、Codex、OpenCode 三个 CLI。

镜像提供两个 build 参数:

参数默认值作用
USER_UID1000容器内 node 用户的 UID,建议与宿主机 UID 一致,避免 bind mount 上的权限问题
USER_GID1000容器内 node 组的 GID

对应的构建命令:

docker build -t paperclip-local \
  --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .

配套的机制在 General Notes 里:docker-entrypoint.sh 会在启动时按 USER_UID / USER_GID 传入的值调整容器内 node 用户的 UID/GID,目的就是规避 bind mount 挂载卷上的权限问题。也就是说,这不是可选的美化项,而是宿主机上那个 ./data/docker-paperclip 目录能不能被容器正常写入的关键。

doc/DOCKER.md 给的一行式 build + run 是这样:

docker build -t paperclip-local . && \
docker run --name paperclip \
  -p 3100:3100 \
  -e HOST=0.0.0.0 \
  -e PAPERCLIP_HOME=/paperclip \
  -e BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
  -e PAPERCLIP_TOOL_ACTION_SIGNING_SECRET=$(openssl rand -hex 32) \
  -v "$(pwd)/data/docker-paperclip:/paperclip" \
  paperclip-local

三个位置值得单独记:HOST=0.0.0.0 决定服务监听是否能被容器外访问;PAPERCLIP_HOME=/paperclip 是容器内的数据根;bind mount 把宿主机 ./data/docker-paperclip 挂到这个数据根上。这三者是一组,改一个就得连带改。

数据到底存了哪些东西

官方把 bind mount 里的持久化内容列成了四项:

  • 内嵌 PostgreSQL 的数据
  • 上传的资源文件
  • 本地密钥(local secrets key)
  • Agent 工作区数据

这份清单值得认真对待——它意味着删掉 ./data/docker-paperclip 不只是丢数据库,本地密钥和 Agent 工作区一起没了。

这里也要指出文档自身的一处不一致:doc/DOCKER.md 里 quickstart 那一节的小标题写的是 “Quickstart (embedded SQLite)“,正文却说单容器、无外部数据库,而数据持久化清单列的又是 “Embedded PostgreSQL data”。两处措辞对不上,官方文档没有说明哪个是准的。真要确认内嵌数据库究竟是什么、以及怎么切到外接实例,应该以数据库那篇专门文档为准,参见内嵌 Postgres 与外接数据库

全栈 compose 这条路就没有这个歧义:官方写明是 Paperclip 服务端 + PostgreSQL 17,数据库会先做健康检查、通过之后服务端才启动。持久化上,PostgreSQL 数据放在名为 pgdata 的具名卷,Paperclip 数据放在 paperclip-data

BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
  docker compose -f docker/docker-compose.yml up --build

容器里预装了哪些适配器 CLI

这是 Docker 部署相比裸机部署真正省事的地方:镜像预装了几个 agent CLI,对应的 *_local 适配器可以直接在容器里跑。文档站列的是四个:

  • claude(Anthropic Claude Code CLI)—— 对应 claude_local
  • codex(OpenAI Codex CLI)—— 对应 codex_local
  • opencode(OpenCode 多供应商 CLI)—— 对应 opencode_local
  • gemini(Google Gemini CLI)—— 对应 gemini_local,官方标注为实验性

注意 doc/DOCKER.md 里只写了 claudecodex 两个,文档站那份多列了 opencodegemini。适配器整体怎么选、每类的接入差异,见五类适配器分别怎么接

要让容器内的本地适配器真的能跑,得把 API key 传进去:

docker run --name paperclip \
  -p 3100:3100 \
  -e HOST=0.0.0.0 \
  -e PAPERCLIP_HOME=/paperclip \
  -e OPENAI_API_KEY=sk-... \
  -e ANTHROPIC_API_KEY=sk-... \
  -e GEMINI_API_KEY=... \
  -v "$(pwd)/data/docker-paperclip:/paperclip" \
  paperclip-local

每个适配器读的是各自供应商的标准凭据变量:Claude 用 ANTHROPIC_API_KEY,Codex 用 OPENAI_API_KEY,Gemini 用 GEMINI_API_KEYGOOGLE_API_KEY;OpenCode 是多供应商的,你给哪个 key 它就用哪个。

Gemini 这条有个官方专门标出来的限制:Google 要求 Gemini API key 必须在 Google Cloud 控制台里限定到 Gemini API,未受限的 key 会被拦截,gemini_local 的运行会以鉴权错误失败。官方给的两条出路是:要么建一个受限 key,要么用 gemini auth login 走 OAuth,并把 ~/.gemini 通过数据卷持久化,让凭据能扛过容器重启。

还有一个容器内特有的开关:镜像设了 GEMINI_SANDBOX=false,目的是让 Gemini CLI 不要在容器里再去起自己的 Docker-in-Docker 沙箱。官方说明 gemini_local 适配器每次运行本来就带 --sandbox=none,所以这个环境变量只在你手工进容器敲 gemini 时才有影响;如果你的环境支持嵌套容器、又想要 CLI 层的沙箱,可以覆盖它。

不给任何 API key 也没关系——官方明确写了应用照常运行,Paperclip 的适配器环境检查会把缺失的前置条件报出来。

认证部署:一个 PAPERCLIP_PUBLIC_URL 带出一串默认值

如果你不是本地自己玩,而是要挂个域名对外,doc/DOCKER.md 给的做法是设一个”权威公开 URL”,让 Paperclip 自己推导认证与回调相关的默认值:

services:
  paperclip:
    environment:
      PAPERCLIP_DEPLOYMENT_MODE: authenticated
      PAPERCLIP_DEPLOYMENT_EXPOSURE: private
      PAPERCLIP_PUBLIC_URL: https://<你的对外域名>

(官方示例里填的是文档作者自己的域名,这里换成占位符,实际填你自己那个。)

PAPERCLIP_PUBLIC_URL 会作为下面四项的主来源:认证公开基础 URL、Better Auth base URL 默认值、引导邀请(bootstrap invite)URL 默认值、主机名白名单默认值(主机名从这个 URL 里提取)。

首个管理员怎么产生,官方分了两种情况:

  • authenticated/private 的全新 Docker 或”设备式”安装,第一个管理员可以完全在浏览器里认领——打开 Paperclip 地址,登录或注册账号,然后在设置页选 Claim this instance
  • authenticated/public 下浏览器认领是禁用的,公开部署要走高熵 CLI 邀请这条兜底路径:
pnpm paperclipai auth bootstrap-ceo

细粒度的覆盖项仍然保留:PAPERCLIP_AUTH_PUBLIC_BASE_URLBETTER_AUTH_URLBETTER_AUTH_TRUSTED_ORIGINSPAPERCLIP_ALLOWED_HOSTNAMES。官方的建议是,只有当你需要公开 URL 主机名之外的额外主机名时(比如 Tailscale 或局域网别名、多个私有主机名)才显式设 PAPERCLIP_ALLOWED_HOSTNAMES

另外在 quickstart 那节也有一句相关提示:如果你改了宿主机端口,或者用的是非本地域名,就要设 PAPERCLIP_PUBLIC_URL 指向你在浏览器/认证流程里实际会用的外部地址。

Podman Quadlet:以 systemd 服务常驻

docker/quadlet/ 目录里放的是用 Podman Quadlet 把 Paperclip + PostgreSQL 跑成 systemd 服务的单元文件,三个文件各司其职:paperclip.pod 定义 pod(把容器归到共享网络命名空间),paperclip.container 是服务端(加入 pod,通过 127.0.0.1 连 Postgres),paperclip-db.container 是 PostgreSQL 17(加入 pod,带健康检查)。

安装步骤官方写了四步:先构建镜像;再把 quadlet 文件拷到 systemd 目录(rootless 推荐 ~/.config/containers/systemd/,rootful 是 /etc/containers/systemd/);然后建一个不入版本库的 secrets env 文件,里面放 BETTER_AUTH_SECRETPAPERCLIP_TOOL_ACTION_SIGNING_SECRET、Postgres 的用户名密码库名和 DATABASE_URL;最后建数据目录、systemctl --user daemon-reloadsystemctl --user start paperclip-pod

日常管理就是标准 systemd 那套:

journalctl --user -u paperclip -f        # 应用日志
journalctl --user -u paperclip-db -f     # 数据库日志
systemctl --user status paperclip-pod    # pod 状态
systemctl --user restart paperclip-pod   # 全部重启
systemctl --user stop paperclip-pod      # 全部停止

Quadlet 这条路有一个官方专门提醒的行为差异,值得提前知道,否则第一次冷启动会误判成故障:Docker Compose 有 condition: service_healthy,能等数据库真正就绪;而 Quadlet 的 After= 只等 DB 单元”启动”,不等 PostgreSQL 就绪。所以冷启动时你可能在 journalctl --user -u paperclip 里看到一两次重启尝试,官方说明这是预期内的,会靠 Restart=on-failure 自动收敛。

其余几条注意事项:同一 pod 内的容器共享 localhost,所以 Paperclip 走 127.0.0.1:5432 连 Postgres;PostgreSQL 数据存在 paperclip-pgdata 具名卷;Paperclip 数据存在 ~/.local/share/paperclip;如果做 rootful 部署,要去掉 %h 前缀改用绝对路径。

那个冒烟测试脚本,其实是给”验证部署”用的

scripts/docker-onboard-smoke.sh 容易被当成开发者内部工具跳过,但它解决的是一个很实际的问题:模拟一台只有 Ubuntu + npm 的干净机器,验证三件事——npx paperclipai onboard --yes 能跑完、服务端绑到 0.0.0.0:3100 因而宿主机能访问、onboard 与 run 的横幅和启动日志在终端里可见。

./scripts/docker-onboard-smoke.sh

默认宿主机端口是 3131(官方说明是为了避开本地 3100 上已有的 Paperclip),持久化数据默认挂在 ./data/docker-onboard-smoke,容器运行用户 id 默认取本机 id -u,让挂载的数据目录保持可写、同时避免以 root 运行。脚本默认走 authenticated/private 模式,这样 HOST=0.0.0.0 才能对宿主机暴露;它还会把 PAPERCLIP_PUBLIC_URL 默认设成 http://localhost:<HOST_PORT>,让引导邀请 URL 和认证回调用的是能访问到的宿主机端口,而不是容器内部的 3100

认证模式下脚本默认 SMOKE_AUTO_BOOTSTRAP=true,会真的把引导流程走一遍:注册一个真实用户、在容器里执行 paperclipai auth bootstrap-ceo 生成真实的引导邀请、通过 HTTP 接受这个邀请,最后验证 board 会话可访问。几个可用的覆盖变量:

HOST_PORT=3200 PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh
PAPERCLIP_DEPLOYMENT_MODE=authenticated PAPERCLIP_DEPLOYMENT_EXPOSURE=private ./scripts/docker-onboard-smoke.sh
SMOKE_DETACH=true SMOKE_METADATA_FILE=/tmp/paperclip-smoke.env PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh

官方建议前台运行以便观察 onboarding 流程,验证完 Ctrl+C 停掉;如果要留着给自动化用,设 SMOKE_DETACH=true,还可以用 SMOKE_METADATA_FILE 写出可直接 source 的元数据。镜像定义在 docker/Dockerfile.onboard-smoke

什么时候不适用,以及文档没有回答的问题

先说不适用的情形。如果你要接的是外部托管的数据库、或者要做多实例,quickstart 那条单容器路径就不是给这个场景准备的;官方在全栈 compose 和 Quadlet 里给的都是自带 PostgreSQL 17 容器的形态,接外部实例属于数据库文档的范围。

再说这两份文档明确没有回答的几件事,别自己脑补:

  • 资源需求:内存、CPU、磁盘占用,官方 Docker 文档一个数字都没给。
  • 缺少 BETTER_AUTH_SECRET 会怎样:文档站的 quickstart 命令没带它,doc/DOCKER.md 带了,两者差异的后果官方没写。
  • 内嵌数据库究竟是 SQLite 还是 PostgreSQL:如前所述,doc/DOCKER.md 里小标题和持久化清单自相矛盾,官方未澄清。
  • 升级路径:镜像怎么升、升级时 bind mount 里的数据要不要迁移,这两份文档都没涉及。
  • 反向代理与 TLS:只给了 PAPERCLIP_PUBLIC_URL 这一层的配置,前面挂 nginx/Caddy 该怎么配、超时和 WebSocket 转发有没有特殊要求,文档未说明。

最后一句实操建议:先用 quickstart 单容器把界面跑起来确认功能面,再决定是走全栈 compose 还是 Quadlet。这两条路的差别不在容器技术本身,而在你要不要一套能开机自启、有日志有状态查询的常驻服务——需要的话 Quadlet 那份单元文件已经写好了,代价是要接受冷启动那一两次预期内的重启。

延伸阅读


本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档 与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。 我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感; 部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。 请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。