Docker Compose로 MCP 터널 배포하기
Docker Compose로 MCP 터널 배포하기 (Deploy MCP tunnels with Docker Compose)
이 가이드는 터널 스택을 강화된(hardened) 컨테이너로 단일 호스트에 배포해요. 같은 구성을 가용성을 위해 여러 호스트에 복제할 수도 있어요.
출처: 문서
본문
참고 (Note) MCP 터널은 연구 프리뷰 상태예요. 사용해보려면 액세스를 요청하세요.
이 가이드는 터널 스택을 강화된(hardened) 컨테이너로 단일 호스트에 배포해요. 같은 구성을 가용성을 위해 여러 호스트에 복제할 수 있어요.
시작하기 전에 (Before you begin)
다음이 필요해요:
- 터널. 프로그래매틱 액세스에서는 터널 ID를 제공하지 않으면 설정 구성 요소가 터널을 만들어줘요. 대신 기존 터널에 연결하려면 Console에서 터널을 만들고 터널 ID(
tnl_...)를 기록하세요. 수동 프로비저닝은 항상 Console에서 만든 터널로 시작해요. - 호스트가 Tunnels API에 인증하는 방법.
- 프로그래매틱 액세스 (권장). 터널을 만들 때 Set up programmatic access를 켜거나(설정 구성 요소가 터널을 만들게 하려면 Settings > Workload identity 아래에서 페더레이션 규칙을 직접 만들어), 설정 구성 요소가 Workload Identity Federation을 통해 인증할 수 있게 해요. 페더레이션 규칙 ID(
fdrl_...)와 조직 ID를 기록하세요. - 수동. 프로그래매틱 액세스를 건너뛰어요. Console에서 터널 토큰을 가져오고, CA와 서버 인증서를 직접 생성하고, Console에서 CA를 등록해요.
- 프로그래매틱 액세스 (권장). 터널을 만들 때 Set up programmatic access를 켜거나(설정 구성 요소가 터널을 만들게 하려면 Settings > Workload identity 아래에서 페더레이션 규칙을 직접 만들어), 설정 구성 요소가 Workload Identity Federation을 통해 인증할 수 있게 해요. 페더레이션 규칙 ID(
- Docker와 Docker Compose가 설치된 호스트. 수동 흐름에는
openssl(1.1.1 이상)도 필요해요. - 호스트에서
api.anthropic.com(443 TCP)과 터널 엣지(7844 TCP 및 UDP)로의 아웃바운드 네트워크 연결. 전체 네트워크 요구 사항을 참고하세요. - 하나 이상의 MCP 서버가 실행 중이고 호스트에서
routes아래에 구성할 주소로 접근 가능해야 해요. 아직 없으면 샘플 서버를 사용하세요.
선택: 샘플 MCP 서버 사용하기 (Optional: Use a sample MCP server)
테스트할 MCP 서버가 없다면 이 최소한의 서버를 사용하세요:
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
아래 Install 단계는 mcp-tunnel/로 cd하며 해당 서비스와 라우트를 어디에 추가하는지 알려줘요.
설치 (Install)
이 가이드는 Docker Compose를 사용한 하나의 참조 접근 방식을 제공해요. 조직의 보안 요구 사항에 맞게 조정할 책임은 여러분에게 있어요.
프로그래매틱 액세스 사용 (With programmatic access)
이 경로는 호스트에 OIDC ID 공급자(클라우드 VM 메타데이터 서버, SPIFFE 등)가 있어야 해요. 없으면 Without programmatic access 탭을 사용하세요.
설정 구성 요소는 Workload Identity Federation을 사용해 터널 토큰을 가져오고, CA와 서버 인증서를 생성하고, CA를 Anthropic에 등록해요.
-
배포 디렉터리 준비하기
mkdir -p mcp-tunnel/{config,data} cd mcp-tunnel sudo chown 65532:65532 data컨테이너는 비-root UID
65532로 실행되며data/에 쓰기 접근이 필요해요. -
docker-compose.yaml 작성하기 — compose 파일은 이미지를 SHA-256 다이제스트로 고정하고, 모든 컨테이너를 읽기 전용 파일 시스템으로 비-root로 실행하며, 모든 Linux 기능(capabilities)을 제거하고, 권한 상승을 비활성화해요.
cat > docker-compose.yaml <<'EOF' services: setup: image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 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:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 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 -
터널 프로비저닝하기 — 식별자를 설정해요. 설정 구성 요소가 터널을 만들게 하려면
TUNNEL_ID를 설정하지 말고, Console의 기존 터널에 연결하려면 설정하세요:# export TUNNEL_ID=tnl_... # set to attach to an existing tunnel export ANTHROPIC_FEDERATION_RULE_ID=fdrl_... export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000페더레이션 규칙이 조직 기본값이 아닌 다른 워크스페이스에 범위 지정되어 있다면
ANTHROPIC_WORKSPACE_ID=wrkspc_...도 설정하세요. 그렇지 않으면 설정 구성 요소가 기본 워크스페이스를 사용해요. 자동 생성된 터널은 그 워크스페이스에서 만들어져요.ANTHROPIC_IDENTITY_TOKEN을 이 호스트 ID 공급자의 OIDC JWT로 설정하세요. 공급자별 WIF 가이드에 따라 발급자를 등록하고, 규칙의 subject를 설정하고, 토큰을 만드세요. 규칙의 audience는 민팅(mint)할 때 요청한 audience와 일치해야 해요.설정 구성 요소를 실행하세요:
docker compose run --rm setupsetup init는data/에 대해 멱등적(idempotent)이에요. 다시 실행하면 이미 저장된 터널 ID와 CA를 재사용하고 두 번째 터널을 만들지 않아요. 새 CA는data/가 비어 있거나TUNNEL_ID가 바뀐 경우에만 생성·등록돼요. 이 경우 활성 인증서 한도가 2개이므로, 슬롯이 모두 차 있다면 Console에서 하나를 먼저 폐지하세요.오류가 나면 설정 구성 요소 인증 실패를 참고하세요.
터널 도메인을 가져와 이후 단계용으로 내보내세요:
export TUNNEL_DOMAIN=$(sudo cat data/tunnel-domain) echo "$TUNNEL_DOMAIN"참고 (Note) Workload Identity Federation 토큰은 단기(기본 1시간)이며 자동으로 만료돼요. 설정이 끝난 후 폐지할 것이 없어요.
-
프록시 설정 작성하기 —
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 EOFecho:라우트는 샘플 MCP 서버를 대상으로 해요. 나만의 라우트로 바꾸거나 추가하세요. 사용 가능한 모든 필드는 프록시 설정 레퍼런스를 참고하세요. -
배포 시작하기
export TUNNEL_TOKEN=$(sudo cat data/tunnel-token) docker compose up -d
프로그래매틱 액세스 미사용 (Without programmatic access)
Set up programmatic access를 켜지 않았거나 로컬 개발·테스트를 위해 이 흐름을 사용하세요. setup 서비스는 없어요.
-
Console에서 터널 토큰과 도메인 가져오기 — 터널 상세 페이지에서 Domain(
abcd1234.tunnel.anthropic.com형태)을 복사하고, Token 옆의 눈 아이콘을 클릭해 터널 토큰을 가져온 다음 복사 아이콘으로 복사하세요.둘 다 나머지 가이드를 위한 셸 변수로 설정하세요:
export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE export TUNNEL_TOKEN='eyJ...' -
스캐폴드와 인증서 생성하기
mkdir -p mcp-tunnel/{data,config} cd mcp-tunnel프록시는 평문 WebSocket으로
:8080에서 듣고, 내부 TLS 핸드셰이크는 이 인증서를 사용해 그 WebSocket 스트림 안에서 일어나요. Anthropic은 Console에서 등록한 CA를 기준으로 내부 핸드셰이크를 검증해요. 서버 인증서의 Subject Alternative Name(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 # Allow the non-root proxy container (UID 65532) to read the key from # the bind mount. Without the world-read bit the container cannot open # a host-owned file. chmod 644 data/tls.key -
Console에서 CA 인증서 등록하기 — 터널 상세 페이지에서 Certificates 섹션으로 스크롤하고 Add certificate를 클릭해요. Choose file로
data/ca.crt를 직접 업로드하거나(모달은.pem,.crt,.cer수락) 내용을 붙여넣어요:cat data/ca.crt인증서가 등록되면 터널 상태가 Active로 바뀌어요. CA 인증서 추가하기를 참고하세요.
-
프록시 설정 작성하기 —
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 EOFecho:라우트는 샘플 MCP 서버를 대상으로 해요. 나만의 라우트로 바꾸거나 추가하세요. 사용 가능한 모든 필드는 프록시 설정 레퍼런스를 참고하세요. -
docker-compose.yaml 작성하기 —
network_mode: "service:mcp-proxy"설정은 cloudflared를 프록시의 네트워크 네임스페이스에 두어 cloudflared 컨테이너 안의localhost:8080이 프록시에 도달하게 해요.--url http://localhost:8080플래그는 cloudflared에 전달 대상을 줘요. 이 플래그가 없으면 cloudflared는 들어오는 요청에 대한 라우트가 없어 503을 반환해요.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:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 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 -
배포 시작하기
docker compose up -d
compose 파일은 기본값 없이 호스트 환경에서 TUNNEL_TOKEN을 읽으므로, export는 새 셸마다 그리고 재부팅 후에도 반복해야 해요.
다중 VM 배포의 경우 mcp-tunnel/ 디렉터리를 각 호스트에 복사하고 TUNNEL_TOKEN을 설정한 다음 docker compose up -d를 실행해요. 프로그래매틱 흐름에서 TUNNEL_TOKEN은 $(sudo cat data/tunnel-token)이고, 수동 흐름에서는 Console에서 복사한 값이에요. 같은 터널 토큰과 인증서가 모든 복제본에서 작동해요.
배포 검증하기 (Verify the deployment)
Anthropic 쪽에서 업스트림 MCP 서버를 호출해 종단 간 검증하세요. 터널링된 MCP 서버 사용하기를 참고하세요. 샘플 MCP 서버를 사용하면 라우팅된 URL은 https://echo.<your-tunnel-domain>/mcp예요. 검증이 실패하면 문제 해결을 참고하세요.
업그레이드 (Upgrades)
이 섹션의 명령은 mcp-tunnel/ 배포 디렉터리 안에서 실행하세요.
터널 토큰 순환하기 (Rotate the tunnel token)
프로그래매틱 액세스에서는 setup 서비스 명령의 --token-version을 증가시키고, Workload Identity Federation 식별자를 설정하고, 새 OIDC JWT를 만든 다음 설정 구성 요소를 다시 실행해요:
# 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_... # set only if you set it during install
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
--token-version 인자는 명령줄로 전달하는 대신 docker-compose.yaml에서 편집해요. 그래야 새 값이 이후의 설정 구성 요소 실행에도 유지돼요. 설정 구성 요소는 Workflow Identity Federation으로 인증하므로 폐지할 API 토큰이 없어요.
프로그래매틱 액세스가 없으면 Console의 터널 상세 페이지에서 Rotate token을 클릭한 다음, 각 호스트의 TUNNEL_TOKEN 환경 변수를 업데이트하고 cloudflared를 재시작하세요 (docker compose up -d cloudflared).
경고 (Warning) Rotate token을 클릭하면 현재 토큰이 즉시 무효화돼요. 그 시점부터 모든 호스트에서
TUNNEL_TOKEN을 업데이트하고 cloudflared를 재시작하기 전까지, cloudflared가 재시작하는(크래시, 호스트 재부팅) 호스트는 다시 연결할 수 없어요. 순환 후 각 호스트를 신속히 업데이트하세요.
인증서 갱신 (Certificate renewal)
만료를 모니터링하고 서버 인증서를 만료 전에 갱신할 책임은 여러분에게 있어요.
프로그래매틱 액세스:
docker compose run --rm setup renew-cert --output=dir:/data
CLI 인자는 setup 서비스의 command(즉 init 인자)를 대체하지만 entrypoint는 유지하므로 /setup renew-cert --output=dir:/data가 실행돼요.
팁 (Tip)
--renew-before=720h를 전달하면 유효 기간이 30일 이상 남았을 때 명령이 아무것도 하지 않게 돼요. 고정 일정으로 실행하기에 안전해요.
프로그래매틱 액세스가 없으면 기존 CA로 새 서버 인증서에 서명하고(Console에 등록된 CA는 바뀌지 않아요) data/tls.crt를 교체하세요. 새 셸에서 실행한다면 먼저 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을 폴링해서 자동으로 다시 로드하므로 재시작이 필요 없어요.
더 알아보기 (Learn more)
- 터널링된 MCP 서버 사용하기 — 업스트림 MCP 서버를 Managed Agent 또는 Messages API에 연결하기
- 보안 (Security) — 강화 지침, 자격 증명 순환, 침해 대응
- 문제 해결 (Troubleshooting) — 연결, TLS, 라우팅 문제 진단