Crowi

デプロイ構成

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-Typeapi がサーバ側の許可リストに固定した値。インライン表示できる種別以外は 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_URLNEXT_PUBLIC_* も不要です。

まとめ

構成ブラウザ → api HTTPブラウザ → api WSインライン画像 / ファイルapi 先変更に再ビルド?
(A) 同一オリジン proxy(推奨)相対・proxy 経由window.location・proxy 経由✅ 動く(同一オリジン)不要
(B) クロスオリジン self-hostNEXT_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

次のステップ

On this page