MCP tunnelsをDocker Composeでデプロイする手順 — トークンと証明書の更新
Docker ComposeでMCP tunnelsのトンネルスタックを構築し、トークンローテーションと証明書更新まで、--url必須やUID 65532といった詰まりどころ込みでまとめます。
Docker Composeで何を起動することになるのか
最短で動作確認を通したいだけならMCP tunnelsクイックスタートの6手順で足ります。本記事はその先、本番ホストでの構築とトークン・証明書の更新運用を対象にします。
MCP tunnelsのConsole側で作るのはトンネルという入れ物だけで、実際に通信を運ぶのはネットワーク内で動くトンネルスタックです。単一ホストで動かす場合の公式な参照実装がDocker Composeで、cloudflared・プロキシ(mcp-proxy)・任意のsetupコンポーネントの3つをコンテナとして起動します。同じ構成を複数ホストに複製すれば可用性も確保できます。
始める前に、Consoleでトンネルを作成しておくか(手動フロー)、setupコンポーネントに作らせるか(プログラム的アクセス)を決めます。作成手順はMCP tunnelsをConsoleで作成・管理する手順にまとめています。
デプロイ先の選び方は単純です。単一ホストや動作確認ならDocker Compose、Kubernetesクラスタで動かすならMCP tunnelsをHelmでデプロイする手順のHelmチャートを使います。認証方式も独立に選べるので、「Kubernetesだが手動フロー」「単一ホストだがプログラム的アクセス」のような組み合わせも成立します。
| 判断軸 | Docker Compose | Helm |
|---|---|---|
| デプロイ先 | Docker Compose単一ホスト・ローカル検証 | HelmKubernetesクラスタ |
| 可用性 | Docker Compose同じ構成を複数ホストへ手動複製 | HelmDeploymentのレプリカで水平化 |
| 証明書更新の自動化 | Docker Composesetup renew-certを自分でcron登録 | Helmプログラム的アクセスなら日次CronJobが標準搭載 |
事前に必要なもの
- トンネル。プログラム的アクセスではトンネルIDを指定しなければsetupコンポーネントが作成します。手動フローでは必ずConsoleで先に作成し、トンネルIDを控えます
- Tunnels APIへの認証手段。プログラム的アクセスならfederation ruleのID(
fdrl_...)と組織ID、手動ならConsoleから取得するトンネルトークンとCA証明書 - DockerとDocker Composeが入ったホスト。手動フローでは
openssl(1.1.1以降)も使います - アウトバウンド通信。
api.anthropic.comへの443/TCPと、トンネルエッジ(198.41.192.0/19、2606:4700:a0::/44)への7844/TCP・UDPが必要です - 到達可能なMCPサーバーが1つ以上。手元に無ければFastMCPで作る最小サンプルで代用できます
- Zero Data RetentionやHIPAA BAAの対象範囲の確認。研究プレビュー段階の機能は対象可否が個別に決まっているため、規制対象データを扱う前に自組織のコンプライアンス要件と突き合わせておきます
手元にMCPサーバーが無いときのテスト用サンプル
到達可能なMCPサーバーをまだ持っていない場合、FastMCPで動く最小構成を代わりに使えます。helloツールが1つあるだけの検証専用サーバーです。
mkdir -p mcp-tunnel
cat > mcp-tunnel/hello_server.py <<'EOF'
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-server", host="0.0.0.0", port=9000)
@mcp.tool()
def hello(name: str = "world") -> str:
"""Say hello to someone."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
EOFこのサンプルを使う場合、docker-compose.yamlにhello-mcpサービスを追記し、プロキシ設定のroutesをecho: http://hello-mcp:9000にします。以降の手順ではこのechoルートを検証先として使います。
プログラム的アクセスでデプロイする
ホストにOIDCのIDプロバイダ(クラウドVMのメタデータサーバーやSPIFFEなど)が無い場合は、次節の手動フローを使います。
作業ディレクトリを整え、コンテナが非rootのUID 65532で動くためdata/の書き込み権限を与えます。
mkdir -p mcp-tunnel/{config,data}
cd mcp-tunnel
sudo chown 65532:65532 data続いてdocker-compose.yamlを用意します。公式ガイドの参照構成は、イメージをSHA-256ダイジェストで固定し、全コンテナを非root・読み取り専用ファイルシステム・Linux capability全剥奪で動かすハードニング済みの内容です。
以下は要点を抜粋した構成です。@sha256:...のダイジェストは省略表記なので、実際に使う際はdeploy-compose.mdの参照構成から完全な値をコピーしてください。
docker-compose.yaml(プログラム的アクセス、要点を抜粋)
services:
setup:
image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27...
entrypoint: ["/setup"]
command:
- init
- --api-url=https://api.anthropic.com
- --output=dir:/data
- --token-version=1
environment:
- TUNNEL_ID
- ANTHROPIC_FEDERATION_RULE_ID
- ANTHROPIC_ORGANIZATION_ID
- ANTHROPIC_WORKSPACE_ID
- ANTHROPIC_IDENTITY_TOKEN
volumes: ["./data:/data"]
user: "65532:65532"
read_only: true
cap_drop: ["ALL"]
profiles: ["setup"]
cloudflared:
image: cloudflare/cloudflared@sha256:6b599c...
command: tunnel --no-autoupdate run --url http://localhost:8080
environment: ["TUNNEL_TOKEN"]
network_mode: "service:mcp-proxy"
user: "65532:65532"
read_only: true
cap_drop: ["ALL"]
mcp-proxy:
image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27...
volumes:
- ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro
- ./data:/data:ro
user: "65532:65532"
read_only: true
cap_drop: ["ALL"]識別子を設定し、setupコンポーネントを走らせてトンネルを発行します。TUNNEL_IDを指定しなければ新規トンネルが作られ、指定すればConsoleで作った既存トンネルに接続します。
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
export ANTHROPIC_IDENTITY_TOKEN=<この環境のOIDC JWT>
docker compose run --rm setup
export TUNNEL_DOMAIN=$(sudo cat data/tunnel-domain)setup initはdata/に対して冪等です。再実行してもそこに保存済みのトンネルIDとCAをそのまま使い、2つ目のトンネルは作りません。新しいCAが生成・登録されるのは、data/が空かTUNNEL_IDを変えたときだけで、その場合は証明書2枚の上限に注意が要ります。両枠が既に埋まっている場合は、Consoleで古いCAを先にRevokeしてから再実行します。Workload Identity Federationのトークンは既定で1時間の短命トークンなので、setup完了後に手動で失効させる作業はありません。
プロキシ設定を書き、トークンを取り出してから起動します。tunnel_domainは必須で、受信ホスト名からドメイン部分を取り除いてからroutesのサブドメインと照合するために使われます。
cat > config/mcp-proxy.yaml <<EOF
listen_addr: ":8080"
tunnel_domain: ${TUNNEL_DOMAIN}
tls:
cert_file: /data/tls.crt
key_file: /data/tls.key
routes:
docs: http://your-mcp-server:9000
EOF
export TUNNEL_TOKEN=$(sudo cat data/tunnel-token)
docker compose up -d手動フローでデプロイする
プログラム的アクセスを使わない場合、または動作確認だけしたい場合は、Consoleでトークンとドメインを取得してから自分でCA証明書を発行します。証明書のSAN(Subject Alternative Name)には*.<トンネルドメイン>を必ず含める必要があります。
export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE
mkdir -p mcp-tunnel/data && cd mcp-tunnel
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout data/ca.key -out data/ca.crt \
-days 3650 -subj "/CN=mcp-tunnel-ca" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
cat > data/tls.ext <<EOF
subjectAltName = DNS:${TUNNEL_DOMAIN},DNS:*.${TUNNEL_DOMAIN}
extendedKeyUsage = serverAuth
EOF
openssl req -newkey rsa:2048 -nodes \
-keyout data/tls.key -out /tmp/server.csr \
-subj "/CN=${TUNNEL_DOMAIN}"
openssl x509 -req -in /tmp/server.csr \
-CA data/ca.crt -CAkey data/ca.key -CAcreateserial \
-out data/tls.crt -days 90 -extfile data/tls.ext
chmod 644 data/tls.key生成したdata/ca.crtをConsoleの証明書登録画面にアップロードすると、トンネルのステータスがActiveに切り替わります。この先のプロキシ設定とdocker-compose.yamlはプログラム的アクセスとほぼ同じですが、setupサービスが無い分シンプルです。
docker-compose.yamlはTUNNEL_TOKENをホストの環境変数からデフォルト無しで読むので、新しいシェルを開くたび、そしてホストを再起動するたびにexportをやり直す必要があります。複数ホストに展開する場合は、mcp-tunnel/ディレクトリをコピーし、各ホストでTUNNEL_TOKENを設定してdocker compose up -dを実行します。同じトンネルトークンと証明書がすべてのレプリカで共通に使えます。
デプロイを検証する
サンプルMCPサーバーを使っている場合、ルーティングされたURLはhttps://echo.<トンネルドメイン>/mcpになります。Managed AgentセッションかMessages APIから、このURLに向けてリクエストが通ることを確認します。MCP自体の基本的な仕組みはMCPとはにまとめています。通らない場合は、まずルーティング設定(tunnel_domainとroutesのキー)とcloudflaredのログを確認します。プロキシとcloudflaredはそれぞれ別コンテナなので、docker compose logs mcp-proxyとdocker compose logs cloudflaredを個別に見比べると、証明書の不一致なのか、ルーティングの設定ミスなのかを切り分けやすくなります。
トークンをローテーションする
プログラム的アクセスでは、docker-compose.yaml内のsetupサービスの--token-versionを1つ増やし、識別子を再設定してsetupコンポーネントを再実行します。この値をコマンドライン引数ではなくファイルに書いておくのは、次回以降の実行でも新しい値が引き継がれるようにするためです。
# docker-compose.yamlの --token-version を 1 から 2 に書き換えてから実行
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
export ANTHROPIC_IDENTITY_TOKEN=<再発行したOIDC JWT>
docker compose run --rm setup
export TUNNEL_TOKEN=$(sudo cat data/tunnel-token)
docker compose up -d cloudflared手動フローでは、Consoleの詳細ページでRotate tokenを押し、各ホストのTUNNEL_TOKENを更新してdocker compose up -d cloudflaredでcloudflaredを再起動します。
証明書を更新する
サーバー証明書の有効期限の監視と更新は利用者の責任です。プログラム的アクセスでは、次のコマンドで既存のCAを使って新しい証明書に差し替えます。
docker compose run --rm setup renew-cert --output=dir:/data--renew-before=720hを付けると、有効期限まで30日以上残っている場合はコマンドが何もしないno-opになるので、定期実行のcronに乗せても安全です。手動フローでは、保存してある既存のCA(data/ca.key)で新しいサーバー証明書に署名し直し、data/tls.crtを置き換えます。どちらの方式でも、プロキシはtls.cert_fileを定期的にポーリングして自動で読み込み直すため、コンテナの再起動は不要です。
証明書更新自体は無停止で終わりますが、期限切れに気づかないまま放置すると接続が突然切れるので、監視の仕組みは別途用意しておく価値があります。
よくあるつまずき
--urlフラグを付け忘れる。手動フローでcloudflaredにこのフラグが無いと、転送先を持たず全リクエストに503を返しますTUNNEL_TOKENの再exportを忘れる。デフォルト値が無いため、シェルを開き直すたびに再設定が要りますdata/の所有権を合わせ忘れる。コンテナは非rootのUID65532で動くため、ホスト側でchownしていないとボリュームに書き込めません--token-versionを上げずに再実行する。setupバイナリは値が変わっていないローテーションを拒否します- 旧CAを使わずに証明書だけ作り直す。証明書更新は既存のCAで署名し直す作業で、CA自体を作り直すとConsole側の登録もやり直しになります
まとめ
Docker Composeでの実運用は、プログラム的アクセスか手動かで初期セットアップの手数は変わりますが、日々の作業として重いのはトークンローテーションと証明書更新です。プログラム的アクセスなら--token-versionの更新とsetupの再実行、手動ならConsoleでのRotate tokenとcloudflaredの再起動に集約されます。証明書はどちらの方式でも自動リロードされるので、コンテナ再起動の要否を毎回気にする必要はありません。