# 使用 Docker Compose 部署 MCP 隧道

使用 Docker Compose 在虚拟机上安装 MCP 隧道技术栈。

---

<Note>
  MCP 隧道是一项研究预览功能。[申请访问权限](https://claude.com/form/claude-managed-agents) 即可试用。
</Note>

本指南将 MCP 隧道技术栈作为加固容器部署在单台主机上。相同的配置可以复制到多台主机上以提高可用性。

## 准备工作

您需要：

- **在控制台中创建的隧道。**请按照 [创建隧道](/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel) 的步骤操作，并记录隧道 ID（`tnl_...`）。
- **一种让主机向 Tunnels API 进行身份验证的方式。**
  - **编程访问（推荐）。**在创建隧道时开启 **Set up programmatic access**，这样 `setup` 服务可以通过 Workload Identity Federation 进行身份验证。记录联邦规则 ID（`fdrl_...`）和您的组织 ID。
  - **手动方式。**跳过编程访问。您将 [从控制台获取隧道令牌](/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details)，自行生成 CA 和服务器证书，[在控制台中注册 CA](/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate)。
- **安装了 Docker 和 Docker Compose 的主机。**手动方式还需要 `openssl`（1.1.1 或更新版本）。
- **从主机到 `api.anthropic.com`（443 TCP）和隧道边缘（7844 TCP 和 UDP）的出站网络连接。**参见完整的 [网络要求](/docs/en/agents-and-tools/mcp-tunnels/overview#network-requirements)。
- **一个或多个 MCP 服务器**正在运行，并且可以从主机上通过您将在 `routes` 下配置的地址访问。如果您还没有，[使用示例服务器](#optional-use-a-sample-mcp-server)。

## 可选：使用示例 MCP 服务器

如果您没有可用于测试的 MCP 服务器，请使用这个最小示例：

```bash
mkdir -p mcp-tunnel/{config,data}
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
```

以下安装步骤会 `cd` 到 `mcp-tunnel/` 目录，并注明在何处添加相应的服务和路由。

## 安装

本指南提供了一种使用 Docker Compose 的参考方案。您有责任根据组织的安全要求进行调整。

<Tabs>
<Tab title="使用编程访问">

`setup` 服务使用 Workload Identity Federation 获取隧道令牌、生成 CA 和服务器证书，并向 Anthropic 注册 CA。

<Steps>
  <Step title="准备部署目录">
    
    ```bash nocheck
    mkdir -p mcp-tunnel/{config,data}
    cd mcp-tunnel
    sudo chown 65532:65532 data
    ```

    容器以非 root UID `65532` 运行，需要对 `data/` 的写入权限。
  </Step>

  <Step title="编写 docker-compose.yaml">

    ```bash
    cat > docker-compose.yaml <<'EOF'
    services:
      setup:
        image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:6b9adedbf2763143ec72f106ecaf0ce7fd3294e89b208f54a1db97a33d14c5ba
        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
        security_opt:
          - no-new-privileges:true
        cap_drop:
          - ALL
        profiles: ["setup"]

      cloudflared:
        image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0
        command: tunnel --no-autoupdate run --url http://localhost:8080
        environment:
          - TUNNEL_TOKEN
        # Share the proxy's netns so localhost:8080 reaches it.
        network_mode: "service:mcp-proxy"
        restart: unless-stopped
        user: "65532:65532"
        read_only: true
        security_opt:
          - no-new-privileges:true
        cap_drop:
          - ALL
        stop_grace_period: 30s
        logging:
          options:
            max-size: "10m"
            max-file: "3"

      mcp-proxy:
        image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:6b9adedbf2763143ec72f106ecaf0ce7fd3294e89b208f54a1db97a33d14c5ba
        volumes:
          - ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro
          - ./data:/data:ro
        restart: unless-stopped
        user: "65532:65532"
        read_only: true
        security_opt:
          - no-new-privileges:true
        cap_drop:
          - ALL
        stop_grace_period: 30s
        logging:
          options:
            max-size: "10m"
            max-file: "3"
    EOF
    ```

    Compose 文件通过 SHA-256 摘要固定镜像，以非 root 用户运行每个容器，使用只读文件系统，丢弃所有 Linux 功能，并禁用权限提升。

    如果您使用的是 [示例 MCP 服务器](#optional-use-a-sample-mcp-server)，请将其作为服务追加：

    ```bash
    cat >> docker-compose.yaml <<'EOF'

      hello-mcp:
        image: python:3.13-slim
        working_dir: /app
        volumes:
          - ./hello_server.py:/app/hello_server.py:ro
        command: sh -c "pip install --quiet mcp && python hello_server.py"
        restart: unless-stopped
    EOF
    ```
  </Step>

  <Step title="配置隧道">
    设置 [控制台创建隧道流程](/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel) 中的标识符：

    ```bash
    export TUNNEL_ID=tnl_...
    export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
    export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
    ```

    如果您的联邦规则范围为组织默认工作区以外的工作区，还需设置 `ANTHROPIC_WORKSPACE_ID=wrkspc_...`；否则 setup 使用默认工作区。

    将 `ANTHROPIC_IDENTITY_TOKEN` 设置为此主机身份提供商的 OIDC JWT。按照 [您的提供商的 WIF 指南](/docs/en/manage-claude/workload-identity-federation#identity-providers) 注册颁发者、设置规则的主题并签发令牌；规则的受众必须与您在签发时请求的受众匹配。如果此主机没有身份提供商，请切换到 **Without programmatic access** 选项卡。

    运行 setup：

    ```bash
    docker compose run --rm setup
    ```

    `setup init` 对 `data/` 是幂等的：重新运行会复用现有 CA 并跳过注册。只有在 `data/` 为空或 `TUNNEL_ID` 已更改时才会生成和注册新的 CA；在这种情况下，两个活跃证书的限制适用，因此如果两个槽位都已满，请先在控制台中撤销一个。

    如果出现错误，请参阅 [Setup Job 身份验证失败](/docs/en/agents-and-tools/mcp-tunnels/troubleshooting#setup-job-authentication-failures)。

    获取隧道域名并导出以供后续步骤使用：

    
    ```bash nocheck
    export TUNNEL_DOMAIN=$(sudo cat data/tunnel-domain)
    echo "$TUNNEL_DOMAIN"
    ```

    <Note>
      Workload Identity Federation 令牌是短期有效的（默认一小时），会自动过期；setup 完成后无需撤销任何内容。
    </Note>
  </Step>

  <Step title="编写代理配置">
    `tunnel_domain` 是**必需的**：代理使用它从传入的主机名中剥离域名后缀，然后在 `routes` 中查找子域名。`routes` 是从子域名到上游 URL 的扁平映射。

    ```bash
    cat > config/mcp-proxy.yaml <<EOF
    listen_addr: ":8080"
    log_level: info
    shutdown_timeout: 30s
    tunnel_domain: ${TUNNEL_DOMAIN}
    tls:
      cert_file: /data/tls.crt
      key_file: /data/tls.key
    routes:
      echo: http://hello-mcp:9000
    EOF
    ```

    `echo:` 路由指向 [示例 MCP 服务器](#optional-use-a-sample-mcp-server)；请替换为（或添加）您自己的路由。有关所有可用字段，请参阅 [代理配置](/docs/en/agents-and-tools/mcp-tunnels/reference#proxy-configuration) 参考。
  </Step>

  <Step title="启动部署">
    
    ```bash nocheck
    export TUNNEL_TOKEN=$(sudo cat data/tunnel-token)
    docker compose up -d
    ```
  </Step>
</Steps>

</Tab>
<Tab title="不使用编程访问">

如果您没有开启 **Set up programmatic access**，或者用于本地开发和测试，请使用此流程。没有 `setup` 服务。

<Steps>
  <Step title="从控制台获取隧道令牌和域名">
    在隧道详情页上，复制 **Domain**（格式类似 `abcd1234.tunnel.anthropic.com`），然后点击 **Token** 旁边的眼睛图标获取隧道令牌，并使用复制图标复制。

    将两者设置为本指南其余部分的 shell 变量：

    ```bash
    export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE
    export TUNNEL_TOKEN='eyJ...'
    ```
  </Step>

  <Step title="生成脚手架和证书">
    ```bash
    mkdir -p mcp-tunnel/{data,config}
    cd mcp-tunnel
    ```

    代理在 `:8080` 上监听纯 WebSocket；内部 TLS 握手使用这些证书在该 WebSocket 流**内部**进行。Anthropic 根据您在控制台中注册的 CA 验证内部握手。服务器证书的 SAN 必须包含 `*.<tunnel-domain>`，参见 [证书要求](/docs/en/agents-and-tools/mcp-tunnels/reference#certificate-requirements)。

    ```bash
    # Self-signed CA. Explicit extensions so it satisfies the certificate
    # requirements regardless of distro openssl.cnf defaults.
    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"

    # Extension file for the server certificate. Using -extfile (instead of
    # -copy_extensions, which is OpenSSL 3.0+ only) keeps this working on
    # OpenSSL 1.1.x.
    cat > data/tls.ext <<EOF
    subjectAltName = DNS:${TUNNEL_DOMAIN},DNS:*.${TUNNEL_DOMAIN}
    authorityKeyIdentifier = keyid,issuer
    extendedKeyUsage = serverAuth
    EOF

    # Server certificate signed by the CA
    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
    ```

  </Step>

  <Step title="在控制台中注册 CA 证书">
    在隧道详情页上，滚动到 **Certificates** 部分并点击 **Add certificate**。使用 **Choose file** 直接上传 `data/ca.crt`（对话框接受 `.pem`、`.crt` 和 `.cer`），或粘贴其内容：

    ```bash
    cat data/ca.crt
    ```

    注册证书后，隧道状态会变为 **Active**。参见 [添加 CA 证书](/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate)。
  </Step>

  <Step title="编写代理配置">
    `tunnel_domain` 是**必需的**：代理使用它从传入的主机名中剥离域名后缀，然后在 `routes` 中查找子域名。`routes` 是从子域名到上游 URL 的扁平映射，而非列表。

    ```bash
    cat > config/mcp-proxy.yaml <<EOF
    listen_addr: ":8080"
    log_level: info
    tunnel_domain: ${TUNNEL_DOMAIN}
    tls:
      cert_file: /data/tls.crt
      key_file: /data/tls.key
    routes:
      echo: http://hello-mcp:9000
    EOF
    ```

    `echo:` 路由指向 [示例 MCP 服务器](#optional-use-a-sample-mcp-server)；请替换为（或添加）您自己的路由。有关所有可用字段，请参阅 [代理配置](/docs/en/agents-and-tools/mcp-tunnels/reference#proxy-configuration) 参考。
  </Step>

  <Step title="编写 docker-compose.yaml">
    在此流程中，服务端未配置入口规则，因此 cloudflared 需要一个明确的本地目标。共享代理的网络命名空间并传递 `--url http://localhost:8080`，以便 cloudflared 在同一网络命名空间中将流量转发到代理；如果没有此配置，请求到达 cloudflared 后没有路由，会返回 503 错误（对调用者表现为 500）。

    ```bash
    cat > docker-compose.yaml <<'EOF'
    services:
      cloudflared:
        image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0
        # --url is required: no ingress rules are pushed in the manual flow,
        # so without it cloudflared 503s every request.
        command: tunnel --no-autoupdate run --url http://localhost:8080
        environment:
          - TUNNEL_TOKEN
        # Share the proxy's netns so localhost:8080 reaches it.
        network_mode: "service:mcp-proxy"
        restart: unless-stopped
        user: "65532:65532"
        read_only: true
        security_opt:
          - no-new-privileges:true
        cap_drop:
          - ALL
        stop_grace_period: 30s
        logging:
          options:
            max-size: "10m"
            max-file: "3"

      mcp-proxy:
        image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:6b9adedbf2763143ec72f106ecaf0ce7fd3294e89b208f54a1db97a33d14c5ba
        volumes:
          - ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro
          - ./data:/data:ro
        restart: unless-stopped
        user: "65532:65532"
        read_only: true
        security_opt:
          - no-new-privileges:true
        cap_drop:
          - ALL
        stop_grace_period: 30s
        logging:
          options:
            max-size: "10m"
            max-file: "3"
    EOF
    ```

    如果您使用的是 [示例 MCP 服务器](#optional-use-a-sample-mcp-server)，请将其作为服务追加：

    ```bash
    cat >> docker-compose.yaml <<'EOF'

      hello-mcp:
        image: python:3.13-slim
        working_dir: /app
        volumes:
          - ./hello_server.py:/app/hello_server.py:ro
        command: sh -c "pip install --quiet mcp && python hello_server.py"
        restart: unless-stopped
    EOF
    ```
  </Step>

  <Step title="启动部署">
    ```bash
    docker compose up -d
    ```
  </Step>
</Steps>

</Tab>
</Tabs>

对于多虚拟机部署，请将部署目录复制到每台主机，设置 `TUNNEL_TOKEN`（编程流程中为 `$(sudo cat data/tunnel-token)`，手动流程中为显示的值），然后运行 `docker compose up -d`。Compose 文件从环境中读取 `TUNNEL_TOKEN`，没有默认值，因此导出操作必须在每个新的 shell 中执行，包括重启之后。相同的隧道令牌和证书适用于所有副本。

## 验证部署

从 Anthropic 端调用路由的服务器来验证端到端连接：参见 [使用隧道 MCP 服务器](/docs/en/agents-and-tools/mcp-tunnels/overview#use-the-tunneled-mcp-servers)。使用 [示例 MCP 服务器](#optional-use-a-sample-mcp-server) 时，路由 URL 为 `https://echo.<your-tunnel-domain>/mcp`。如果验证失败，请参阅 [故障排除](/docs/en/agents-and-tools/mcp-tunnels/troubleshooting)。

## 升级

在 `mcp-tunnel/` 部署目录中运行本节中的命令。

### 轮换隧道令牌

使用编程访问时，递增 `setup` 服务命令中的 `--token-version`，设置 Workload Identity Federation 标识符，签发新的 OIDC JWT（自安装后已过期），然后重新运行 setup：

```bash nocheck
# Edit docker-compose.yaml: increment the integer in the setup service's
# --token-version argument (for example, --token-version=1 to
# --token-version=2). The setup binary refuses to rotate when the value
# hasn't changed.

export TUNNEL_ID=tnl_...
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
# export ANTHROPIC_WORKSPACE_ID=wrkspc_...   # if your rule is workspace-scoped
# Re-mint ANTHROPIC_IDENTITY_TOKEN per the WIF provider guide for your
# environment (it will have expired since install).
export ANTHROPIC_IDENTITY_TOKEN=...

docker compose run --rm setup

export TUNNEL_TOKEN=$(sudo cat data/tunnel-token)
docker compose up -d cloudflared
```

setup 程序通过 Workload Identity Federation 进行身份验证；无需撤销 API 令牌。

不使用编程访问时，在控制台的隧道详情页上点击 **Rotate token**，然后在每台主机上更新 `TUNNEL_TOKEN` 环境变量并重启 cloudflared（`docker compose up -d cloudflared`）。

<Warning>
  点击 **Rotate token** 会立即撤销当前令牌。在该时刻与在每台主机上更新 `TUNNEL_TOKEN` 并重启 cloudflared 之间，任何 cloudflared 重启的主机（崩溃、主机重启）都无法重新连接。轮换后请尽快更新每台主机。
</Warning>

### 证书续期

您有责任监控过期时间并在服务器证书过期前续期。

使用编程访问：

```bash
docker compose run --rm setup renew-cert --output=dir:/data
```

<Tip>
  传递 `--renew-before=720h` 可使命令在剩余有效期超过 30 天时跳过续期。这使其可以安全地按固定计划运行。
</Tip>

不使用编程访问时，使用您现有的 CA（在控制台中注册的 CA 不会更改）签发新的服务器证书并替换 `data/tls.crt`。如果从新的 shell 运行，请先设置 `TUNNEL_DOMAIN`。

```bash
export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE
openssl req -new -key 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
```

在任一流程中，代理都会轮询 `tls.cert_file` 并自动重新加载，因此无需重启。

## 后续步骤

<CardGroup cols={2}>
  <Card title="使用隧道 MCP 服务器" icon="link" href="/docs/en/agents-and-tools/mcp-tunnels/overview#use-the-tunneled-mcp-servers">
    将路由的 MCP 服务器附加到 Managed Agent 或 Messages API。
  </Card>
  <Card title="安全" icon="lock" href="/docs/en/agents-and-tools/mcp-tunnels/security">
    加固指南、凭据轮换和泄露响应。
  </Card>
  <Card title="故障排除" icon="wrench" href="/docs/en/agents-and-tools/mcp-tunnels/troubleshooting">
    诊断连接、TLS 和路由问题。
  </Card>
</CardGroup>
