Claude Media
MCP tunnelsをDocker Composeでデプロイする手順 — トークンと証明書の更新

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 ComposeHelm
可用性Docker Compose同じ構成を複数ホストへ手動複製HelmDeploymentのレプリカで水平化
証明書更新の自動化Docker Composesetup renew-certを自分でcron登録Helmプログラム的アクセスなら日次CronJobが標準搭載

ホストの条件で認証方式を決める

方式の分かれ目は、そのホストがOIDCのIDプロバイダ(クラウドVMのメタデータサーバーやSPIFFEなど)からJWTを受け取れるかどうかです。受け取れるならプログラム的アクセス、受け取れなければ手動フローになります。ローカルでの動作確認にも手動フローが使えます。

くらべる

2つの認証方式

公式の推奨

プログラム的アクセス

setupコンポーネントがWorkload Identity Federationでトークンを取得し、CAとサーバー証明書を生成してCAをAnthropicに登録します。手元でopensslを打つ必要がなく、証明書更新もrenew-certの1コマンドです。

IDプロバイダが無いホスト向け

手動フロー

Consoleでトークンとドメインを取り、opensslでCAとサーバー証明書を作り、CAをConsoleに登録します。setupサービスが無いぶんComposeファイルは短くなりますが、更新は自分の手で回します。

事前に必要なもの

  • トンネル。プログラム的アクセスではトンネルIDを指定しなければsetupコンポーネントが作成します。手動フローでは必ずConsoleで先に作成し、トンネルIDを控えます
  • Tunnels APIへの認証手段。プログラム的アクセスならfederation ruleのID(fdrl_...)と組織ID、手動ならConsoleから取得するトンネルトークンです
  • 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 tunnelsは研究プレビューで、公式の「API and data retention」ページの機能別の一覧では、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ルートを検証先として使います。

プログラム的アクセスでデプロイする

IDプロバイダのあるホストでは、まず作業ディレクトリを整え、コンテナが非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:...のダイジェストは省略表記なので、実際に使う際は公式Docker Composeガイドの参照構成から完全な値をコピーしてください。

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証明書を自分で発行します。

手順

手動フローの5ステップ

  1. 1

    Consoleでドメインとトークンを取る

    トンネル詳細ページのDomain(abcd1234.tunnel.anthropic.comの形)をコピーし、Tokenの目のアイコンで表示したトークンもコピーします。

  2. 2

    CAとサーバー証明書を作る

    下のコマンドでdata/に出力します。サーバー証明書のSAN(Subject Alternative Name)には*.<トンネルドメイン>を含めます。

  3. 3

    CAをConsoleに登録する

    CertificatesのAdd certificateでdata/ca.crtを選びます。ステータスがActiveになります。

  4. 4

    プロキシ設定とComposeファイルを書く

    config/mcp-proxy.yamlと、cloudflaredに--urlを付けたdocker-compose.yamlを用意します。

  5. 5

    起動する

    TUNNEL_TOKENをexportしてdocker compose up -dを実行します。

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" \
  -addext "subjectKeyIdentifier=hash"
 
cat > data/tls.ext <<EOF
subjectAltName = DNS:${TUNNEL_DOMAIN},DNS:*.${TUNNEL_DOMAIN}
authorityKeyIdentifier = keyid,issuer
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

CAにsubjectKeyIdentifier、サーバー証明書にauthorityKeyIdentifierを付けているのは、自前のPKIで発行する証明書の要件だからです。サーバー証明書はCAが直接署名し(中間CAは不可)、このAKIがCAのSKIと一致している必要があります。CAは単一の証明書でRSA 2048bit以上(またはECDSA P-256以上)、PEMで8kB以内です。

この先のプロキシ設定とdocker-compose.yamlはプログラム的アクセスとほぼ同じで、setupサービスが無いぶん短くなります。

docker-compose.yamlはTUNNEL_TOKENをホストの環境変数からデフォルト無しで読むので、新しいシェルを開くたび、そしてホストを再起動するたびにexportをやり直す必要があります。複数ホストに展開する場合は、mcp-tunnel/ディレクトリをコピーし、各ホストでTUNNEL_TOKENを設定してdocker compose up -dを実行します。同じトンネルトークンと証明書がすべてのレプリカで共通に使えます。

routesとupstreamの書き方で起きること

routesはリストではなく、サブドメインからupstreamへの対応表です。upstreamの値はscheme://host:portの形に限られ、ポートは省略できません。パスを付けると設定の読み込み時にinvalid upstream (must be scheme://host:port)で弾かれます。リクエストのパスとクエリはそのままupstreamに転送されるので、パスはルートの側に書かずMCPサーバーのURLに委ねます。

tunnel_domainを空にすると、routesのキーは完全なホスト名で書く必要があります。サブドメインだけでキーを書くならtunnel_domainが要ります。

もう1つ見落としやすいのが、upstreamへの接続先の制限です。upstream.allowed_ipsの既定値はRFC1918の私設アドレス帯で、それ以外のIPに解決されるMCPサーバーへは接続しません。パブリックIPのサーバーへ向けるときは、許可するCIDRをupstream.allowed_ipsに書きます。https://のupstreamにはupstream.tls.ca_fileかupstream.tls.include_system_casのどちらかも要ります。

デプロイを検証する

サンプルMCPサーバーを使っている場合、ルーティングされたURLはhttps://echo.<トンネルドメイン>/mcpになります。Managed AgentセッションかMessages APIから、このURLに向けてリクエストが通ることを確認します。MCP自体の基本的な仕組みはMCPとはにまとめています。手元からcurl https://<プロキシ>:8080を打つ検証はできません。リスナーは平文のWebSocketで、TLSはそのストリームの内側で始まるため、wrong version numberで失敗するのが正常です。

通らないときは、プロキシとcloudflaredが別コンテナなのでdocker compose logs mcp-proxyとdocker compose logs cloudflaredを別々に見ます。次の表が、出力された文言から原因を引く早見表です。

ログや症状原因直し方
mcp-proxyがopen /data/tls.key: permission deniedで終了原因鍵が0600で、非rootのコンテナが読めない直し方chmod 644 data/tls.key
mcp-proxyがcannot unmarshal !!seq into map[string]stringで終了原因routesがYAMLのリスト直し方routes: { name: http://host:port }の形にする
mcp-proxyのログにno route for host原因tunnel_domainが割り当てドメインと違う、または設定を変えて再起動していない直し方Consoleのドメインと一致させ、docker compose restart mcp-proxy
mcp-proxyのログにIP validation failed: <ip> is not a private address原因upstreamがRFC1918の外に解決される直し方upstream.allowed_ipsを設定する
cloudflaredのログにNo ingress rules were defined原因cloudflaredに転送先が無い直し方--url http://localhost:8080とnetwork_mode: "service:mcp-proxy"を付ける

症状が増えたときはMCP tunnelsに接続できないときのトラブルシューティングに、OAuthやIP許可リスト絡みの長い切り分けがあります。

手動フローの証明書の出力例

OpenSSL 3.2.1で手順どおりCA、サーバー証明書の順に作ると、次の出力になります。TUNNEL_DOMAINはabcd1234.tunnel.anthropic.comの例です。

$ openssl x509 -in data/tls.crt -noout -ext subjectAltName,authorityKeyIdentifier,extendedKeyUsage
X509v3 Subject Alternative Name:
    DNS:abcd1234.tunnel.anthropic.com, DNS:*.abcd1234.tunnel.anthropic.com
X509v3 Authority Key Identifier:
    BB:7D:E0:6F:D9:4D:42:FF:5F:F0:70:18:E5:37:19:7C:FB:06:11:8E
X509v3 Extended Key Usage:
    TLS Web Server Authentication
$ openssl verify -CAfile data/ca.crt -verify_hostname echo.abcd1234.tunnel.anthropic.com data/tls.crt
data/tls.crt: OK

AKIの値はCAのsubjectKeyIdentifierと同じ並び(BB:7D:...:8E)で、ワイルドカードSANのおかげでecho.のような任意のルート名のホスト名検証も通ります。ただし、OpenSSL 3.2.1ではsubjectKeyIdentifier=hashとauthorityKeyIdentifierを指定しない場合も同じ値が自動で付きます。自動で付く版でも、要件に書かれた拡張なので明示しておくのが確実です。

もう1つ、生成直後のdata/tls.keyは-rw-------(600)で作られます。非rootのコンテナは別ユーザーで動くので、chmod 644を忘れるとここが詰まります。前節の表にあるpermission deniedの正体です。

Compose側では、TUNNEL_TOKENを未設定のままdocker compose configを実行しても、Docker Compose v2.2.3ではエラーも警告も出ません。出力にTUNNEL_TOKEN: nullと出るだけです。設定の構文検査ではexport漏れを拾えないので、up -dの前にecho "${TUNNEL_TOKEN:+設定済み}"のような確認を挟む手があります。

トークンをローテーションする

プログラム的アクセスでは、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を付けたコマンドなら、cronに乗せても安全です。手動フローでは、保存してある既存のCA(data/ca.key)で新しいサーバー証明書に署名し直し、data/tls.crtを置き換えます。

数字

証明書更新の数字

  • サーバー証明書の有効期間

    90日

    手順の`-days 90`の値

  • renew-beforeの閾値

    720h

    残り30日以上ならno-op

  • CAの登録枠

    2枚

    新旧を並走させて入れ替える

  • 更新後の再起動

    不要

    プロキシがtls.cert_fileをポーリング

期限の監視は自分で用意します。期限切れに気づかないと接続が突然切れるためです。

よくあるつまずき

  • TUNNEL_TOKENの再exportを忘れる。デフォルト値が無く、未設定でもdocker compose configは通るため、シェルを開き直すたびに再設定が要ります
  • data/の所有権を合わせ忘れる。コンテナは非rootのUID 65532で動くため、ホスト側でchownしていないとボリュームに書き込めません
  • --token-versionを上げずに再実行する。setupバイナリは値が変わっていないローテーションを拒否します
  • 旧CAを使わずに証明書だけ作り直す。証明書更新は既存のCAで署名し直す作業で、CA自体を作り直すとConsole側の登録もやり直しになります

まとめ

IDプロバイダを持つホストならプログラム的アクセス、持たないホストなら手動フローを選び、起動後に困ったらログの文言を表と突き合わせるのが近道です。運用で手がかかるのはトークンの回転と証明書の更新で、プログラム的アクセスなら--renew-before=720h付きのrenew-certをそのまま定期実行に乗せられます。自前でホストせずPaaSに載せる選択肢としてはRender MCPサーバーでWebサービスとDBを管理するがあります。

この記事を共有:XはてブLinkedIn
MCP をもっと見る →