SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-28

Planka 自托管部署:Docker Compose 配置指南

使用 Docker Compose 在 VPS 上部署 Planka,配置 Postgres、Traefik 和管理员初始化变量,并修复 BASE_URL 设置错误导致的登录失败。

自行托管 Planka 的收益

自行托管 Planka 后,您的团队可以在自己控制的 VPS 上使用 Kanban 看板,继续采用用户已经熟悉的 Trello 卡片、列表和标签模型。它没有席位限制,也不按用户收费,因为唯一的成本是服务器。本指南使用 Docker Compose 部署 Planka,并将其置于 Traefik 后方;数据存储在 Postgres 中,用户上传的每个文件都保存到一个命名卷。

本指南面向一个准备离开 Trello 免费版、规模为 2 到 5 人的团队。如果您还在决定使用哪个看板,请先阅读自托管 Trello 替代方案的比较。本指南假设您已经决定使用 Planka,只介绍部署过程。

您需要一台运行 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 还要存放团队文档,应先按文档应用的需求进行配置:运行类似 Notion 的 AFFiNE 工作区,在 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 .env

openssl rand -hex 是有意这样设置的。十六进制字符串只包含数字和字母 a 到 f,因此不会破坏粘贴到其中的 DATABASE_URL 连接字符串。包含斜杠或 at 符号的 base64 密码会产生连接错误,错误信息看起来像主机名错误,排查起来可能浪费一小时。更广泛的处理方式见避免将密钥写入 Compose 文件。

现在执行 docker-compose.yml。将 kanban.example.com 替换为您自己的主机名。该主机名在文件中出现两次,都要替换。

services:
  planka:
    image: ghcr.io/plankanban/planka:2.1.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 文件,也不会进入容器环境。如果有多个人可以访问 VPS 的 shell,请使用此方式。由于 depends_on,该命令会先启动 Postgres,因此即使整个堆栈从未启动过,也可以正常工作。

无论采用哪种方式,您都需要手动管理 Planka 密码。如果这已经是团队收集的第 4 组凭据,Planka 也可以将登录委托给 OIDC 提供商,例如 作为您自己的单点登录服务器运行的 Authentik;同时保留初始化管理员账户,作为提供商不可用时的紧急备用账户。

BASE_URL 与主机名不匹配时为何会导致登录失败

BASE_URL 是用户在浏览器中输入的完整地址,包含 scheme,且末尾不带斜杠。对于此堆栈,该地址是 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 地址。设置该选项后,应用会读取这些请求头,并与浏览器使用相同的 scheme。

Traefik 无需额外配置即可代理 WebSocket,这是此处更适合使用 Traefik 的原因之一。在 nginx 上,socket.io 需要单独的 location 块,并在其中包含 proxy_set_header Upgrade $http_upgrade 和 proxy_set_header Connection "upgrade";否则也会出现卡住的加载指示器,但原因不同。

以后将看板迁移到新的主机名时,必须同时修改两项: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 ps

docker 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.com

HTTP/2 200 表示 Traefik 已持有证书,并且能够访问容器。由 Traefik 返回的 404 表示路由器标签不匹配,最常见的原因是容器未连接到 proxy 网络。现在打开站点,并使用管理员账户登录。

每次升级版本前执行 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.1.1 是截至 2026 年 8 月的特定发行版本。latest 会在上游发布新版本时随之变化,因此例行执行 docker compose pull 可能会在你未选择的时间引入架构迁移。修改该数字前,请阅读发行说明,因为其中会说明不兼容变更和安全修复。2.0.3 版本作为安全版本发布,正是应该先阅读、而不是意外获取的内容。在这里固定标签很容易,因为上游会发布镜像;如果项目没有发布镜像,则仍应遵循同样的纪律,只是需要额外步骤,例如 从已检出的 git 标签在本机上构建 openGym。

postgres:16-alpine 固定到主版本的原因更重要。Postgres 会以与主版本相关的格式写入数据目录,而服务器拒绝打开由其他主版本写入的目录。写入 postgres:latest,让标签滚动到 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 主版本意味着从旧版本导出数据,再将其恢复到新版本的全新数据目录中。这项工作应在停止整个服务栈后按计划执行,而不是作为拉取镜像的副作用发生。

如果你要迁移现有的 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。 容器未连接到 proxy 网络,或者 Host() 规则与 DNS 记录不匹配。docker compose config 会显示替换变量后的标签,拼写错误会在这里显现。

通知或 Webhook 始终无法送达。 Planka 2 通过内部过滤器发送对外 HTTP 请求,默认阻止列表包含 localhost 和 postgres。发往同一主机上另一个容器的 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 版开始,Planka 不会自动创建管理员。您可以设置 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 进程,因此 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 标签固定在其主版本上,因为服务器拒绝打开由其他主版本写入的数据目录。