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

自行託管 Planka:Docker Compose Kanban 看板

使用 Docker Compose 在 VPS 部署 Planka,設定 Postgres、Traefik 與管理員初始化變數,並避開 BASE_URL 設定錯誤造成的登入問題。

自行託管 Planka 可獲得的功能

自行託管 Planka 可為團隊提供 Kanban 看板,以及 Trello 使用者熟悉的卡片、清單和標籤模型,並在由您控制的 VPS 上執行。它沒有席位數量限制,也不按使用者計費,因為唯一的成本是伺服器。本指南會使用 Docker Compose 在 Traefik 後方部署 Planka,使用 Postgres 儲存資料,並為使用者上傳的每個檔案使用具名 volume。

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

您需要一台執行 Docker Engine 且已安裝 Compose plugin 的 VPS,並需要一筆指向該 VPS 的 DNS A record。您也需要在該伺服器上已有 Traefik,負責終止 TLS(transport layer security)連線。如果尚未設定 Traefik,請先完成 在多個 Compose 應用程式前方設定 Traefik reverse proxy;如果不熟悉下方的檔案,請閱讀 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 使用量。第二個指令會顯示每個 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:

該檔案中有四項設定值得說明,因為這些設定最常被修改,之後又造成問題。

  • Planka 服務沒有 ports: 區塊。Traefik 會透過 proxy network 連線至容器,因此不會在主機上公開 1337 埠。公開該埠會讓任何人繞過代理與憑證。
  • loadbalancer.server.port=1337 指定容器內的埠。Planka 監聽 1337,而上游範例只有在將該埠映射至主機後,才會透過 3000 連線。這裡沒有主機映射,因此必須告訴 Traefik 容器埠。
  • condition: service_healthy 會搭配 Postgres healthcheck 使用。沒有這項設定時,Planka 會在資料庫接受連線前啟動,第一次查詢失敗後結束,看起來就像持續崩潰重啟。相關機制請參閱 Compose healthcheck 與啟動順序。
  • 資料庫服務刻意命名為 postgres。Planka 2 會透過內部篩選器處理自己的對外請求,而其預設封鎖清單為 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 的使用者。若找不到,便會使用與該變數一同設定的密碼、顯示名稱和使用者名稱建立使用者。這只會在對空白資料庫進行首次啟動時發生,因此這些變數的用途是 bootstrap 帳號,而不是管理帳號。

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,因此即使整個堆疊從未啟動過,也能正常運作。

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

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 後方,因此所有請求都會從 proxy 的位址,透過 Docker network 內的 plain HTTP 到達 Planka。未設定 TRUST_PROXY 時,應用程式會忽略 Traefik 設定的 X-Forwarded-Proto 與 X-Forwarded-For 標頭,因此會認為連線不安全,並將所有用戶端視為同一個 IP 位址。設定後,應用程式會讀取這些標頭,並使 scheme 與瀏覽器認知的內容一致。

Traefik 不需額外設定即可 proxy 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 發布。較舊的 tag 應改用專用子網域。

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-compatible 儲存空間。這可以指向代管的 bucket,也可以指向另一台主機上的 自架 MinIO object store。請在團隊填滿看板前決定是否採用此設定,因為該設定只會套用至新的上傳內容。

啟動服務堆疊並確認運作正常

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 從未連線,因此請比較 .env 中的 DATABASE_URL 與 POSTGRES_USER、POSTGRES_PASSWORD 值。

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

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

HTTP/2 200 表示 Traefik 已取得憑證,且能連線至容器。Traefik 回應 404 表示路由器標籤不相符,最常見的原因是容器未連接至 proxy 網路。現在開啟網站,並使用管理員帳戶登入。

每次升級版本前先執行 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 會配置 pseudo-terminal,而終端機層會改寫資料流中的換行字元,導致傾印檔在還原過程中途失敗。這項問題可能在數週後才出現,而那通常是最糟的時機。

接著處理 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 .

該專案的 repository 也提供 docker-backup.sh 和 docker-restore.sh,官方文件建議以 nightly cron job 執行。兩種方式都可以。不可接受的是從未還原過的備份。因此,請先將其中一份備份還原到 scratch VPS,確認可以登入並開啟附件。凡是接受 uploads 的 Compose app,都會使用這一組儲存位置。因此,日後若將 Chatwoot 與 support desk 放在同一台伺服器上,只需修改 volume 名稱,這裡建立的流程大致即可沿用。

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

固定標籤並閱讀發行說明

該檔案中的兩個映像標籤都是刻意固定的。

ghcr.io/plankanban/planka:2.1.1 是特定版本,截至 2026 年 8 月仍為目前版本。latest 會在上游發布新版本時移動,因此例行執行 docker compose pull 可能在你未選定的時間帶入 schema migration。修改該版本號前,請先閱讀發行說明,因為其中會說明重大變更與安全性修正。2.0.3 以安全性版本的形式發布,這正是應該先閱讀,而不是意外納入的內容。這裡之所以容易固定版本,是因為上游會發布映像;如果專案沒有發布映像,你仍應採用相同的紀律,只是需要多一個步驟,例如 在主機上從已簽出的 git 標籤建置 openGym。

postgres:16-alpine 固定在 major version,原因更重要。Postgres 會以與 major version 綁定的格式寫入資料目錄,伺服器拒絕開啟由其他版本寫入的目錄。若寫入 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 major version,表示必須從舊版本建立 dump,再還原到新版本的全新資料目錄。這是應在服務堆疊停止時規劃執行的工作,不是拉取映像時產生的附帶結果。

如果你要搬遷現有的 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,然後重新啟動容器。

升級後附件消失。 /app/data 沒有放在 volume 上,因此檔案位於升級時被替換的容器層。先從備份還原檔案,再加入 volume,之後才再次變更 image tag。

Traefik 回傳 404。 容器不在 proxy network 上,或 Host() 規則與您的 DNS 記錄不相符。docker compose config 會顯示 substitution 後的 labels,可在其中看出拼寫錯誤。

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

啟動後,日常維運負擔很小。請留意 release notes,並在每次升級前傾印資料庫。只要 Docker 服務本身已設定為開機啟動,重新開機後 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 程序以 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 而不遺失資料?

請在升級前立即傾印資料庫並封存 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 pull 和 docker compose up -d,並監控日誌中的 migration。請將 Postgres tag 固定在其 major version,因為伺服器拒絕開啟由不同 major version 寫入的資料目錄。