SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-10

如何用 Docker Compose 自托管 Planka

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

自托管 Planka 可获得什么

自托管 Planka 可以为团队提供 Kanban 看板,使用大家已经熟悉的 Trello 卡片、列表和标签模型,并运行在您控制的 VPS 上。它没有席位限制,也不按用户计费,因为唯一的成本是服务器。本文使用 Docker Compose 在 Traefik 后部署 Planka,使用 Postgres 存储数据,并为用户上传的每个文件使用一个命名卷。

本文面向一个准备离开 Trello 免费套餐、由两到五人组成的团队。如果您还在决定使用哪个看板,请先阅读自托管 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 用作缓存。

在确定内存配置前,先确定磁盘配置,因为增长最快的是附件。不要直接相信本段内容,而应测量您自己的实例:

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 的用户。如果不存在,就会使用与其一起设置的密码、显示名称和用户名创建该用户。此操作发生在针对空数据库的首次启动期间,因此这些变量用于引导创建账户,而不是管理账户。

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 密码。如果这已经是团队收集的第四组凭据,Planka 也可以将登录交给 OIDC 提供商,例如 作为您自己的单点登录服务器运行的 Authentik,同时保留引导管理员账户,作为 OIDC 提供商不可用时的紧急恢复账户。

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-ProtoX-Forwarded-For 标头,因此会认为连接不安全,并将所有客户端视为同一个 IP 地址。设置该参数后,应用会读取这些标头,并与浏览器对协议的判断保持一致。

Traefik 无需额外配置即可代理 WebSocket,这也是此处优先选择它的原因之一。在 nginx 上,socket.io 需要单独的 location 块,并包含 proxy_set_header Upgrade $http_upgradeproxy_set_header Connection "upgrade";否则也会出现同样卡住的加载指示器,但原因不同。

以后将看板迁移到新的主机名时,需要同时修改两项:BASE_URL 值和 Traefik 的 Host() 规则。只修改其中一项而忘记另一项,就会再次卡在加载指示器上。从版本 2.1.0 开始,Planka 支持通过 https://example.com/planka 这样的子路径提供服务,该版本于 March 2026 发布。在较旧的标签上,请为其分配独立的子域名。

Planka 存储附件和头像的位置

Planka 2 会将用户上传的所有内容存储在容器内的单一路径下:/app/data。附件、用户头像和看板背景图片都存储在此路径下。版本 1 使用 3 个独立目录,因此从旧教程复制的 Compose 文件会挂载已经不存在的路径,而实际数据目录则未被挂载。

这个单一挂载点决定了看板能否在升级后保留数据。如果 /app/data 未位于卷中,上传的文件就会写入容器的可写层。重新创建容器时,该可写层会被销毁;每次修改镜像标签都会重新创建容器。看板恢复后看起来正常,卡片也都还在,但所有附件链接都会失效,因为数据库记录仍指向已经不存在的文件。

上面的 Compose 文件使用命名卷,可以避免此问题。绑定挂载同样可用,并且便于使用普通工具备份文件,但还需要额外执行一个步骤。容器内的 Node 进程以 UID 1000 运行,因此,如果主机目录的所有者是 root,首次上传时会出现权限错误:

sudo chown -R 1000:1000 /opt/planka/data

两者之间的取舍详见绑定挂载与命名卷的对比

如果附件占用的空间超过套餐提供的磁盘容量,Planka 也可以通过 S3_ENDPOINTS3_BUCKET 及对应的密钥变量,将附件写入兼容 S3 的存储。存储目标可以是托管存储桶,也可以是另一台主机上的自托管 MinIO 对象存储。请在团队开始大量使用看板前决定是否采用此方案,因为该设置仅适用于新上传的文件。

启动堆栈并检查结果

docker compose pull
docker compose up -d
docker compose ps

docker compose ps 应显示 postgreshealthy,并显示 plankarunning。如果 Planka 反复重启,首先应检查数据库连接,而不是应用本身。

docker compose logs -f planka

首次正常启动时,系统会运行数据库迁移,然后报告服务器正在监听 1337 端口。不要只相信日志,应直接查询 Postgres,确认架构确实已创建:

docker compose exec postgres psql -U planka -d planka -c '\dt'

如果表列表中包含 boardcard,说明迁移已执行。“Did not find any relations”表示 Planka 从未连接到数据库。请将 DATABASE_URL.env 中的 POSTGRES_USERPOSTGRES_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.shdocker-restore.sh,官方文档建议通过 nightly cron job 运行它们。两种方式都可以。不可接受的是从未执行过恢复测试的备份。因此,请将备份恢复到一台临时 VPS 上,确认可以登录并打开附件。

每次更改版本前立即执行转储。昨晚的备份与即将执行迁移前创建的备份不是一回事。

固定镜像标签并阅读发行说明

该文件中的两个镜像标签都经过有意固定。

ghcr.io/plankanban/planka:2.1.1 是一个具体版本,截至 2026 年 8 月仍为当前版本。latest 会在上游发布新版本时自动变化,因此例行执行 docker compose pull 可能在你未选择的时间引入数据库架构迁移。修改该版本号前,请先阅读发行说明,其中会说明不兼容变更和安全修复。2.0.3 作为安全版本发布,正是应该主动阅读,而不是意外引入的变更。

postgres:16-alpine 固定到主版本的原因更为严格。Postgres 会以与主版本相关的格式写入数据目录,服务器拒绝打开由其他主版本写入的目录。将 postgres:latest 写成 17,使标签自动更新到 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 请求,默认阻止列表涵盖 localhostpostgres。指向同一主机上另一个容器的 Webhook 可能会被有意阻止。请调整 OUTGOING_ALLOWED_HOSTS,不要移除过滤器。

运行后,运维负担较小。请关注版本说明,并在每次升级前转储数据库。只要 Docker 服务本身已配置为开机启动,系统重启后该栈会因 restart: unless-stopped 自动恢复;如果不会自动恢复,请参阅重启后自动恢复的 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 进程,因此 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 pulldocker compose up -d,并监控日志中的迁移过程。将 Postgres 标签固定在当前主版本,因为服务器拒绝打开由其他主版本写入的数据目录。