Penpot Docker Compose 部署实战
Penpot 2.18.5 Docker Compose 部署实战:官方 8 服务编排定制(端口/子网/密钥/.env)、逐服务首启日志验证、prepare-register 两步注册实测(弱密码拒绝回显)、团队/项目/文件/画板创建,以及 exporter 全链路导出 SVG/PNG 实测与资源占用盘点(2026-10-28 实测)。
Penpot Docker Compose 部署实战
目标:在一台 Linux 服务器上,用官方 Docker Compose 编排完整跑起 Penpot 2.18.5,完成首启验证、账号初始化、团队/文件/画板创建,并实测导出服务(exporter)的 SVG/PNG 全链路。本文全部命令与回显均为 2026-10-28 真实服务器实测记录;产品定位与选型背景见 《Penpot 介绍与选型指南》,配置参数逐项解释见 《Penpot 配置参数详解》,日常运维与排障见 《Penpot 常用命令与运维实战》。
一、环境信息
- 服务器:Linux 宿主机(Docker 26+,模拟真实生产服务器),测试日期 2026-10-28。
- Penpot 版本:2.18.5(
PENPOT_VERSION=2.18.5固定版本;官方 2.18.5 编排文件于同日取自 GitHub tag2.18.5)。 - 一个值得记录的实查细节:本栈构建串为
2.18.5-RC1-1793006488,而 GitHub tags 实查显示2.18.5与2.18.5-RC1指向同一提交(f4456696)——即 2.18.5 正式版镜像与 RC1 为同一构建,见到-RC1字样不必怀疑拿错版本。 - 部署目录:
/opt/penpot/deploy/(docker-compose.yaml+.env两个文件)。 - 访问入口:
http://<服务器IP>:8140(在官方默认 9001 基础上按本机端口规划改为 8140)。 - 官方编排定义 8 个服务:penpot-frontend(Nginx 统一入口)、penpot-backend(业务后端)、penpot-exporter(导出渲染)、penpot-admin-console(管理控制台)、penpot-mcp(AI 智能体接口)、penpot-postgres(PostgreSQL 15 主库)、penpot-valkey(通知/队列协调)、penpot-mailpit(测试邮箱)。
二、部署规划
2.1 编排文件获取与定制点
按官方文档获取编排文件后,针对本机环境做了 6 处定制(与官方 tag 2.18.5 原文逐行 diff 确认,其余保持原样):
mkdir -p /opt/penpot/deploy && cd /opt/penpot/deploy
wget https://raw.githubusercontent.com/penpot/penpot/2.18.5/docker/images/docker-compose.yaml
| 定制点 | 官方默认 | 本例值 | 定制原因 |
|---|---|---|---|
name: | 无(目录名即项目名) | penpot-2.18.5-stack | 固定项目名,避免目录改名导致卷漂移 |
ports(frontend) | 9001:8080 | 8140:8080 | 本机 9001 已被占用,按端口规划表取 8140 |
PENPOT_PUBLIC_URI | http://localhost:9001 | http://localhost:8140 | 必须与实际访问地址一致,否则会话/邀请链接指向错误域名 |
PENPOT_SECRET_KEY | 明文 change-this-insecure-key | ${PENPOT_SECRET_KEY:-...} 读 .env | 密钥不入编排文件 |
| 网络 | 默认桥接 | 显式 subnet: 10.89.68.0/24 | 本机已占用多段 10.89.x.0/24,显式声明避免冲突 |
mailpit ports | "8025:8025" | "127.0.0.1:8148:8025" | 测试邮箱仅本机访问,且避开宿主机 8025 |
另有一处追加:为 penpot-backend 显式加 JVM_OPTS: "-Xms256m -Xmx512m",把后端 JVM 堆上限压到 512MB(实测默认堆配置下后端常驻内存约 2.1GiB,小内存服务器建议显式限制,详见第六章)。
2.2 .env 文件
# /opt/penpot/deploy/.env
PENPOT_VERSION=2.18.5
PENPOT_SECRET_KEY=DPZmcz4JrIHp2I8ziPhB3LCbLm0dNN7B9zyFDKNQQu1IWrqbXWRhqeeXvIq1jb9OdxsGipMa2-b3mMl12dJP5Q
PENPOT_SECRET_KEY 用官方文档建议的命令生成(512 位随机串):
python3 -c "import secrets; print(secrets.token_urlsafe(64))"
⚠️ 注意:这是会话与邀请体系的派生主键,一旦投入生产使用就不可更换(换了之后所有登录会话失效),务必随
.env一起纳入备份。官方默认编排里的change-this-insecure-key仅适用于本机试玩。
2.3 服务拓扑与启动依赖
官方编排的启动依赖关系(depends_on 实查):
penpot-frontend ──依赖──> penpot-backend / penpot-admin-console / penpot-mcp / penpot-exporter
penpot-backend ──(condition: service_healthy)──> penpot-postgres / penpot-valkey
penpot-admin-console ──(service_healthy)──> penpot-postgres
penpot-exporter ──(service_healthy)──> penpot-valkey
penpot-mcp ──无依赖(独立启动)
penpot-mailpit ──无依赖(独立启动)
PostgreSQL 与 Valkey 均带健康检查(pg_isready -U penpot / valkey-cli ping),后端与管理控制台会等数据库健康后才启动;导出服务等 Valkey 健康。这个顺序决定了两个运维特性:数据库最晚 2 秒间隔探活 5 次,以及后端启动日志中一定会先出现迁移(migrations)再出现 HTTP 监听。
三、部署实施
3.1 启动与首启日志验证
cd /opt/penpot/deploy
docker compose up -d
实测启动耗时约 1 分钟(首次需拉取 8 组组件数据)。逐服务核对首启日志:
penpot-postgres(初始化 + 建库):
penpot-postgres-1 | 2026-10-27 17:31:58.397 UTC [48] LOG: database system is ready to accept connections
penpot-postgres-1 | 2026-10-27 17:31:58.547807867Z CREATE DATABASE
penpot-postgres-1 | 2026-10-27 17:31:58.668 UTC [1] LOG: database system is ready to accept connections
官方编排带 POSTGRES_INITDB_ARGS=--data-checksums,首次初始化即开启数据校验和。
penpot-backend(迁移 → 监听 → 欢迎横幅):
penpot-backend-1 | [2026-10-27 17:33:21.302] I app.migrations - hint="running migrations", module=:app.migrations/migrations
penpot-backend-1 | [2026-10-27 17:33:22.378] I app.http - hint="starting http server", port=6060, host="0.0.0.0"
penpot-backend-1 | [2026-10-27 17:33:22.488] I app.main - hint="welcome to penpot", flags="backend-openapi-doc,dashboard-templates-section,token-shadow,stroke-path,feature-render-wasm,login-with-password,google-fonts-provider,exporter-svgo,background-blur,render-switch,backend-api-doc,render-wasm-info,backend-svgo,export-file-v3,available-viewer-wasm,token-color,token-import-from-library,feature-fdata-objects-map,inspect-styles,onboarding,admin-console,backend-worker,telemetry,render-wasm-dpr,mcp,smtp,component-thumbnails,token-combobox,registration,prepl-server,token-typography-row,frontend-svgo", worker?=true, version="2.18.5-RC1"
welcome to penpot 一行是最有用的单行健康证据:flags 列表把当前生效旗标全部打印(含 mcp、admin-console、smtp、registration),version 显示实际版本。配置排障时先看这一行(旗标不生效的排查见 《Penpot 配置参数详解》)。
penpot-exporter(等待无头浏览器就绪):
penpot-exporter-1 | INF ... [app.browser] origin="factory", action="create", browser-id=1
penpot-exporter-1 | INF ... hint="welcome to penpot", module="exporter", version="2.18.5-RC1"
导出服务首启要拉起内置无头浏览器,日志先出现 browser factory create 再出现欢迎横幅;若浏览器未就绪就收到导出请求,接口会返回 503(排障实录见 《Penpot 常用命令与运维实战》)。
3.2 启动结果确认
docker compose ps
实测回显(8 个服务全部 Up):
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
penpot-218-stack-penpot-admin-console-1 penpotapp/admin-console:2.18.5 "docker-start-admin…" penpot-admin-console 2 hours ago Up 2 hours
penpot-218-stack-penpot-backend-1 penpotapp/backend:2.18.5 "/bin/bash run.sh" penpot-backend 2 hours ago Up 2 hours
penpot-218-stack-penpot-exporter-1 penpotapp/exporter:2.18.5 "/opt/entrypoints/to…" penpot-exporter 2 hours ago Up 2 hours
penpot-218-stack-penpot-frontend-1 penpotapp/frontend:2.18.5 "/bin/bash /entrypoi…" penpot-frontend 2 hours ago Up 2 hours 0.0.0.0:8140->8080/tcp, [::]:8140->8080/tcp
penpot-218-stack-penpot-mailpit-1 axllent/mailpit:v1 "/mailpit" penpot-mailpit 2 hours ago Up 2 hours 127.0.0.1:8148->8025/tcp
penpot-218-stack-penpot-mcp-1 penpotapp/mcp:2.18.5 "node dist/cli.js" penpot-mcp 2 hours ago Up 2 hours
penpot-218-stack-penpot-postgres-1 postgres:15 "docker-entrypoint.s…" penpot-postgres 2 hours ago Up 2 hours (healthy)
penpot-218-stack-penpot-valkey-1 valkey/valkey:8.1 "docker-entrypoint.s…" penpot-valkey 2 hours ago Up 2 hours (health…)
浏览器访问 http://<服务器IP>:8140 出现 Penpot 登录/注册页即部署成功。前端监听 8140(映射到内部 8080),mailpit 监听 127.0.0.1:8148。
3.3 统一入口路由(Nginx 层实查)
frontend 内置 Nginx 把所有子系统收拢到一个端口下,实测路由表:
| 路径 | 转发目标 | 用途 |
|---|---|---|
/ | 前端静态资源 | Web 工作台 |
/api/* | penpot-backend:6060 | 业务 API/RPC |
/api/export | penpot-exporter:6061 | 导出服务 HTTP 入口 |
/assets | penpot-backend:6060/assets | 用户资产下载 |
/internal/assets | 本机 /opt/data/assets | 资产内部加速(X-Accel) |
/admin-console | penpot-admin-console:3000 | 管理控制台 |
/mcp/ws、/mcp/stream | penpot-mcp | MCP(AI 智能体)通道 |
/github/penpot-files/* | raw.githubusercontent.com | 官方库/模板拉取 |
/internal/gfonts/* | fonts.googleapis.com / fonts.gstatic.com | Google Fonts 代理 |
这意味着只暴露 8140 一个端口即可(mailpit 的 8148 是调试用额外通道);反向代理/TLS 也只需代理这一个端口。
四、初始化:注册与首建内容
4.1 注册首个账号(含密码策略实测)
浏览器进入 http://<服务器IP>:8140,选择「Create account」注册。也可以走 API(与本例实测一致)。实测中最有价值的一课是密码策略:先试了批次标准示例密码 q1w2e3r4,服务端直接拒绝:
HTTP 400
["^ ","~:type","~:validation","~:code","~:weak-password","~:hint","password must contain at least 1 lowercase letter, 1 uppercase letter, 1 digit, and 1 special character","~:details",["errors.weak-password.insufficient-uppercase","errors.weak-password.insufficient-special"]]
即 Penpot 要求密码同时包含小写、大写、数字、特殊字符。改用强化变体 Q1w2e3r4! 通过。API 完整注册是「两步流」:
第一步 prepare-register-profile 换取一次性 token(本步就会触发密码强度校验):
curl -s -X POST http://localhost:8140/api/main/methods/prepare-register-profile \
-H 'Content-Type: application/json' \
-d '{"fullname":"root","email":"root@penpot.local","password":"Q1w2e3r4!"}'
第二步拿 token 完成注册:
curl -s -X POST http://localhost:8140/api/main/methods/register-profile \
-H 'Content-Type: application/json' \
-d '{"token":"<第一步返回的 token>"}'
实测成功回显(截取):
HTTP 200
["^ ","~:email","root@penpot.local","~:is-demo",false,"~:auth-backend","penpot","~:fullname","root", "~:is-active",true,"~:default-project-id","~uc796c070-...","~:id","~uc796c070-...","~:default-team-id","~uc796c070-...",...]
💡 技术拓展:官方编排默认旗标含
disable-email-verification,所以注册无需邮箱验证即可直接登录;一旦生产启用邮件验证旗标,注册会改为「邮件确认」流,未配 SMTP 时用户会卡在验证环节。
本例共注册两个账号用于测试:admin@penpot.local / Q1w2e3r4!(建团队 ops-demo)与 root@penpot.local / Q1w2e3r4!(使用默认团队)。登录同样走 API 一发即中:
POST /api/main/methods/login-with-password {"email":"root@penpot.local","password":"Q1w2e3r4!"}
HTTP 200 ["^ ","~:is-admin",false,"~:email","root@penpot.local",...]
4.2 建项目 / 文件 / 画板
登录后按 UI 流程:创建团队 → 团队内创建项目 → 项目内新建设计文件。本例实测用 API 创建了用于导出测试的文件「导出实测看板」:
curl -s -b cookies.txt -X POST http://localhost:8140/api/main/methods/create-file \
-H 'Content-Type: application/json' \
-d '{"name":"导出实测看板","projectId":"c796c070-6bc0-807a-8008-db53269301dd"}'
# HTTP 200,返回文件 id 与页面 id
随后通过 update-file 的 add-obj 变更往默认页写入一个 800×600 画板(frame,命名 DemoBoard)和一个 200×120 矩形(rect,填充 #4F46E5)。这里踩到两个真实坑,均以服务端 spec 校验错误形式出现,值得记录:
- 所有形状必须携带
points与transform/transform-inverse字段,points 是{"x":..,"y":..}map 数组,transform 是{a,b,c,d,e,f}六元仿射矩阵;缺任何一个都会被拒绝。 selrect必须与 x/y/width/height 一致(x1/y1/x2/y2四角坐标),只给 width/height 会报 spec 错误。
写入成功后 update-file 返回 HTTP 200 与新的 revn 版本号(Penpot 用 revn 做文件版本递增,后续所有变更都要带上最新值)。
五、部署验证:导出全链路实测
部署是否真正可用,最有说服力的验证是把画板导出成文件——这会同时穿过 frontend → exporter → backend 资产存储三层。实测导出 PNG/SVG 全链路:
curl -s -b cookies.txt -X POST "http://localhost:8140/api/export" \
-H 'Content-Type: application/transit+json' \
-d '{"~:cmd":"~:export-shapes","~:profile-id":"<profile-id>","~:wait":true,
"~:exports":[{"~:file-id":"<file-id>","~:page-id":"<page-id>",
"~:object-id":"<frame-id>","~:type":"~:png",
"~:suffix":"","~:scale":2,"~:name":"demo-board"}]}'
实测返回(截取):
HTTP 200
{"~:mtype":"image/png","~:name":"demo-board","~:filename":"demo-board.png","~:id":"~u<资产id>","~:uri":{"~#uri":"http://localhost:8140/assets/by-id/ee883710-5859-495d-bb65-a93731be5196"}}
按返回的 uri 下载产物并用 file 验证:
download: 200 image/png 10187B
exported_demo-board.png: PNG image data, 1600 x 1200, 8-bit/color RGBA, non-interlaced
800×600 画板 × scale=2 → 1600×1200 PNG,尺寸换算符合预期。同参数 ~:type 换 ~:svg 得到 1271B 的 SVG(内容为标准 <svg> 文档,image/svg+xml)。导出期间 exporter 侧实测日志(无头浏览器渲染证据):
penpot-exporter-1 | INF [app.renderer.bitmap] uri=#lambdaisland/uri "http://penpot-frontend:8080/render.html?file-id=c796c070-...&page-id=c796c070-...&object-id=c796c070-...&route=objects"
penpot-exporter-1 | TRC [app.util.shell] hint="schedule tempfile deletion", path="/tmp/penpot/penpot.tmp.bitmap.42fb1840-...-0.png", scheduled-at="2026-10-28T03:12:26Z"
可以看清导出的工作机制:exporter 用内置无头浏览器打开 frontend 的 render.html 渲染页截图 → 生成临时文件( /tmp/penpot/ 下,带定时删除)→ 上传为资产(/assets/by-id/)→ 返回下载地址。这就是 exporter 体积最大(约 2.5GB)且内存常驻几百 MB 的原因。
六、资源占用盘点
实测稳定态内存占用(docker stats --no-stream,宿主内存 19.48GiB):
| 服务 | 内存占用 | 说明 |
|---|---|---|
| penpot-backend | 2.1 GiB | JVM 大头;已显式 -Xmx512m 仍常驻 2GB 级(堆外+元空间),小内存宿主需预留 |
| penpot-exporter | 461.7 MiB | 内置无头浏览器常驻 |
| penpot-frontend | 172.9 MiB | Nginx + 静态资源 |
| penpot-mcp | 190.6 MiB | Node 服务 |
| penpot-postgres | 167.7 MiB | 空库状态 12MB 数据 |
| penpot-admin-console | 93.1 MiB | Node 服务 |
| penpot-mailpit | 32.5 MiB | 内存型邮箱(消息不落盘) |
| penpot-valkey | 4.2 MiB | 限 128MB 上限,实际极低 |
结论:Penpot 全栈建议至少 4GB 内存(其中 backend 单服务就要按 2.5GB 规划);2GB 机器跑不动官方默认栈。数据库初始仅 12MB,资产卷初始 36KB,存储压力随使用增长。
七、常见部署问题
- 8140 能打开但样式错乱/白屏:多为
PENPOT_PUBLIC_URI与实际访问地址不一致。该值同时驱动前端资源加载与会话 Cookie 作用域,改完需整栈重启生效。 - 注册时 400 weak-password:见 4.1 实测回显,密码必须含大小写+数字+特殊字符;这不是配置项,是服务端硬策略。
docker compose up卡在 backend 等待:PostgreSQL 健康检查是 2 秒间隔×5 次,首次初始化磁盘慢时可能超时重试;观察docker compose logs penpot-postgres出现两次ready to accept connections(初始化完成+正常启动)即可。- 服务器重启后栈自愈:全服务
restart: always,实测宿主机重启后 8 服务自动拉起;但 admin-console 可能先于数据库就绪失败一轮再自动恢复(真实案例与日志见 《Penpot 常用命令与运维实战》排障实录)。 - 非 HTTPS 环境的功能缺失:官方默认旗标含
disable-secure-session-cookies(配合 HTTP 访问);若自行改成secure-session-cookies又不上 TLS,登录会话将无法建立。生产暴露前的旗标复核清单见 《Penpot 配置参数详解》。
八、参考链接
- 官方 Docker 部署文档:https://help.penpot.app/technical-guide/getting-started/docker/
- 官方 2.18.5 编排文件(本文定制基线):https://github.com/penpot/penpot/blob/2.18.5/docker/images/docker-compose.yaml
- 配置参数总表:https://help.penpot.app/technical-guide/configuration/
- 站内系列:《Penpot 介绍与选型指南》 · 《Penpot 配置参数详解》 · 《Penpot 常用命令与运维实战》