PlankaをVPSにセルフホストする方法
Docker ComposeでPlankaをVPSに構築します。Postgres、Traefik、管理者作成用変数に加え、ログインを壊すBASE_URL設定と、2~5人向けの1 vCPU・2 GB構成を解説します。
Planka をセルフホストすると得られるもの
Planka をセルフホストすると、Trello ですでに知られているカード、リスト、ラベルのモデルを備えた Kanban ボードを、管理下の VPS でチームに提供できます。ユーザー数の上限やユーザー単位の課金はありません。費用はサーバー代だけです。このガイドでは、Docker Compose と Traefik を組み合わせ、データに Postgres を使用し、ユーザーがアップロードするすべてのファイルを named volume に保存してデプロイします。
想定している読者は、Trello の無料プランから移行する2~5人のチームです。使用するボードをまだ決めていない場合は、先にセルフホスト可能な Trello 代替製品の比較を読んでください。このガイドでは選択済みであることを前提とし、デプロイだけを扱います。
Docker Engine と Compose plugin が動作し、対象を指す DNS A レコードが設定された VPS が必要です。また、そのサーバーで TLS(transport layer security)終端をすでに担っている Traefik instance も必要です。まだ Traefik がない場合は、先に複数の Compose アプリの前段に Traefik reverse proxy を構成するを行ってください。以下のファイルの内容が分かりにくい場合は、VPS 向け Docker Compose の基本も読んでください。
Planka に必要な VPS の規模
プロジェクトは必要なハードウェアの下限を公開していません。そのため、見かけた数値は測定値ではなく、出発点として扱ってください。ホスティングサービスのページで繰り返し示される 2 vCPU と 4 GB という値は、プロバイダーが余裕を持たせた標準値です。プロジェクトが測定した必須条件ではありません。5 人が操作するボードには十分すぎる規模です。
実際に動作する構成は小規模です。API とビルド済みフロントエンドを提供する Node.js プロセスが 1 つ、データを保持する Postgres プロセスが 1 つ動作します。さらに、Planka コンテナ内では、送信リクエストをフィルタリングする小規模なプロキシプロセスが 1 つ動作します。1 vCPU と 2 GB のプランで、2~5 人のボードを運用できます。余ったメモリの大部分は Postgres のキャッシュに使われます。ボードの負荷は軽いため、同じ VPS にチームのドキュメントも保存する場合は、そちらのアプリケーションを基準に規模を決めてください。Notion 風ワークスペースとして AFFiNE を運用するには、Planka が必要とする分とは別に、数 GB 程度のメモリが必要です。
メモリより先にディスク容量を決めてください。増加するのは主に添付ファイルだからです。次の説明をそのまま信頼せず、自分のインスタンスを測定してください。
docker stats --no-stream
docker system df -v最初のコマンドは、コンテナごとの現在のメモリ使用量と CPU 使用率を表示します。2 つ目のコマンドは、各ボリュームが使用している容量を表示します。両方とも、通常の運用を 1 週間行った後に確認してください。インストール直後では、アイドル状態のボードからチームの利用状況は分かりません。
Compose ファイルを作成する
ディレクトリを作成して所有権を取得します。これで、sudo を介してこれらのファイルを編集する必要がなくなります。
sudo mkdir -p /opt/planka
sudo chown "$USER":"$USER" /opt/planka
cd /opt/plankaCompose ファイルの隣にある .env ファイルへ Secret を生成します。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 は意図的な設定です。16 進数文字列には数字と a から f の文字しか含まれないため、貼り付け先の DATABASE_URL 接続文字列を壊しません。スラッシュやアットマークを含む base64 パスワードを使うと、ホスト名が間違っているように見える接続エラーが発生し、原因の特定に 1 時間かかることがあります。より広い考え方については、Compose ファイルから Secret を除外する で説明しています。
次に docker-compose.yml を実行します。2 か所にある 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 の healthcheck と組み合わせて使います。これがないと、データベースが接続を受け付ける前に Planka が起動し、最初のクエリに失敗して終了します。その結果、クラッシュループのように見えます。仕組みについては、Compose の healthcheck と起動順序 で説明しています。- データベースサービスを
postgresという名前にしているのは意図的です。Planka 2 は、内部フィルターを通して自身の外向きリクエストを処理します。このフィルターのデフォルトのブロックリストはlocalhost,postgresです。サービス名を変更すると、そのリストからデータベースが静かに除外されます。
何も起動する前に、Compose から Secret を参照できることを確認します。
docker compose config | grep -E 'image:|BASE_URL|POSTGRES_USER'これにより、.env の値がすでに置換されたファイルが出力されます。値が空の場合、Compose が .env ファイルを読み込めていません。通常は、別のディレクトリからコマンドを実行しています。
管理者ブートストラップ変数の実際の動作
Planka 1.13 以降では、管理者は自動作成されません。そのため、新しいデータベースにはログインできるユーザーが存在しません。DEFAULT_ADMIN_* グループは、この問題を解決する2つの方法の1つです。
起動時に 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 へのシェルアクセス権を複数の人が持つ場合は、この方法を使用してください。depends_on のため、このコマンドは最初に Postgres を起動します。そのため、まだ一度も起動していないスタックでも動作します。
どちらの方法でも、Planka のパスワードは手動で管理する必要があります。チームがすでに4組目の認証情報を管理しているなら、Planka は 自分で運用するシングルサインオン用サーバーとしての Authentik などの OIDC プロバイダーにログイン処理を委任できます。プロバイダーが停止した場合に備え、ブートストラップ用の管理者は緊急用アカウントとして残しておきます。
ホスト名と一致しないと BASE_URL によってログインが壊れる理由
BASE_URL は、スキームを含み、末尾にスラッシュを付けずに、ユーザーがブラウザーへ入力する正確なアドレスです。この構成では https://kanban.example.com です。Planka はこの値から独自のリンクと WebSocket 接続を生成します。そのため、BASE_URL が誤っていても、明確なエラーにはなりません。ページは表示されますが、読み込みが完了しない状態になります。
よくあるケースは、upstream の例をそのままコピーし、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 ヘッダーを無視します。その結果、接続は安全ではないと判断され、すべてのクライアントが同じ 1 つの IP アドレスとして扱われます。設定すると、アプリはそれらのヘッダーを読み取り、ブラウザーとスキームの認識を一致させます。
Traefik は追加設定なしで WebSocket をプロキシします。この点も、ここで Traefik を選ぶ理由の 1 つです。nginx では、socket.io に proxy_set_header Upgrade $http_upgrade と proxy_set_header Connection "upgrade" を含む専用の location ブロックが必要です。設定しないと、別の原因で同じように読み込み中のスピナーが停止します。
後でボードを新しいホスト名へ移行する場合は、BASE_URL の値と Traefik の Host() ルールを同時に変更します。一方だけを変更すると、再びスピナーが停止します。https://example.com/planka のようなサブパスからの Planka の提供は、2026 年 3 月にリリースされた version 2.1.0 以降で機能します。古いタグでは、専用のサブドメインを割り当ててください。
Planka が添付ファイルとアバターを保存する場所
Planka 2 では、ユーザーがアップロードしたすべてのファイルをコンテナ内の単一のパス /app/data に保存します。添付ファイル、ユーザーアバター、ボードの背景画像はすべてこのパスの配下にあります。Version 1 では 3 つのディレクトリを個別に使用していたため、以前の記事からコピーした Compose ファイルでは存在しなくなったパスがマウントされ、実際のデータディレクトリがマウントされない状態になります。
この単一のマウントが、アップグレード後もボードを維持できるか、復旧に多くの時間を要するかを分けます。/app/data がボリューム上にない場合、アップロードしたファイルはコンテナの書き込み可能レイヤーに保存されます。このレイヤーはコンテナを再作成すると破棄されます。イメージタグを変更するたびに、コンテナは再作成されます。ボードは正常に表示され、カードもすべて残りますが、添付ファイルへのリンクはすべて無効になります。データベースの行は、すでに存在しないファイルを参照し続けるためです。
上記の Compose ファイルにある名前付きボリュームが、この問題を防ぎます。bind mount も使用でき、通常のツールでファイルをバックアップしやすくなります。ただし、追加の手順が 1 つ必要です。コンテナ内の Node プロセスは UID 1000 で実行されるため、ホスト上のディレクトリが root 所有だと、最初のアップロード時に権限エラーが発生します。
sudo chown -R 1000:1000 /opt/planka/dataこの 2 つの方式の違いについては、bind mount と名前付きボリュームの比較で説明しています。
契約しているプランのディスク容量では添付ファイルを収容できなくなった場合、Planka は S3_ENDPOINT、S3_BUCKET および対応する key 変数を使用して、S3 互換ストレージに保存できます。ホスト型バケットを指定することも、別のサーバー上に構築した セルフホストの MinIO オブジェクトストアを指定することもできます。この設定は新しいアップロードに適用されるため、チームがボードを使い切る前に決めてください。
スタックを起動して動作を確認する
docker compose pull
docker compose up -d
docker compose psdocker compose ps には healthy として postgres が表示され、planka には running として表示されます。Planka が再起動を繰り返している場合、最初に確認するのはアプリケーションではなくデータベース接続です。
docker compose logs -f planka正常な初回起動では、データベースのマイグレーションが実行され、その後、サーバーがポート 1337 で待ち受けていることが報告されます。ログだけを信頼せず、Postgres に直接問い合わせて、スキーマが実際に作成されたことを確認します。
docker compose exec postgres psql -U planka -d planka -c '\dt'テーブル一覧に board と card が含まれていれば、マイグレーションは実行済みです。「リレーションが見つかりません」と表示される場合、Planka はまだ接続できていません。.env 内の POSTGRES_USER と POSTGRES_PASSWORD の値を確認し、DATABASE_URL と比較します。
次に、VPS ではなく自分のマシンからルートを確認します。
curl -I https://kanban.example.comHTTP/2 200 は、Traefik が証明書を保持し、コンテナに到達できていることを示します。Traefik が返す 404 は、ルーターのラベルが一致していないことを示します。最も多い原因は、コンテナが proxy ネットワークに接続されていないことです。サイトを開き、管理者アカウントでログインします。
すべてのバージョン更新前に pg_dump を取得する
ボードのデータは 2 つのストアに分かれて保存されるため、バックアップでは 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 で実行する方法が示されています。どちらの方法でも問題ありません。ただし、一度もリストアしていないバックアップは不十分です。scratch VPS に一度リストアし、ログインして添付ファイルを開けることを確認してください。この 2 つのストアは、アップロードを受け付けるすべての Compose アプリで共通して登場します。そのため、後で サポートデスクと同じホストに Chatwoot を配置する場合でも、ここで構築した手順はボリューム名を変更するだけでほぼそのまま利用できます。
すべてのバージョン変更の直前にダンプを実行してください。昨夜取得したバックアップは、これから実行する移行の直前に取得したバックアップとは異なります。
タグを固定し、リリースノートを読む
このファイルの両方のイメージタグは、意図的に固定しています。
ghcr.io/plankanban/planka:2.1.1は、2026年8月時点での特定のリリースです。latestは upstream が公開するたびに変わるため、通常の docker compose pullによって、選んでいないタイミングでスキーマ移行が実行される可能性があります。その番号を変更する前に、リリースノートを確認してください。破壊的変更とセキュリティ修正は、そこに記載されています。Version 2.0.3 はセキュリティリリースとして公開されました。このような変更は、意図せず取り込むのではなく、内容を確認してから適用すべきです。ここでタグを固定しやすいのは、upstream がイメージを公開しているためです。プロジェクトがイメージを公開していない場合も、同じ管理を行う必要があります。その場合は、チェックアウトした git tag からホスト上で構築した openGymのように、追加の手順が必要です。
postgres:16-alpineを major version に固定する理由は、さらに重要です。Postgres は major version に依存する形式でデータディレクトリを書き込みます。そのため、別の 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 を取得し、新しいバージョンの新しいデータディレクトリへ restore します。これは stack を停止して計画的に実施する作業です。イメージの pull に伴う副作用ではありません。
新規構築ではなく、既存の Planka 1.x インストールを移行する場合、この upgrade には独自の手順があり、プロジェクトのドキュメントに記載されています。事前に取得した backup がなければ、version 1 に戻す方法はありません。
障害の種類と表示される文字列
Planka がループ状に再起動し、ログにデータベースが記録される場合。 DATABASE_URL の認証情報が Postgres の環境変数と一致していません。POSTGRES_PASSWORD はデータディレクトリの初回初期化時にのみ適用されるため、最初の起動に失敗した後で変数を修正しても何も変わりません。db-data ボリュームを削除して、最初から起動する必要があります。
ログインには成功するが、ボードが表示されない場合。 BASE_URL がブラウザーのアドレスバーに表示されたアドレスと一致していないか、TRUST_PROXY がありません。ブラウザーのコンソールには、/socket.io/ へのリクエストが失敗したことが表示されます。
他の機能は正常だが、アップロードだけ失敗する場合。 bind mount の所有者が 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 を生成します。そのため、サイトには https://kanban.example.com でアクセスしているのに、その変数が http://localhost:3000 のままだと、ブラウザーはマシン上に存在しないアドレスへソケット接続を試みます。開発者コンソールには /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 という値は、測定結果ではなくプロバイダーの既定値です。小規模なボードには余裕があります。処理全体は、1 つの Node プロセスと 1 つの Postgres プロセスで構成されます。そのため、1 vCPU と 2 GB のプランで 2 人から 5 人のチームに対応できます。通常の 1 週間の運用後に 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 のタグはメジャーバージョンに固定してください。異なるメジャーバージョンで書き込まれたデータディレクトリは、サーバーが開けないためです。