cloudflared 구조 완전 해설ssh alpha docker ps -a 에 잡히는 cloudflared 컨테이너는 6개. 전부 동일 이미지 cloudflare/cloudflared:latest, 전부 동일 Cloudflare 계정(a=33cdf220…) 소속, 전부 restart: unless-stopped. 각 컨테이너는 하나의 독립 서비스 스택에 붙어 그 스택을 외부로 노출하는 egress-only 리버스 프록시다.
| 컨테이너 | 공개 호스트 | Origin(내부) | 네트워크 | 패턴 |
|---|---|---|---|---|
grindaai-kms-astro-cloudflared | kms.rinda.ai | web:80 | grindaai-kms-astro-net | A config |
grindaai-mcp-cloudflared | mcp.rinda.ai | mcp:8080 | grindaai-mcp-net | A config |
rinda-html-cloudflared | html.rinda.ai | app:3300 | rinda-html-net | B token |
ai-real-estate-cloudflared-1 | admin.eodisalji.com 외 | frontend:5173 등 | estate-network | B token |
pps-infisical-cloudflared | (Infisical 셀프호스트) | infisical 서비스 | infisical-standalone_default | B token |
kailab-cloudflared | kailab.grinda.ai | host 포트 | host | B token |
모든 스택의 핵심은 호스트에 inbound 포트를 하나도 열지 않는다는 것이다. cloudflared 는 서버 → Cloudflare 엣지 방향으로 아웃바운드 QUIC 연결(UDP 7844) 만 맺고, 트래픽은 그 연결의 역방향으로 흘러 들어온다.
networks: [htmlnet])하므로 app:3300 같은 컨테이너 DNS 이름으로 직결. 호스트 포트 게시 불필요.Registered tunnel connection … protocol=quic location=icn06. 인천 엣지에 4개 커넥션(connIndex 0~3) HA 로 상시 유지.겉보기 CMD 는 비슷하지만 ingress(어떤 호스트를 어디로 보낼지) 규칙이 어디에 사는가가 근본적으로 다르다.
ingress 규칙이 서버 파일에 산다.
tunnel --config /etc/cloudflared/config.yml run./cloudflared:/etc/cloudflared:roconfig.yml(터널ID+ingress) + creds.json(터널 시크릿)ingress 규칙이 Cloudflare 대시보드에 산다.
tunnel run --token <JWT> 또는 TUNNEL_TOKEN env{a,t,s} (계정·터널ID·시크릿)# grindaai-kms-astro/docker-compose.yml
cloudflared:
image: cloudflare/cloudflared:latest
container_name: grindaai-kms-astro-cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate --config /etc/cloudflared/config.yml run
volumes:
- ./cloudflared:/etc/cloudflared:ro # config.yml + creds.json 주입 (읽기전용)
depends_on: [web] # origin 이 먼저 떠야 함
networks: [kmsastronet] # origin 과 같은 네트워크 → web:80 직결
# /home/ec2-user/grindaai-kms-astro/cloudflared/config.yml
tunnel: cfbb3bca-caee-46d0-b007-fd9c172b357c # 터널 UUID
credentials-file: /etc/cloudflared/creds.json
ingress:
- hostname: kms.rinda.ai
service: http://web:80 # 이 호스트로 온 트래픽을 web:80 으로
- service: http_status:404 # catch-all (필수, 맨 마지막)
{
"AccountTag": "33cdf2206b3653878e6bca1694506a49",
"TunnelID": "cfbb3bca-caee-46d0-b007-fd9c172b357c",
"TunnelSecret": "Fo8Q1PHrkR…" ← 재발급 시크릿 (gitignore, 서버에만)
}
cloudflared tunnel create grindaai-kms-astro → 새 creds.json 생성. config.yml 의 tunnel UUID 도 함께 갱신.# rinda-html-share/docker-compose.yml — 완전 독립 스택
app:
image: rinda-html-share
container_name: rinda-html-app
# ports 섹션 없음 → 호스트로 아무 포트도 안 염 (충돌 0)
networks: [htmlnet]
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN:?TUNNEL_TOKEN required in .env} # 없으면 기동 실패
depends_on: [app]
networks: [htmlnet] # app:3300 직결
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --token ${CLOUDFLARE_TUNNEL_TOKEN}
depends_on: [backend, frontend]
networks: [estate-network]
docker inspect·ps 에 토큰이 그대로 노출된다(위 실측에서도 보임). env 방식(TUNNEL_TOKEN)이 더 안전 — 프로세스 인자에 시크릿이 안 남는다.eyJhIjoiMzNj… 를 base64 디코드하면:
{
"a": "33cdf2206b3653878e6bca1694506a49", # account tag (6개 터널 전부 동일 = 같은 CF 계정)
"t": "84252dff-b3ed-4f35-9490-6848604e1df5", # tunnel id
"s": "+wVEJtAa/nyD… (redacted)" # tunnel secret
}
즉 token 방식 = creds.json 의 세 필드를 base64 로 뭉친 것. 차이는 ingress 를 어디서 관리하느냐뿐이다.
network: host다른 5개는 전용 bridge 네트워크를 쓰지만 kailab 만 host 네트워크. 온프렘 H100(청사 air-gapped) 접근 패턴상 호스트 포트로 직접 붙기 위함. origin 을 서비스명이 아닌 localhost:PORT 로 지정.
로그의 ingressRule=5 는 대시보드에 ingress 규칙이 6개(0~5) 걸려 있다는 뜻. admin.eodisalji.com 은 frontend:5173 로, API·SSE 등 여러 호스트를 한 터널이 처리.
cloudflare/cloudflared:latest 는 distroless — 셸·cat·ls 가 없다. docker exec … cat config.yml 은 exec: "cat": executable file not found 로 실패한다. 설정 확인은 컨테이너 안이 아니라 호스트 마운트 경로에서:
ssh alpha cat /home/ec2-user/grindaai-kms-astro/cloudflared/config.yml
datagram manager … failure 는 엣지 재연결 시 WARN(1초 내 자동 retry) — 정상 범주.version 2026.7.0 is outdated → 2026.7.1 권장. --no-autoupdate 라 수동 갱신 필요(이미지 pull + 재기동).