Penpot Docker Compose 部署实战

Penpot 2.18.5 Docker Compose 部署实战:官方 8 服务编排定制(端口/子网/密钥/.env)、逐服务首启日志验证、prepare-register 两步注册实测(弱密码拒绝回显)、团队/项目/文件/画板创建,以及 exporter 全链路导出 SVG/PNG 实测与资源占用盘点(2026-10-28 实测)。

PenpotPenpot设计工具GUI2026/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 tag 2.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:80808140:8080本机 9001 已被占用,按端口规划表取 8140
PENPOT_PUBLIC_URIhttp://localhost:9001http://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/exportpenpot-exporter:6061导出服务 HTTP 入口
/assetspenpot-backend:6060/assets用户资产下载
/internal/assets本机 /opt/data/assets资产内部加速(X-Accel)
/admin-consolepenpot-admin-console:3000管理控制台
/mcp/ws、/mcp/streampenpot-mcpMCP(AI 智能体)通道
/github/penpot-files/*raw.githubusercontent.com官方库/模板拉取
/internal/gfonts/*fonts.googleapis.com / fonts.gstatic.comGoogle 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 校验错误形式出现,值得记录:

  1. 所有形状必须携带 points 与 transform/transform-inverse 字段,points 是 {"x":..,"y":..} map 数组,transform 是 {a,b,c,d,e,f} 六元仿射矩阵;缺任何一个都会被拒绝。
  2. 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-backend2.1 GiBJVM 大头;已显式 -Xmx512m 仍常驻 2GB 级(堆外+元空间),小内存宿主需预留
penpot-exporter461.7 MiB内置无头浏览器常驻
penpot-frontend172.9 MiBNginx + 静态资源
penpot-mcp190.6 MiBNode 服务
penpot-postgres167.7 MiB空库状态 12MB 数据
penpot-admin-console93.1 MiBNode 服务
penpot-mailpit32.5 MiB内存型邮箱(消息不落盘)
penpot-valkey4.2 MiB限 128MB 上限,实际极低

结论:Penpot 全栈建议至少 4GB 内存(其中 backend 单服务就要按 2.5GB 规划);2GB 机器跑不动官方默认栈。数据库初始仅 12MB,资产卷初始 36KB,存储压力随使用增长。

七、常见部署问题

  1. 8140 能打开但样式错乱/白屏:多为 PENPOT_PUBLIC_URI 与实际访问地址不一致。该值同时驱动前端资源加载与会话 Cookie 作用域,改完需整栈重启生效。
  2. 注册时 400 weak-password:见 4.1 实测回显,密码必须含大小写+数字+特殊字符;这不是配置项,是服务端硬策略。
  3. docker compose up 卡在 backend 等待:PostgreSQL 健康检查是 2 秒间隔×5 次,首次初始化磁盘慢时可能超时重试;观察 docker compose logs penpot-postgres 出现两次 ready to accept connections(初始化完成+正常启动)即可。
  4. 服务器重启后栈自愈:全服务 restart: always,实测宿主机重启后 8 服务自动拉起;但 admin-console 可能先于数据库就绪失败一轮再自动恢复(真实案例与日志见 《Penpot 常用命令与运维实战》排障实录)。
  5. 非 HTTPS 环境的功能缺失:官方默认旗标含 disable-secure-session-cookies(配合 HTTP 访问);若自行改成 secure-session-cookies 又不上 TLS,登录会话将无法建立。生产暴露前的旗标复核清单见 《Penpot 配置参数详解》。

八、参考链接