如何用 Docker Compose 自托管 Planka 看板
在 VPS 上用 Docker Compose 部署 Planka:配置 Postgres、Traefik 和管理员初始化变量,并修复会导致登录失败的 BASE_URL 设置问题。
自行托管 Planka 的收获
自行托管 Planka 后,团队可以获得一个 Kanban 看板。它采用用户熟悉的 Trello 卡片、列表和标签模型,并运行在您控制的 VPS 上。它不限制成员数量,也不按用户收费,因为唯一的成本是服务器。本指南使用 Docker Compose 在 Traefik 后部署 Planka,使用 Postgres 存储数据,并为用户上传的每个文件使用一个命名卷。
Planka 2 采用项目自己的 Fair Use License 发布。项目将其描述为可获取源代码,而不是开源软件。您可以将其用于组织内部项目。若要将其作为商业托管服务提供给第三方,则需要 Pro 或 Enterprise 许可证。
本指南面向刚要离开 Trello 免费版的 2 到 5 人团队。如果您还在决定使用哪个看板,请先阅读 自托管 Trello 替代方案对比。本指南假定您已经完成选择,只介绍部署过程。
您需要一台运行 Docker Engine 和 Compose 插件的 VPS,并为其配置一个指向该 VPS 的 DNS A 记录。您还需要在该服务器上已有一个负责终止 TLS(传输层安全)的 Traefik 实例。如果尚未部署 Traefik,请先设置 在多个 Compose 应用前部署 Traefik 反向代理;如果下面的文件不熟悉,请阅读 VPS 的 Docker Compose 基础知识。
Planka 需要多大的 VPS?
该项目没有公布最低硬件配置,因此应将你看到的任何数值视为起点,而不是实测结果。托管服务页面反复提到的 2 vCPU 和 4 GB,是服务商给出的宽松默认值,不是项目实测出的要求。对于只有 5 人使用的看板,这个配置较为充裕。
实际运行的组件很少:一个 Node.js 进程负责提供 API 和构建后的前端;一个 Postgres 服务器负责保存数据,并运行一个监管进程,以及每个打开连接对应的一个后端进程。Planka 容器内还会运行一个小型代理进程,用于过滤其发出的请求。1 vCPU 和 2 GB 的方案可以承载 2 到 5 人使用的看板,大部分空闲内存最终会作为 Postgres 缓存使用。看板对资源的占用较低,因此如果同一台 VPS 还要保存团队文档,应先按该应用的需求规划配置:运行 AFFiNE 作为 Notion 风格的工作区 在 Planka 需要资源之前,通常就需要单独占用几 GB 内存。
应先规划磁盘空间,再规划内存,因为增长最快的部分是附件。不要直接相信本段内容,应测量自己的实例:
docker stats --no-stream
docker system df -v第一个命令会显示每个容器的实时内存和 CPU 使用情况。第二个命令会显示每个卷占用的空间。应在正常工作一周后进行这两项测量,而不是在安装当天测量,因为空闲看板无法反映团队的实际使用情况。
编写 Compose 文件
创建目录并将其所有权交给当前用户,这样就不必通过 sudo 编辑这些文件。
sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/planka将密钥生成到 Compose 文件旁边的 .env 文件中。Compose 会自动读取该文件并替换其中的值。
umask 077
{
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 64)"
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)"
printf 'ADMIN_PASSWORD=%s\n' "$(openssl rand -hex 12)"
} > .env
chmod 600 .envopenssl rand -hex 是经过 deliberate 设计的。十六进制字符串只包含数字和 a 到 f 字母,因此不会破坏被粘贴到其中的 DATABASE_URL 连接字符串。包含斜杠或 at 符号的 base64 密码会导致连接错误,而错误信息看起来像是主机名错误,排查可能耗费 1 小时。更广泛的模式请参阅避免将密钥放入 Compose 文件。
现在执行 docker-compose.yml。将 kanban.example.com 替换为您自己的主机名,文件中出现的两处都要替换。
services:
planka:
image: ghcr.io/plankanban/planka:2.2.1
restart: unless-stopped
volumes:
- planka-data:/app/data
environment:
- BASE_URL=https://kanban.example.com
- DATABASE_URL=postgresql://planka:${POSTGRES_PASSWORD}@postgres/planka
- SECRET_KEY=${SECRET_KEY}
- TRUST_PROXY=true
- DEFAULT_ADMIN_EMAIL=you@example.com
- DEFAULT_ADMIN_PASSWORD=${ADMIN_PASSWORD}
- DEFAULT_ADMIN_NAME=Your Name
- DEFAULT_ADMIN_USERNAME=admin
networks:
- proxy
- internal
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.planka.rule=Host(`kanban.example.com`)"
- "traefik.http.routers.planka.entrypoints=websecure"
- "traefik.http.routers.planka.tls.certresolver=default"
- "traefik.http.services.planka.loadbalancer.server.port=1337"
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=planka
- POSTGRES_USER=planka
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U planka -d planka"]
interval: 10s
timeout: 5s
retries: 5
volumes:
planka-data:
db-data:
networks:
proxy:
external: true
internal:该文件中有 4 个决定需要说明,因为人们经常修改它们,之后又不得不回退。
- Planka 服务没有
ports:块。Traefik 通过proxy网络访问容器,因此不会在主机上发布 1337 端口。发布该端口会让任何人绕过代理和证书直接访问服务。 loadbalancer.server.port=1337指定容器内的端口。Planka 监听 1337 端口,而上游示例将该端口映射到主机,因此只能通过 3000 访问。这里没有主机端口映射,所以必须告知 Traefik 容器端口。condition: service_healthy与 Postgres 健康检查配合使用。没有它,Planka 会在数据库接受连接之前启动,首次查询失败后退出,看起来像是不断崩溃和重启。具体机制请参阅Compose 健康检查和启动顺序。- 数据库服务有意命名为
postgres。Planka 2 会通过内部过滤器转发自身发出的请求,其默认阻止列表为localhost,postgres。如果重命名该服务,数据库就会悄然从该列表中移除。
启动任何服务前,先确认 Compose 可以读取您的密钥:
docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'该命令会输出已替换 .env 值的文件。空值表示 Compose 没有读取 .env 文件,通常是因为您在其他目录中运行了该命令。
管理员引导变量的实际作用
从 Planka 1.13 开始,系统不会自动创建管理员,因此新数据库中没有任何用户可以登录。DEFAULT_ADMIN_* 组是解决此问题的两种方式之一。
启动时,Planka 会查找与 DEFAULT_ADMIN_EMAIL 匹配的用户。如果不存在匹配用户,Planka 会使用与该变量一起设置的密码、显示名称和用户名创建一个用户。这只会在首次使用空数据库启动时发生,因此这些变量用于引导创建账户,而不是管理账户。
DEFAULT_ADMIN_EMAIL 还有一个容易被忽略的作用。只要设置了该变量,任何人都无法通过界面编辑或删除它指定的账户。这是一个防锁定保护机制,也正因如此,您无法在界面中重命名该账户或更改其电子邮件地址。删除该变量并重启后,该账户就会变成普通管理员,您可以像编辑其他账户一样编辑它。
密码配置行需要特别注意。environment: 下的任何内容都可被能够在容器中运行 docker inspect 的用户读取,因此 DEFAULT_ADMIN_PASSWORD 不应永久保留在那里。登录后,在界面中更改密码,删除该行,然后再次运行 docker compose up -d。
更安全的方式是完全跳过这些变量。注释掉整个 DEFAULT_ADMIN_* 组,然后以交互方式创建账户:
docker compose run --rm planka npm run db:create-admin-user命令会依次提示输入电子邮件地址、密码、显示名称和可选的用户名,并直接将用户写入数据库。密码不会写入 Compose 文件或容器环境。如果有多个人可以通过 shell 访问 VPS,请使用这种方式。由于 depends_on,该命令会先启动 Postgres,因此即使整个堆栈从未启动过,也可以正常执行。
无论选择哪种方式,您都需要手动管理 Planka 密码。在 2.1.1 及更早版本中,Community 版可以将登录交给 OIDC 提供商,例如 作为您自己的单点登录服务器运行的 Authentik;但在 2.2.0 版本中,OIDC 和 SSO 已移至 PLANKA Pro。Community 版在同一版本中新增的是本地账户的双因素认证(TOTP),因此至少应为管理员账户启用该功能。
主机名不匹配时,BASE_URL 为什么会导致登录失败
BASE_URL 是用户在浏览器中输入的完整地址,包含协议,但不带末尾斜杠。对于此堆栈,该值为 https://kanban.example.com。Planka 会根据该值生成自己的链接和 WebSocket 连接。因此,错误的 BASE_URL 不会产生明确的错误,而是表现为页面可以加载,但始终无法完成加载。
常见情况是:复制上游示例,保留 BASE_URL=http://localhost:3000,然后通过真实域名使用 HTTPS 访问站点。登录表单可以提交,凭据也会通过验证,但看不到看板。打开浏览器开发者控制台后,会看到对 /socket.io/ 的请求失败。这是因为客户端被告知要连接到 localhost:3000,而该地址在你的笔记本上根本不存在。
TRUST_PROXY=true 是同一问题的另一部分。Planka 位于 Traefik 后面,因此每个请求都会从代理地址通过 Docker 网络中的普通 HTTP 到达 Planka。如果没有设置 TRUST_PROXY,应用会忽略 Traefik 设置的 X-Forwarded-Proto 和 X-Forwarded-For 请求头,因此会认为连接不安全,并将所有客户端视为同一个 IP 地址。设置该参数后,应用会读取这些请求头,并与浏览器使用相同的协议。
Traefik 无需额外配置即可代理 WebSocket,这也是此处更适合使用 Traefik 的原因之一。在 nginx 中,代理必须转发升级请求头 proxy_set_header Upgrade $http_upgrade 和 proxy_set_header Connection "upgrade",否则会因另一种原因出现同样卡住的加载指示器。它们可以放在通用的 location / 块中;单独为 /socket.io 配置一个块也可以,但不是必需的。
以后将看板迁移到新主机名时,必须同时修改两项:BASE_URL 的值和 Traefik 的 Host() 规则。只修改其中一项而忘记另一项,就会再次出现加载指示器卡住的问题。从版本 2.1.0 开始,Planka 支持通过 https://example.com/planka 这样的子路径提供服务,该版本于 March 2026 发布。在更早的标签版本中,请为其使用独立的子域名。
Planka 如何存储附件和头像
Planka 2 会将用户上传的所有内容存储在容器内的单一路径下:/app/data。附件、用户头像和看板背景图片都存储在此路径中。Version 1 使用 3 个独立目录,因此从旧教程复制的 Compose 文件会挂载已经不存在的路径,而实际数据目录则未被挂载。
这个挂载点决定了看板能否在升级后保留数据。如果 /app/data 未挂载到 volume,上传的文件就会写入容器的可写层。重新创建容器时,该层会被销毁;每次修改 image tag 时,容器都会重新创建。看板恢复后表面上看起来正常,卡片也都在,但所有附件链接都会失效,因为数据库记录仍指向已经不存在的文件。
上面的 Compose 文件使用 named volume,可以避免此问题。bind mount 同样可用,并且便于使用普通工具备份文件,但还需要执行一个额外步骤。容器内的 Node 进程以 UID 1000 运行,因此如果主机目录归 root 所有,首次上传时会出现权限错误:
sudo chown -R 1000:1000 /opt/planka/data两者之间的取舍详见bind mount 与 named volume 的比较。
如果附件占用的空间超过套餐提供的磁盘容量,Planka 也可以通过 S3_ENDPOINT、S3_BUCKET 及对应的密钥变量,将附件写入兼容 S3 的存储。目标可以是托管 bucket,也可以是另一台主机上的自托管 MinIO 对象存储。应在团队填满看板前决定是否采用此方案,因为该设置只对新上传的文件生效。
启动服务栈并确认运行正常
docker compose pull
docker compose up -d
docker compose psdocker compose ps 应显示 postgres 为 healthy,并显示 planka 为 running。如果 Planka 反复重启,首先应检查数据库连接,而不是应用本身。
docker compose logs -f planka首次正常启动时,系统会运行数据库迁移,然后报告服务器正在监听端口 1337。请直接查询 Postgres,确认数据库架构确实已写入,不要只依赖日志:
docker compose exec postgres psql -U planka -d planka -c '\dt'如果表列表包含 board 和 card,说明迁移已运行。“Did not find any relations”表示 Planka 从未连接到数据库。请将 DATABASE_URL 与 .env 中的 POSTGRES_USER 和 POSTGRES_PASSWORD 值进行比较。
然后从您自己的计算机检查路由,不要从 VPS 检查:
curl -I https://kanban.example.comHTTP/2 200 表示 Traefik 已持有证书,并且能够访问容器。由 Traefik 返回的 404 表示没有路由器匹配请求:Host() 规则与您请求的主机名不匹配,或者 Traefik 从未读取到这些标签。502 表示路由器已匹配,但 Traefik 无法访问容器,例如容器未连接到 proxy 网络。现在打开网站,并使用 admin 账户登录。
每次升级版本前执行 pg_dump
看板数据存储在两个独立位置,因此备份必须同时覆盖 Postgres 数据库和 planka-data 卷。堆栈运行时导出数据库。
docker compose exec -T postgres pg_dump -U planka -d planka > "planka-db-$(date +%F).sql"-T 不是可选项。不使用它时,Compose 会分配伪终端,终端层会改写数据流中的换行符,导致导出文件在恢复过程中途失败。这个问题可能在几周后才暴露,而那通常是最不合适的时候。
然后备份上传文件。先查找实际的卷名称,因为 Compose 会在卷名前加上项目目录名称。
docker volume ls | grep planka
docker run --rm -v planka_planka-data:/data -v "$PWD":/backup alpine \
tar czf /backup/planka-files-$(date +%F).tgz -C /data .该项目还在其代码仓库中提供 docker-backup.sh 和 docker-restore.sh,官方文档建议通过 nightly cron job 执行它们。两种方式都可以。不能接受的是从未进行过恢复验证的备份。因此,应将备份恢复到一次性 VPS 上,确认可以登录并打开附件。同样的两个存储位置会出现在所有接受上传的 Compose 应用中。因此,如果以后将 Chatwoot 与支持工单系统部署在同一台服务器上,这里只需更改卷名称,建立的流程基本就能直接复用。
每次更改版本前都要立即执行导出。昨晚的备份不等于即将执行迁移前的备份。
固定镜像标签并阅读发行说明
该文件中的两个镜像标签都是有意固定的。
ghcr.io/plankanban/planka:2.2.1 是截至 2026 年 10 月的特定发行版。latest 会随着上游发布新版本而变化,因此例行执行 docker compose pull 可能在你未选择的时间引入架构迁移。修改该版本号前,请先阅读发行说明,因为其中会说明不兼容变更和安全修复。2.0.3 作为安全版本发布,这正是应主动阅读、而不是意外引入的更新类型。2026 年 8 月发布的 2.2.1 也是如此:它修复了路径遍历漏洞。该漏洞允许已登录用户读取使用本地文件存储的实例中的任意文件,而本指南采用的正是这种配置。安全公告将 2.1.1 及更早版本列为受影响版本。如果你从 2.1.x 升级,请先阅读 2.2.0 的说明,因为该版本会停用仅通过 SSO 登录过的用户,直到管理员为其设置密码。这里可以轻松固定版本,因为上游会发布镜像。如果项目不发布镜像,也应遵循同样的规范,只是需要额外执行一步,例如在主机上从已检出的 git 标签构建 openGym。
固定 postgres:16-alpine 的主版本有更重要的原因。Postgres 会以与主版本绑定的格式写入数据目录,服务器拒绝打开由其他主版本写入的目录。写入 postgres:latest,让标签滚动到 16 之后,容器将无法启动。例如,17 镜像会停止并显示:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.数据不会丢失,重启也无法解决问题。迁移到新的 Postgres 主版本,必须先从旧版本执行转储,再将其恢复到新版本的全新数据目录;或者执行 pg_upgrade,直接转换数据而不进行转储,但这需要同时提供新旧版本的服务器二进制文件。无论采用哪种方式,都应在计划停机期间完成,而不是让它成为拉取镜像的副作用。
如果你要迁移现有的 Planka 1.x 安装,而不是全新安装,请按照项目文档中记录的专用升级流程操作。没有事先创建的备份,就无法回退到版本 1。
故障模式及日志中会出现的字符串
Planka 反复重启,日志中提到数据库。 DATABASE_URL 中的凭据与 Postgres 环境变量不匹配。请注意,POSTGRES_PASSWORD 仅在首次初始化数据目录时应用,因此首次启动失败后再修复该变量不会产生任何效果。您必须删除 db-data 卷,然后重新启动。
登录成功,但看板始终无法加载。 BASE_URL 与浏览器地址栏中的地址不匹配,或者缺少 TRUST_PROXY。浏览器控制台会显示对 /socket.io/ 的请求失败。
其他功能正常,但上传失败。 绑定挂载的所有者是 root。在主机上对该目录运行 sudo chown -R 1000:1000,然后重启容器。
升级后附件消失。 /app/data 未使用卷,因此文件位于容器层中,而升级替换了该层。请从备份恢复文件,然后在再次修改镜像标签前添加该卷。
Traefik 返回 404。 没有路由匹配该请求:Host() 规则与您的 DNS 记录不匹配,或者标签根本没有传递到 Traefik。docker compose config 会显示替换变量后的标签,拼写错误可在此处发现。返回 502 则表示路由已匹配,但无法访问容器,因此请检查容器是否加入 proxy 网络。
通知或 Webhook 始终无法送达。 Planka 2 通过内部过滤器发送出站 HTTP 请求,默认阻止列表包含 localhost 和 postgres。指向 localhost 的 Webhook 会按设计被阻止;设置 OUTGOING_ALLOWED_HOSTS 后,不在该列表中的所有主机也会被阻止。如果使用允许列表,请将 Webhook 指向另一个容器的服务名,并将该名称添加到 OUTGOING_ALLOWED_HOSTS,不要移除过滤器。
服务运行后,日常维护负担很小。请关注版本说明,并在每次升级前导出数据库。由于 restart: unless-stopped,主机重启后整个服务栈会自动恢复,前提是 Docker 服务已设置为开机启动;如果无法自动恢复,请参阅 重启后自动恢复的 Compose 服务栈。
FAQ
为什么我登录后 Planka 一直加载?
凭据已通过验证,但实时连接未建立。Planka 根据 BASE_URL 构建 WebSocket URL。因此,如果该变量仍为 http://localhost:3000,而您通过 https://kanban.example.com 访问网站,浏览器就会尝试连接到本机不存在的地址。开发者控制台会显示对 /socket.io/ 的请求失败。将 BASE_URL 设置为不带末尾斜杠的准确公网地址,添加 TRUST_PROXY=true 让应用遵循反向代理发送的 X-Forwarded-Proto 头,然后运行 docker compose up -d。
如何创建第一个 Planka 管理员用户?
从 1.13 版本开始,系统不会自动创建管理员。您可以设置 DEFAULT_ADMIN_EMAIL 及其匹配的密码、姓名和用户名变量,然后启动堆栈;也可以运行 docker compose run --rm planka npm run db:create-admin-user 并按提示操作。在共享服务器上,交互式命令更安全,因为密码不会进入容器环境,docker inspect 可以在其中读取该密码。之后保留 DEFAULT_ADMIN_EMAIL 的设置,可禁止通过界面编辑或删除该账户。
Planka 将附件和头像存储在哪里?
在 Planka 2 中,所有上传的文件都存储在容器内的 /app/data 下,包括附件、用户头像和看板背景。将此路径挂载到命名卷。如果未挂载,文件会保存在容器的可写层中;下次重新创建容器时,这些文件就会被删除,而每次升级镜像都会重新创建容器。绑定挂载也可以使用,但 Node 进程以 UID 1000 运行,因此请在主机目录上运行 sudo chown -R 1000:1000,否则上传会因权限错误失败。
自托管 Planka 需要多少 RAM?
项目没有公布最低硬件配置。托管页面反复提到的 2 vCPU 和 4 GB 是提供商的默认配置,不是实测结果;对于小型看板而言,这一配置较为宽裕。整个工作负载只有一个 Node 进程和一个 Postgres 服务器。Postgres 会为每个打开的连接启动一个小型后端进程,因此 1 vCPU 和 2 GB 的方案可以支持 2 到 5 人的团队。正常运行一周后执行 docker stats --no-stream,再根据您自己的数据确定配置。相比内存,应更密切关注磁盘,因为增长最快的是附件。
如何升级 Planka 而不丢失数据?
应在升级前立即转储数据库并归档上传文件卷,不要依赖昨晚的计划任务。使用 docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql,保留 -T,以免伪终端破坏重定向的输出。阅读每个跳过版本的发布说明,将镜像标签改为具体版本,不要使用 latest,然后运行 docker compose pull 和 docker compose up -d,并监控日志中的迁移过程。将 Postgres 标签固定在其主版本上,因为服务器拒绝打开由其他主版本写入的数据目录。