Paperclip 用 Docker 部署:四条路径、两个必生成的密钥和数据落在哪
想用 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 quickstart | docker/docker-compose.quickstart.yml | 单容器内嵌,无外部数据库 | 文档站标为 Recommended |
| 手工 build + run | docker build + docker run | 同上(走 bind mount) | 需要自己控制参数时用 |
| 全栈 compose | docker/docker-compose.yml | 独立 PostgreSQL 17 容器 | 服务端 + 数据库分离 |
| Podman Quadlet | docker/quadlet/*.pod、*.container | PostgreSQL 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 工具:git、gh、curl、wget、ripgrep、python3,以及 Claude、Codex、OpenCode 三个 CLI。
镜像提供两个 build 参数:
| 参数 | 默认值 | 作用 |
|---|---|---|
USER_UID | 1000 | 容器内 node 用户的 UID,建议与宿主机 UID 一致,避免 bind mount 上的权限问题 |
USER_GID | 1000 | 容器内 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_localcodex(OpenAI Codex CLI)—— 对应codex_localopencode(OpenCode 多供应商 CLI)—— 对应opencode_localgemini(Google Gemini CLI)—— 对应gemini_local,官方标注为实验性
注意 doc/DOCKER.md 里只写了 claude 和 codex 两个,文档站那份多列了 opencode 和 gemini。适配器整体怎么选、每类的接入差异,见五类适配器分别怎么接。
要让容器内的本地适配器真的能跑,得把 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_KEY 或 GOOGLE_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_URL、BETTER_AUTH_URL、BETTER_AUTH_TRUSTED_ORIGINS、PAPERCLIP_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_SECRET、PAPERCLIP_TOOL_ACTION_SIGNING_SECRET、Postgres 的用户名密码库名和 DATABASE_URL;最后建数据目录、systemctl --user daemon-reload、systemctl --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 是什么:一个自己不跑 Agent 的控制平面,怎么管住一整家 AI 公司
- 本专题共 40 篇,完整分组目录见专题页
- Paperclip 数据库怎么配:内嵌 Postgres、本机 Docker 与外接托管库的取舍
- Paperclip 环境变量怎么读:分清必填、部署模式相关和可选调优三组
本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档
与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。
我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感;
部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。
请以仓库最新内容为准。
相关阅读
- Paperclip 的 skill 怎么写:SKILL.md 格式、信任等级与技能库的完整流程
- Paperclip 仪表盘与状态卡片怎么读:五组指标的口径、数据来源与刷新机制
- Macro 的技术选型:Rust 后端 + Solid 前端 + Loro CRDT,这套组合带来哪些工程约束
- Macro MCP 内容类工具怎么用:ReadContent、ReadMetadata 与 CreateDocument 的参数与陷阱
- Macro 是什么:邮件、任务、文档、CRM 共用一个双向数据库的开源工作区
- Macro 团队与成员管理:三种拉人方式、Member/Admin/Owner 三档角色与移除成员时收回什么