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

自架 Planka:Docker Compose Kanban 看板部署

使用 Docker Compose 在 VPS 部署 Planka,涵蓋 Postgres、Traefik、管理員初始變數,以及 BASE_URL 設定錯誤導致登入失敗的問題。

自架 Planka 的效益

自架 Planka 可讓團隊使用熟悉的 Kanban 看板,採用 Trello 已有的卡片、清單與標籤模型,並執行於由您控管的 VPS。此方案沒有席位限制,也不按使用者計費,因為唯一的成本是伺服器。本指南會使用 Docker Compose 搭配 Traefik 部署,使用 Postgres 儲存資料,並為使用者上傳的每個檔案配置 named volume。

本指南的目標讀者是由兩到五人組成、準備離開 Trello 免費方案的團隊。如果您仍在決定要使用哪個看板,請先閱讀 自架 Trello 替代方案比較。本指南假設您已經決定使用 Planka,內容只涵蓋部署流程。

您需要一台執行 Docker Engine 且安裝 Compose plugin 的 VPS,以及一筆指向該 VPS 的 DNS A record。您也需要在該伺服器上已有 Traefik,並由它完成 TLS termination。如果尚未設定 Traefik,請先建立 位於多個 Compose 應用程式前方的 Traefik 反向代理;如果不熟悉下方的檔案,請先閱讀 VPS 的 Docker Compose 基礎

Planka 需要多少 VPS 資源?

專案沒有公布硬體最低需求,因此請將你看到的任何數字視為起始值,而不是實測結果。託管服務頁面反覆列出的 2 vCPU 和 4 GB,是供應商提供的寬裕預設值,不是專案實測出的必要條件。對於只有 5 人使用的看板而言,這個配置相當充裕。

實際執行的元件很少:一個 Node.js 程序負責提供 API 與建置後的前端,另一個 Postgres 程序負責儲存資料。Planka 容器內還會執行另一個小型 proxy 程序,用來篩選對外發出的請求。1 vCPU 和 2 GB 的方案足以支援 2 到 5 人使用的看板,而多數剩餘記憶體最後都會成為 Postgres 快取。

請先規劃磁碟容量,再規劃記憶體,因為成長最快的是附件。請測量自己的執行個體,不要直接採用本段內容:

docker stats --no-stream
docker system df -v

第一個指令會顯示每個容器目前的記憶體與 CPU 使用量。第二個指令會顯示每個 volume 使用的空間。請在正常工作一週後取得這兩項數據,不要在安裝當天測量,因為閒置的看板無法反映團隊的實際使用量。

撰寫 Compose 檔案

建立目錄並變更其擁有者,這樣就不必透過 sudo 編輯這些檔案。

sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/planka

將 secret 產生至 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 連線字串。若 base64 密碼包含斜線或 at 符號,會產生看似主機名稱錯誤的連線錯誤,讓你浪費一個小時。更完整的做法請參閱 避免將 secret 放入 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 network 連線至容器,因此不會將 1337 埠發布至主機。發布該埠會讓任何人繞過代理與憑證。
  • loadbalancer.server.port=1337 指定容器內的埠。Planka 監聽 1337,而上游範例只能透過 3000 連線,因為它會將該埠映射至主機。這裡沒有主機映射,因此必須告訴 Traefik 容器埠。
  • condition: service_healthy 會搭配 Postgres healthcheck 使用。沒有這項設定時,Planka 會在資料庫接受連線前啟動,第一次查詢失敗後結束,看起來就像發生 crash loop。相關機制請參閱 Compose healthcheck 與啟動順序
  • 資料庫服務刻意命名為 postgres。Planka 2 會透過內部 filter 處理自行發出的請求,而其預設封鎖清單是 localhost,postgres。若重新命名該服務,就會在不易察覺的情況下,將資料庫從清單中移除。

啟動任何服務前,先確認 Compose 能讀取你的 secret:

docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'

這會列印已代入 .env 值的檔案。空值表示 Compose 沒有讀取 .env 檔案,通常是因為你在不同目錄中執行了該命令。

管理員 bootstrap 變數的實際作用

自 Planka 1.13 起,系統不會替你建立管理員,因此全新的資料庫中沒有任何可登入的帳號。DEFAULT_ADMIN_* 群組是解決這個問題的兩種方式之一。

Planka 啟動時會尋找符合 DEFAULT_ADMIN_EMAIL 的使用者。若找不到,便使用同組設定的密碼、顯示名稱和使用者名稱建立帳號。這會在空白資料庫的第一次啟動時執行,因此這些變數的作用是建立初始帳號,而不是管理帳號。

DEFAULT_ADMIN_EMAIL 還有另一項容易造成誤解的作用。只要設定此變數,任何人都無法從介面編輯或刪除它指定的帳號。這是防止帳號被鎖在外面的保護機制,也因此你無法在 UI 中重新命名該帳號或變更其電子郵件地址。移除變數並重新啟動後,該帳號就會變成一般管理員,可以像其他帳號一樣編輯。

密碼那一行必須特別小心。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,因此即使整個 stack 從未啟動過,也能正常執行。

無論採用哪種方式,你都必須自行管理 Planka 密碼。如果這已經是團隊收集的第 4 組認證,Planka 也可以改為委派給 OIDC provider,例如 將 Authentik 作為自建的單一登入伺服器,並保留 bootstrap admin 作為 provider 當機時使用的 break-glass 帳號。

BASE_URL 與主機名稱不一致時,登入為何會失效

BASE_URL 是使用者在瀏覽器中輸入的完整位址,必須包含 scheme,且結尾不可有斜線。對這個 stack 而言,該值是 https://kanban.example.com。Planka 會依據這個值建立自身的連結與 WebSocket 連線,因此 BASE_URL 錯誤時不會顯示明確錯誤,而是頁面載入後一直無法完成載入。

最常見的情況是:你複製 upstream 範例,保留 BASE_URL=http://localhost:3000,但實際上是透過真實網域的 HTTPS 存取網站。登入表單成功送出,認證資訊也獲得接受,但看不到 board。開啟瀏覽器的開發人員主控台後,會看到連往 /socket.io/ 的請求失敗,因為系統要求用戶端將即時連線開啟至 localhost:3000,而該位址在你的筆電上根本不存在。

TRUST_PROXY=true 是同一問題的另一部分。Planka 位於 Traefik 後方,因此每個請求都會從 proxy 的位址,透過 Docker network 內的純 HTTP 傳入。未設定 TRUST_PROXY 時,應用程式會忽略 Traefik 設定的 X-Forwarded-ProtoX-Forwarded-For 標頭,因此認為連線不安全,並將所有用戶端視為來自同一個 IP 位址。設定後,應用程式會讀取這些標頭,讓它對 scheme 的判斷與瀏覽器一致。

Traefik 不需額外設定即可 proxy WebSocket,這也是此處偏好使用 Traefik 的原因之一。在 nginx 上,socket.io 需要自己的 location 區塊,並包含 proxy_set_header Upgrade $http_upgradeproxy_set_header Connection "upgrade";否則也會出現相同的卡住轉圈圖示,但成因不同。

日後將 board 移至新的主機名稱時,必須同時變更兩項設定:BASE_URL 值與 Traefik 的 Host() 規則。只變更其中一項而忘記另一項,就會再次卡在轉圈畫面。從版本 2.1.0 起,Planka 支援以 https://example.com/planka 這類子路徑提供服務;該版本於 March 2026 發布。在較舊的 tag 上,請為它提供獨立的 subdomain。

Planka 儲存附件與頭像的位置

Planka 2 會將使用者上傳的所有內容儲存在容器內的單一路徑:/app/data。附件、使用者頭像與看板背景圖片都儲存在這個路徑下。Version 1 使用 3 個不同的目錄,因此從較早文章複製的 Compose 檔案會掛載已不存在的路徑,真正的資料目錄則未掛載。

這個單一掛載點,決定看板能否在升級後保留資料。若 /app/data 不在 volume 上,附件就會寫入容器的可寫入層。容器重新建立時,這一層會被刪除;每次變更 image tag,都會重新建立容器。看板恢復後表面上看起來正常,卡片也都還在,但所有附件連結都會失效,因為資料庫資料列仍指向已不存在的檔案。

上方 Compose 檔案中的 named volume 可避免此問題。bind mount 也可以使用,且便於透過一般工具備份檔案,但需要多一個步驟。容器內的 Node process 以 UID 1000 執行,因此如果 host 目錄的擁有者是 root,第一次上傳時會發生權限錯誤:

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

兩者之間的取捨,請參閱bind mount 與 named volume 的比較

如果附件超出方案提供的磁碟容量,Planka 也能透過 S3_ENDPOINTS3_BUCKET 及對應的 key 變數,將附件寫入 S3-compatible storage。這可以指向代管的 bucket,也可以指向另一台主機上的自架 MinIO object store。請在團隊填滿看板前決定,因為此設定只會套用至新的上傳內容。

啟動堆疊並確認運作正常

docker compose pull
docker compose up -d
docker compose ps

docker compose ps 應顯示 postgreshealthy,並顯示 plankarunning。如果 Planka 持續重啟,應先檢查資料庫連線,而不是先檢查應用程式。

docker compose logs -f planka

首次正常啟動時,系統會執行資料庫 migration,然後回報伺服器正在監聽 1337 埠。請直接查詢 Postgres,確認 schema 確實已建立,不要只依賴日誌:

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

如果資料表清單包含 boardcard,表示 migration 已執行。若顯示「找不到任何 relation」,表示 Planka 從未成功連線,因此請比較 .env 中的 DATABASE_URLPOSTGRES_USERPOSTGRES_PASSWORD 值。

接著從你自己的電腦檢查路由,不要從 VPS 檢查:

curl -I https://kanban.example.com

HTTP/2 200 表示 Traefik 已持有憑證,且能連線到容器。由 Traefik 提供的 404 表示 router labels 不相符,最常見的原因是容器未連接到 proxy network。現在開啟網站,並使用管理員帳號登入。

每次升級版本前執行 pg_dump

系統會在兩個不同的儲存區保存你的看板,因此備份必須涵蓋 Postgres 資料庫與 planka-data volume。請在 stack 執行期間匯出資料庫。

docker compose exec -T postgres pg_dump -U planka -d planka > "planka-db-$(date +%F).sql"

-T 不可省略。若未使用它,Compose 會配置虛擬終端機,而終端機層會改寫資料流中的換行字元,因此產生的 dump 檔案會在還原過程中途失敗。這個問題可能要到數週後才會出現,而那通常是最不適合發生故障的時機。

接著處理 uploads。請先找出實際的 volume 名稱,因為 Compose 會在名稱前加上 project directory 名稱。

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 .

該 project 也會在其 repository 中提供 docker-backup.shdocker-restore.sh,官方文件則建議以 nightly cron job 執行。兩種方式都可以。不可接受的是從未實際還原過的備份。因此,請先將其中一份備份還原到測試用 VPS,確認你能登入並開啟附件。

每次變更版本前,都要立即執行 dump。昨晚的備份不等於即將執行 migration 前建立的備份。

固定標籤並閱讀發行說明

該檔案中的兩個 image tag 都是刻意固定的。

ghcr.io/plankanban/planka:2.1.1 是截至 2026 年 8 月的特定版本。latest 會在 upstream 發布新版本時移動,因此例行執行 docker compose pull 可能在你未選定的時機引入 schema migration。變更該數字前,請先閱讀發行說明,因為其中會說明重大變更與安全修正。2.0.3 版是以安全版本形式發布,這正是你應該先閱讀,而不是意外套用的內容。

postgres:16-alpine 固定在 major version,原因更為重要。Postgres 會以繫結至 major version 的格式寫入資料目錄,且伺服器拒絕開啟由其他 major version 寫入的目錄。若寫入 postgres:latest,讓標籤移至 17,container 將無法啟動:

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 major version,表示要先從舊版本建立 dump,再將其 restore 至新版本的全新資料目錄。這是必須在 stack 停止時規劃執行的工作,不是 image pull 的附帶結果。

如果你要移轉現有的 Planka 1.x 安裝,而不是全新開始,該升級有自己的專案文件所載程序;若事前沒有備份,就無法回到 version 1。

故障模式與您會看到的訊息

Planka 不斷重新啟動,且日誌提到資料庫。 DATABASE_URL 中的認證資料與 Postgres 環境變數不一致。請注意,POSTGRES_PASSWORD 只會在資料目錄首次初始化時套用,因此在首次啟動失敗後修正變數不會產生作用。您必須移除 db-data volume,然後重新啟動。

登入成功,但看板始終無法載入。 BASE_URL 與瀏覽器網址列中的位址不一致,或缺少 TRUST_PROXY。瀏覽器主控台會顯示對 /socket.io/ 的請求失敗。

其他功能都正常,但上傳失敗。 bind mount 的擁有者是 root。在主機上的目錄執行 sudo chown -R 1000:1000,然後重新啟動 container。

升級後附件消失。 /app/data 未配置在 volume 上,因此檔案位於升級時遭替換的 container layer 中。從備份還原檔案,然後在再次變更 image tag 前加入 volume。

Traefik 回傳 404。 container 不在 proxy network 上,或 Host() 規則與您的 DNS record 不相符。docker compose config 會顯示 substitution 後的 labels,拼寫錯誤可在此處發現。

通知或 webhook 始終未送達。 Planka 2 會透過內部 filter 傳送對外 HTTP 請求,而預設封鎖清單涵蓋 localhostpostgres。指向同一主機上另一個 container 的 webhook 可能會依設計遭到封鎖。請調整 OUTGOING_ALLOWED_HOSTS,不要移除 filter。

服務運作後,日常維運負擔不大。請查看 release notes,並在每次升級前匯出資料庫。只要 Docker service 本身已設定為開機啟動,重新開機後 stack 會因為 restart: unless-stopped 自動恢復;若未恢復,請參閱 重新開機後自動恢復的 Compose stack

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 及相符的密碼、名稱和使用者名稱變數後啟動 stack,或執行 docker compose run --rm planka npm run db:create-admin-user 並依提示操作。在共用伺服器上,互動式指令較安全,因為密碼不會進入 docker inspect 可讀取的容器環境。之後持續設定 DEFAULT_ADMIN_EMAIL,會鎖定該帳號,使其無法從介面編輯或刪除。

Planka 將附件和頭像儲存在哪裡?

在 Planka 2 中,所有上傳檔案都位於容器內的 /app/data,包括附件、使用者頭像和看板背景。請將該路徑掛載到 named volume。如果未掛載,檔案會留在容器的可寫入層,容器下次重新建立時就會被刪除;每次升級映像檔都會重新建立容器。bind mount 也可以使用,但 Node process 以 UID 1000 執行,因此請在主機目錄上執行 sudo chown -R 1000:1000,否則上傳會因權限錯誤而失敗。

自架 Planka 需要多少 RAM?

專案沒有公布最低硬體需求。託管頁面反覆提到的 2 vCPU 和 4 GB,是供應商的預設值,不是實測數據;對小型看板而言,這個配置通常綽綽有餘。整體工作負載只有一個 Node process 和一個 Postgres process,因此 1 vCPU 和 2 GB 的方案可支援 2 至 5 人的團隊。正常使用一週後執行 docker stats --no-stream,再根據實際數據決定配置。請比記憶體更密切監控磁碟,因為持續增長的是附件。

如何升級 Planka 而不遺失資料?

請在升級前立即傾印資料庫並封存 uploads volume,不要依賴前一晚排程的備份。使用 docker compose exec -T postgres pg_dump -U planka -d planka > planka-db.sql,保留 -T,避免 pseudo-terminal 損壞重新導向的輸出。閱讀每個跳過版本的 release notes,將 image tag 改為特定版本,而不是 latest,然後執行 docker compose pulldocker compose up -d,並監控日誌中的 migration。請將 Postgres tag 固定在其 major version,因為伺服器拒絕開啟由不同 major version 寫入的資料目錄。