デプロイ構成
crowi/crowi-web イメージを api に向ける方法 — 同一オリジン + リバースプロキシ(推奨)、クロスオリジンの制約、Vercel / PaaS、ローカル開発
Crowi のデプロイは 2 つのサービス で構成されます: api(Hono)と
web(Next.js)。web 側の要点は ブラウザがどうやって api に到達するか です。
結論かつ推奨構成: ブラウザには単一オリジンを見せ、前段にリバースプロキシを置いて
パスごとに api / web へ振り分けます。以下はその理由と、代替案のコストの説明です。
モデル: 1 つの公開オリジン、2 つの認証モード
「api が公開到達可能であること」と「ブラウザが api をクロスオリジンで叩くこと」は 別の問題 です。前段リバースプロキシは、これを 1 オリジンで両立させます。
| 認証モード | 利用者 | URL | クロスオリジン? |
|---|---|---|---|
| Bearer トークン | CLI / MCP / OAuth クライアント、そしてブラウザ自身(ページ API・JSON 更新。アクセストークンは localStorage に保持し Authorization で送る) | https://wiki.example.com/api/... に Authorization: Bearer … | 可(トークンはオリジンを越える) |
| WebSocket トークン | ブラウザ。realtime の 3 経路(/collab・/presence・/notifications)向け——認証済み Bearer 呼び出しで発行される短命・単一用途のトークンを、WebSocket 接続時に ?token= クエリパラメータとして渡す。ブラウザは WS handshake に Authorization ヘッダを付けられないため(リアルタイム共同編集の運用 参照) | wss://wiki.example.com/collab?token=… | 可(トークンはヘッダではなく URL に乗る) |
| Cookie / 相対 | ブラウザ。ただし headerless な添付配信 3 経路のみ(GET /api/attachments/{id}[/original]、GET /api/attachments/by-key/*)——<img src> や直接ナビゲーションはブラウザが JS フックなしで組み立てるため Authorization ヘッダを付けられない | ページと同一オリジン | 不可(first-party cookie 前提) |
つまり 公開 api サーフェスは 1 枚(<origin>/api、proxy が晒す)です。ブラウザも
ほとんどのリクエストは他のクライアントと同じ方法——localStorage から読んだ Bearer
トークン、あるいは realtime の WebSocket 3 経路については同じ Bearer 接続の上で発行される
短命な WebSocket トークンをクエリパラメータとして渡す方法——で認証します(WS handshake には
カスタムヘッダを付けられないため)。インライン <img> 添付とファイルダウンロードだけは、
ブラウザがヘッダを付けられないまま自分で取得するため first-party の crowi.accessToken
cookie にフォールバックします。この cookie フォールバックは
上記の添付配信 3 経路だけに絞られており、それ以外のルート(添付 API の upload / meta /
delete を含む)は cookie の有無に関わらず Bearer ヘッダが必須です。proxy があるおかげで、
CLI は wiki.example.com/api を Bearer で叩け、ブラウザは同じオリジンを自身の Bearer
トークン(と、配信 3 経路向けの cookie)で使えます。本当に private なのは api の内部直
アドレス(http://api:3000)だけで、外部クライアントはそこを使いません。
web イメージが api URL を解決する仕組み
配布される crowi/crowi-web イメージは絶対 API URL を 一切焼き込みません。
3 つの URL を分けて考えます:
- ブラウザ → api(HTTP): 既定でブラウザは 相対 パス(
/api/...・/files/...)を 自身のオリジン に投げ、前段 proxy が api へ振ります。(前段 proxy を置かず web サーバを直接公開する場合、web 内蔵の proxy でも/api・/filesを転送できますが、 これは dev か宛先が焼かれている場合のみ有効。下記 (D) と Vercel の注記参照。) - ブラウザ → api(WebSocket): realtime(
/collab・/presence・/notifications)は web 内蔵の proxy を 通せません(HTTP 専用でupgradeを落とす)。ブラウザは既定でwindow.locationから WS URL を導出し、前段 proxy がそのパスを api へ振ります。 - クロスオリジン用の上書き:
NEXT_PUBLIC_API_URL(ブラウザ HTTP)とNEXT_PUBLIC_COLLAB_URL(ブラウザ WS)は 実行時 に読まれます。root layout が リクエストごとにコンテナ env から 同期 inline でwindow.__ENVを注入するため、 同一イメージのまま別オリジンの api を向けられます(再ビルド不要)。
これにより 1 度ビルドしたイメージが任意の api 先で動く(再ビルド不要、起動時 env
だけ)。同一オリジン (A) では NEXT_PUBLIC_* の上書きを未設定のまま proxy を前段に置きます。
(A) 同一オリジン + リバースプロキシ — 推奨
両サービスの前段にリバースプロキシ(Caddy / nginx / Traefik)を 1 つの公開オリジンで置きます。 ブラウザはそのオリジンだけと話し、proxy がパスで振り分けます。
┌───────────────────────────────────────────┐
browser ──HTTPS──▶ reverse proxy (one origin, e.g. wiki.example.com) │
│ /api/* → api :3000 │
│ /files/* → api :3000 │
│ /collab/* → api :3000 (WS upgrade) │
│ /presence/* → api :3000 (WS upgrade) │
│ /notifications/* → api :3000 (WS upgrade) │
│ / → web :3000 │
└───────────────────────────────────────────┘
│ │
┌────▼───┐ ┌─────▼──┐
│ web │ │ api │──▶ MongoDB / Redis / …
└────────┘ └────────┘web コンテナでは NEXT_PUBLIC_API_URL / NEXT_PUBLIC_COLLAB_URL を 未設定 のままに
します。ブラウザは相対 HTTP パスを使い、WS URL は window.location から導出するので、
イメージに api 固有の情報は焼き込まれません。
Caddy(最小・自動 HTTPS + WS)
Caddy は WebSocket の Upgrade を自動で通し、TLS も自動取得します:
wiki.example.com {
# HTTP API + 添付 + OAuth/well-known メタデータ → api
# `/.well-known/*` は OAuth discovery(`oauth-authorization-server`)専用に
# 絞らず広く振っている。web は standalone build 時にこのパスを絶対 URL で
# rewrite できない(web 内蔵 proxy の宛先が build 時に固定される)ため、前段 proxy が
# api に直送する必要がある。ACME challenge(`/.well-known/acme-challenge/*`)
# は Caddy の TLS 自動化が reverse_proxy より前の内部ハンドラで処理するため
# 衝突しない。
@api path /api/* /files/* /.well-known/*
reverse_proxy @api api:3000
# realtime WebSocket → api(Upgrade は自動で透過)
# 共同編集クライアントは素の `/collab` に接続するので bare パスも列挙する
@ws path /collab /collab/* /presence /presence/* /notifications /notifications/*
reverse_proxy @ws api:3000
# それ以外 → web
reverse_proxy web:3000
}nginx(Upgrade ヘッダは手動)
location /api/ { proxy_pass http://api:3000; }
location /files/ { proxy_pass http://api:3000; }
location /.well-known/ { proxy_pass http://api:3000; } # OAuth discovery 等
# realtime WebSocket — Upgrade ヘッダの透過が必須
# 末尾の (/|$) で素の `/collab`(セグメントなし)も拾う
location ~ ^/(collab|presence|notifications)(/|$) {
proxy_pass http://api:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 長時間 WS 接続を維持
}
location / { proxy_pass http://web:3000; }3 つの realtime namespace(/collab・/presence・/notifications)は
Upgrade / Connection の透過が 必須 です。無いと WebSocket upgrade が落ち、
リアルタイム共同編集 / プレゼンス / 通知ベルが静かに非リアルタイム動作にフォールバック
します(他の機能は動きます)。さらに共同編集クライアントは 素の /collab
(末尾セグメントなし)に接続するため、マッチャには /collab/* だけでなく
bare /collab も 含める必要があります(上記の Caddy / nginx 例は対応済み)。
リアルタイム共同編集の運用 参照。
アップロードの body size 上限
添付アップロードの上限は既定 50MB です(CROWI_UPLOAD_MAX_BYTES で下げられる —
環境変数 参照)。ただしこれは api プロセス内で判定される
値でしかありません。前段にリバースプロキシを置く場合、プロキシ自身の body size 上限は
crowi の上限より少し大きい値に設定する必要があります(同じ値では不十分な理由は下記)。
nginx の client_max_body_size の既定は 1MB です。 何も設定しないと、crowi が
50MB まで許可していても nginx が 1MB で 413 を返し、リクエストは api に届きません
(api 側のログにも残りません)。上記の nginx 設定例の /api/ ロケーションに、次のように
足してください。
location /api/ {
proxy_pass http://api:3000;
client_max_body_size 51m; # crowi の CROWI_UPLOAD_MAX_BYTES(既定 50MB)+ multipart のフレーミング分の余裕(下記参照)
}プロキシの上限は crowi の上限と完全に同じ値ではなく、少し上に設定してください。
client_max_body_size はリクエストボディ全体を見て判定しますが、multipart アップロードは
中身のファイルより常にいくらか大きくなります(境界文字列・各パートのヘッダ・他のフォーム
フィールド分)。crowi 自身、この framing のオーバーヘッドを自分の上限に対して最大 64KiB
まで許容してから拒否します。プロキシの上限を CROWI_UPLOAD_MAX_BYTES と完全に同じ値に
すると、crowi なら受け付けるはずのリクエストをプロキシが 413 で弾いてしまうことがあります。
1MB の余裕(既定 50MB に対して 51m)を見ておけば、現実的な framing オーバーヘッドより
十分に大きく安全です。
Caddy には既定の body size 制限がありません。 上記の Caddy 設定例のままで、 crowi の上限一杯までアップロードが通ります。
プロキシの上限が crowi の上限より小さいと、CLI の pre-flight チェック(GET /attachments/upload-policy を見て判定)は通過するのに、実際のアップロードでは
プロキシが 413 を返す状態になります。CLI と、エディタの paste / ドラッグ&ドロップ
経路は、この 413 が crowi 自身の応答形式でないことを検出して「前段のリバースプロキシが
拒否した」旨を表示します(添付一覧などからの直接アップロードは汎用のエラー表示に
なります)。いずれにせよ根本的な対処は、運用者がプロキシの body size 上限を crowi の
上限より少し大きい値に上げることです
(上記「アップロードの body size 上限」参照 — 完全に同じ値では crowi が受け付ける
リクエストが 413 になることがあります)。
添付ファイルの配信とヘッダ
添付ファイルの配信は、api がレスポンスヘッダで安全性を担保しています。前段のリバースプロキシでは、次のヘッダを書き換えたり削ったりしないでください。
| ヘッダ | 役割 |
|---|---|
Content-Type | api がサーバ側の許可リストに固定した値。インライン表示できる種別以外は application/octet-stream に置き換えられます |
Content-Disposition | インライン表示しない添付をダウンロードさせます |
X-Content-Type-Options: nosniff | ブラウザが中身から種別を推測して、固定した Content-Type を迂回するのを防ぎます |
Content-Security-Policy: sandbox | インライン表示される添付 (PDF を除く) に付きます。SVG のようにスクリプトを持てる形式でも、スクリプト実行不可・オリジンなしでしか描画されません |
どの種別がインライン表示になるかは 添付ファイル を参照してください。ヘッダがブラウザまで届くことが前提の設計なので、プロキシで応答ヘッダを正規化している場合は添付配信の経路 (/api/attachments/*・/files/*) を対象外にしてください。
添付の URL は認証を必要とします。ブラウザが <img> やファイルダウンロードで取得する経路だけは、ヘッダを付けられないため first-party cookie にフォールバックします。プロキシで cookie を落とさないでください。
(B) web と api を別ホストに(クロスオリジン)— 限定対応
web と api が 別オリジン(例: web = https://wiki.example.com、api =
https://api.example.com)で、前段に単一 proxy を 置かない 場合、対応は 部分的 です。
Crowi wiki では (A) を推奨します。
動くもの: ブラウザの HTTP API 呼び出し。web コンテナにブラウザ向け api URL を起動時に 設定します(実行時・再ビルド不要):
# WEB コンテナ(実行時 env・再ビルド不要):
NEXT_PUBLIC_API_URL=https://api.example.com # ブラウザ → api HTTP(絶対)
NEXT_PUBLIC_COLLAB_URL=wss://api.example.com # ブラウザ → api WebSocket# API コンテナ:
CLIENT_URL=https://wiki.example.com # CORS allow-origin(完全一致)インライン画像・アバター・ファイル DL はクロスオリジンで動きません。 これらは
相対 URL(/api/attachments/by-key/…・/files/…)で出力され、ブラウザは web
オリジンから読もうとします(api ではない)。さらに by-key 画像配信は first-party の
crowi.accessToken cookie で認証されますが、別オリジンの <img> はその cookie を
送れません。よって別オリジン構成ではアプリと HTTP データは表示されても 添付画像 /
ファイルリンクが壊れます。完全なクロスオリジン対応(全 asset URL の絶対化 + cross-site
cookie)は将来対応(defer)。代わりに (A) か Vercel edge-proxy variant (C) を使ってください
(どちらもブラウザを同一オリジンに保ちます)。
動く HTTP 側の CORS: api はブラウザ要求の origin が CLIENT_URL と 完全一致
(scheme + host + port、ワイルドカード不可)するもののみ許可します。web のオリジンを
設定してください。dev では localhost オリジンが自動許可されます。
(C) Vercel / マネージド PaaS
api は別所(VM / コンテナ基盤)の公開オリジンに置き、web を Vercel にデプロイします。 クリーンな手は、Vercel の edge を proxy にして ブラウザを同一オリジン に保つことです:
CROWI_API_URL=https://api.example.comを Vercel の build/runtime env に設定。 Vercel は rewrite を build 時に解決し、edge が/api/*・/files/*を per-request で proxy するため、ブラウザは Vercel オリジン(相対 URL)のまま — インライン画像・ ファイル DL・first-party cookie がすべて動きます。- WebSocket は例外: Vercel の edge は WS を proxy しません。realtime namespace は api を
直接指してください:
NEXT_PUBLIC_COLLAB_URL=wss://api.example.com。(WS には cookie / 画像の問題はありません — api は短命のトークンで upgrade を認証します。) - api には
CLIENT_URLを Vercel の web オリジンに設定(CORS・完全一致)。WS の origin チェックにも効きます。
host-only cookie 不変条件。 crowi.accessToken cookie はクライアント側で Domain
無し(host-only)・SameSite=Lax で発行されるため、ページを配信するオリジン(edge が
転送する Vercel/proxy オリジンを含む)に first-party で乗ります。api 側で Domain 固定の
session cookie を設定しないでください(proxy 越しに乗らなくなります)。これは self-host
proxy (A) でも同じ前提です。
self-host の standalone サーバと違い、Vercel は deploy ごとに build し直し、edge が
rewrite を per-request で適用するため、build 時固定の proxy 宛先は
ここでは制約になりません。build 時に NEXT_PUBLIC_API_URL=https://api.example.com
(クロスオリジン・焼き込み)を渡す手もありますが、(B) の画像 / cookie 制約を引き継ぎます。
(D) ローカル開発
pnpm dev は api(:4301)と web(:4302)を起動し、その前段に
同一オリジンの reverse proxy(:4304) を立てます(Caddy があれば
Caddy、無ければ依存ゼロの node フォールバック。ルーティング表は本番の
前段 proxy と同一)。:4304 が dev の正式な入口です:
- HTTP: ブラウザは
:4304に対して相対/api/...を叩き、proxy が api パスを:4301へ、それ以外を web へ振り分けます。 - WebSocket:
/collab・/presence・/notificationsも同じ:4304オリジンを upgrade します — resolver は既定でwindow.locationから URL を 導出するため、追加設定は不要です。素の web ポート:4302を直接開くと proxy を迂回し、リアルタイム系は接続できません(web 内蔵の proxy は HTTP 専用で WS upgrade を転送できません)。
dev では CROWI_API_URL も NEXT_PUBLIC_* も不要です。
まとめ
| 構成 | ブラウザ → api HTTP | ブラウザ → api WS | インライン画像 / ファイル | api 先変更に再ビルド? |
|---|---|---|---|---|
| (A) 同一オリジン proxy(推奨) | 相対・proxy 経由 | window.location・proxy 経由 | ✅ 動く(同一オリジン) | 不要 |
| (B) クロスオリジン self-host | NEXT_PUBLIC_API_URL(実行時) | NEXT_PUBLIC_COLLAB_URL(実行時) | ❌ 壊れる(相対 + cookie) | 不要(実行時 env) |
| (C) Vercel + edge proxy | 相対・Vercel edge 経由(CROWI_API_URL build env) | NEXT_PUBLIC_COLLAB_URL(直接) | ✅ 動く(同一オリジン) | n/a(deploy ごと build) |
| (D) dev | 相対・:4304 dev proxy 経由 | window.location・:4304 dev proxy 経由 | ✅ 動く | n/a |
次のステップ
- セルフホスティング (自前の runner プロジェクト) — 自前の runner プロジェクトから api + web イメージをビルド
- リアルタイム共同編集の運用 — WS 要件とマルチレプリカの注意点
- バージョンアップ — 全レプリカを揃えて入れ替える手順と版ごとの注記
- 設定 — 設定レイヤーとプラグインの解決方法
- 環境変数 — 変数名・既定値・意味の一覧