使用 Docker Compose 部署 MCP 隧道

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


Note

MCP 隧道是一项研究预览功能。申请访问权限 即可试用。

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

准备工作

您需要:

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

可选:使用示例 MCP 服务器

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

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

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

安装

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

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

  1. 准备部署目录

    mkdir -p mcp-tunnel/{config,data}
    cd mcp-tunnel
    sudo chown 65532:65532 data
    

    容器以非 root UID 65532 运行,需要对 data/ 的写入权限。

  2. 编写 docker-compose.yaml

    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 服务器,请将其作为服务追加:

    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
    
  3. 配置隧道

    设置 控制台创建隧道流程 中的标识符:

    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 指南 注册颁发者、设置规则的主题并签发令牌;规则的受众必须与您在签发时请求的受众匹配。如果此主机没有身份提供商,请切换到 Without programmatic access 选项卡。

    运行 setup:

    docker compose run --rm setup
    

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

    如果出现错误,请参阅 Setup Job 身份验证失败

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

    export TUNNEL_DOMAIN=$(sudo cat data/tunnel-domain)
    echo "$TUNNEL_DOMAIN"
    
    Note

    Workload Identity Federation 令牌是短期有效的(默认一小时),会自动过期;setup 完成后无需撤销任何内容。

  4. 编写代理配置

    tunnel_domain必需的:代理使用它从传入的主机名中剥离域名后缀,然后在 routes 中查找子域名。routes 是从子域名到上游 URL 的扁平映射。

    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 服务器;请替换为(或添加)您自己的路由。有关所有可用字段,请参阅 代理配置 参考。

  5. 启动部署

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

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

  1. 从控制台获取隧道令牌和域名

    在隧道详情页上,复制 Domain(格式类似 abcd1234.tunnel.anthropic.com),然后点击 Token 旁边的眼睛图标获取隧道令牌,并使用复制图标复制。

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

    export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE
    export TUNNEL_TOKEN='eyJ...'
    
  2. 生成脚手架和证书

    mkdir -p mcp-tunnel/{data,config}
    cd mcp-tunnel
    

    代理在 :8080 上监听纯 WebSocket;内部 TLS 握手使用这些证书在该 WebSocket 流内部进行。Anthropic 根据您在控制台中注册的 CA 验证内部握手。服务器证书的 SAN 必须包含 *.<tunnel-domain>,参见 证书要求

    # 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
    
  3. 在控制台中注册 CA 证书

    在隧道详情页上,滚动到 Certificates 部分并点击 Add certificate。使用 Choose file 直接上传 data/ca.crt(对话框接受 .pem.crt.cer),或粘贴其内容:

    cat data/ca.crt
    

    注册证书后,隧道状态会变为 Active。参见 添加 CA 证书

  4. 编写代理配置

    tunnel_domain必需的:代理使用它从传入的主机名中剥离域名后缀,然后在 routes 中查找子域名。routes 是从子域名到上游 URL 的扁平映射,而非列表。

    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 服务器;请替换为(或添加)您自己的路由。有关所有可用字段,请参阅 代理配置 参考。

  5. 编写 docker-compose.yaml

    在此流程中,服务端未配置入口规则,因此 cloudflared 需要一个明确的本地目标。共享代理的网络命名空间并传递 --url http://localhost:8080,以便 cloudflared 在同一网络命名空间中将流量转发到代理;如果没有此配置,请求到达 cloudflared 后没有路由,会返回 503 错误(对调用者表现为 500)。

    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 服务器,请将其作为服务追加:

    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
    
  6. 启动部署

    docker compose up -d
    

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

验证部署

从 Anthropic 端调用路由的服务器来验证端到端连接:参见 使用隧道 MCP 服务器。使用 示例 MCP 服务器 时,路由 URL 为 https://echo.<your-tunnel-domain>/mcp。如果验证失败,请参阅 故障排除

升级

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

轮换隧道令牌

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

# 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 重启的主机(崩溃、主机重启)都无法重新连接。轮换后请尽快更新每台主机。

证书续期

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

使用编程访问:

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

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

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

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 并自动重新加载,因此无需重启。

后续步骤