Cloudflare Tunnel 셋업 가이드 — Docker 환경 기준




Cloudflare Tunnel의 개념과 장점은 별도 글에서 다루었다. 이 글은 실제로 Tunnel을 셋업하는 단계를 정리한다. Docker 환경을 전제로 하며, 다른 컨테이너 서비스로 트래픽을 라우팅하는 시나리오에 집중한다.

사전 준비

다음이 갖춰져 있어야 한다.

  • [ ] 도메인 1개 이상이 Cloudflare에서 관리되고 있음
  • [ ] Cloudflare 계정 (무료 플랜 가능)
  • [ ] Docker가 설치된 서버
  • [ ] 외부에 노출할 컨테이너 서비스 (예: 웹 앱, 관리 패널 등)

도메인을 Cloudflare에서 관리하지 않는 경우 먼저 다음을 진행한다.

  1. Cloudflare 대시보드(https://dash.cloudflare.com)에서 Add a Site
  2. 도메인 입력 → 자동 스캔된 DNS 레코드 확인
  3. 도메인 등록 기관의 네임서버를 Cloudflare가 안내한 주소로 변경
  4. 전파 완료 대기 (보통 수십 분, 길어도 24시간)

1단계: Zero Trust 대시보드 접속

Tunnel은 Cloudflare의 Zero Trust 제품군에 포함되어 있다.

https://one.dash.cloudflare.com 에 접속한다.

처음 접속하면 팀 이름 설정과 플랜 선택 화면이 표시된다. Free 플랜을 선택한다. 무료 플랜에서도 Tunnel 기능은 전부 사용 가능하다 (50명까지의 사용자 한도가 있지만 개인/소규모 운영에서는 의미가 없다).

2단계: 터널 생성

  1. 왼쪽 메뉴: Networks → Tunnels
  2. Create a tunnel 클릭
  3. Connector type: Cloudflared 선택 → Next
  4. 터널 이름 입력 (예: home-server)
  5. Save tunnel

다음 화면이 핵심이다. Install and run a connector 단계에서 환경 선택 메뉴가 나타난다. Docker 탭을 선택한다.

화면 하단에 다음과 같은 형태의 실행 명령이 표시된다.

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token eyJhIjoi.....

eyJhIjoi로 시작하는 긴 문자열이 터널 토큰이다. 이 토큰을 복사한다. 토큰 자체가 자격증명이므로 외부 노출에 주의한다.

3단계: 사용할 Docker 네트워크 확인

cloudflared가 다른 컨테이너로 트래픽을 전달하려면 그 컨테이너와 같은 Docker 네트워크에 있어야 한다. 먼저 사용 가능한 네트워크를 확인한다.

docker network ls

출력 예시:

NETWORK ID     NAME              DRIVER    SCOPE
abc123def456   bridge            bridge    local
def789abc012   coolify           bridge    local
345678abcdef   myproject_default bridge    local

이 중에서 대상 컨테이너가 속한 네트워크를 사용한다.

  • Coolify 사용자라면 coolify
  • docker-compose로 직접 운영한다면 <프로젝트명>_default
  • 별도로 만든 네트워크가 있다면 그 이름

대상 컨테이너의 네트워크는 다음으로 확인할 수 있다.

docker inspect <컨테이너이름> --format '{{json .NetworkSettings.Networks}}' | jq

4단계: cloudflared 컨테이너 실행

Cloudflare가 안내한 기본 명령은 --network host를 사용하지 않지만, 백그라운드 실행과 네트워크 지정이 빠져있다. 다음 명령으로 보강해서 실행한다.

docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  --network <네트워크이름> \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run --token <복사한_토큰>

각 플래그의 의미:

플래그의미
-d백그라운드 실행
--name cloudflared컨테이너 이름 지정
--restart unless-stopped서버 재부팅 시에도 자동 시작
--network <네트워크>대상 네트워크에 연결
--no-autoupdate자동 업데이트 비활성화 (필요 시 직접 갱신)

5단계: 연결 상태 확인

실행 직후 로그를 확인한다.

docker logs cloudflared

다음과 비슷한 로그가 보이면 정상이다.

INF Starting tunnel tunnelID=...
INF Connection ... registered connIndex=0 location=...
INF Connection ... registered connIndex=1 location=...
INF Connection ... registered connIndex=2 location=...
INF Connection ... registered connIndex=3 location=...

Connection ... registered 메시지가 4개 정도 보이는 것이 정상이다. cloudflared는 기본적으로 4개의 Cloudflare 데이터센터에 동시 연결을 유지한다.

Cloudflare 대시보드의 터널 상태도 HEALTHY로 표시되어야 한다. 표시되지 않으면 트러블슈팅 섹션을 참고한다.

6단계: Public Hostname 추가

이제 도메인을 내부 컨테이너에 연결한다.

  1. Cloudflare Zero Trust 대시보드 → Networks → Tunnels
  2. 생성한 터널 이름 클릭
  3. Public Hostname
  4. Add a public hostname

각 입력 항목:

항목값 예시비고
Subdomainapp1비울 수도 있음 (루트 도메인)
Domainexample.com등록된 도메인 중 선택
Path(비움)특정 경로만 라우팅할 때 사용
TypeHTTP내부 통신 (HTTPS는 cloudflared가 처리)
URLmy-app:3000컨테이너 이름:포트

URL 입력이 가장 중요하다. localhost:3000이나 127.0.0.1:3000이 아니라 같은 Docker 네트워크의 컨테이너 이름을 사용한다. Docker 내장 DNS가 컨테이너 이름을 자동으로 해석한다.

Save hostname을 클릭하면 1분 이내에 적용된다.

브라우저에서 https://app1.example.com으로 접속해서 내부 서비스에 도달하는지 확인한다.

7단계: 여러 서비스 추가

같은 터널로 여러 도메인/서비스를 처리할 수 있다. Public Hostname을 계속 추가한다.

app1.example.com    → http://my-app-1:3000
app2.example.com    → http://my-app-2:3000
admin.example.com   → http://admin-panel:8080
api.example.com     → http://api-server:4000

각 매핑은 즉시 반영되며 cloudflared 재시작이 필요 없다.

트러블슈팅

토큰 관련 오류

ERR Failed to fetch tunnel credentials: ...

토큰이 잘못되었거나 줄바꿈이 포함되어 있다. 명령어를 다시 복사해서 토큰을 한 줄로 붙여넣는다. 셸이 줄바꿈을 자동으로 처리하지 않으면 토큰 문자열에 공백/줄바꿈이 들어갈 수 있다.

Connector not connected 상태가 지속됨

대시보드에서 터널이 비활성 상태로 표시된다.

docker logs cloudflared --tail 50

확인 사항:

  1. 컨테이너가 실행 중인가? (docker ps)
  2. 아웃바운드 연결이 막혀있지 않은가? (curl -v https://api.cloudflare.com)
  3. 토큰이 정확한가?

Bad Gateway (502) 응답

도메인에 접속하면 Cloudflare의 502 페이지가 표시된다. cloudflared가 컨테이너 이름을 해석하지 못한 경우다.

먼저 cloudflared의 네트워크 소속을 확인한다.

docker inspect cloudflared --format '{{json .NetworkSettings.Networks}}'

대상 컨테이너의 네트워크 소속도 확인한다.

docker inspect my-app --format '{{json .NetworkSettings.Networks}}'

두 컨테이너가 같은 네트워크에 있어야 한다. 누락된 경우 수동 연결한다.

docker network connect <네트워크이름> cloudflared

또는 컨테이너 재생성 시 --network 옵션을 정확히 지정한다.

도메인은 접속되는데 내부 페이지에서 mixed content 경고

내부 앱이 HTTP를 가정하고 동작 중인 경우 발생한다. Cloudflare는 사용자에게 HTTPS를 제공하지만, 앱이 응답에 절대 경로 HTTP 링크를 포함하면 브라우저가 경고한다.

해결책:

  • 앱에서 절대 경로 대신 상대 경로 사용
  • Cloudflare 대시보드의 SSL/TLS → Edge Certificates → Automatic HTTPS Rewrites 활성화
  • 앱 측에서 X-Forwarded-Proto 헤더를 신뢰하도록 설정

실제 사용자 IP가 모두 Cloudflare IP로 표시됨

이는 정상 동작이다. cloudflared가 Cloudflare 측에서 수신한 요청을 내부로 전달할 때 출발지 IP는 cloudflared 컨테이너의 IP가 된다.

실제 사용자 IP는 다음 헤더에 담겨 있다.

CF-Connecting-IP: <실제 IP>

애플리케이션 코드에서 이 헤더를 읽거나, 앞단의 리버스 프록시에서 X-Real-IP로 변환한다. Express(Node.js) 예시:

app.set('trust proxy', true);
// req.ip가 X-Forwarded-For를 신뢰하게 됨

// 또는 CF-Connecting-IP를 직접 사용
app.use((req, res, next) => {
  req.realIp = req.headers['cf-connecting-ip'] || req.ip;
  next();
});

운영 팁

토큰 갱신

토큰이 노출된 경우 다음 절차로 갱신한다.

  1. Cloudflare 대시보드 → 터널 → Configure → Refresh token
  2. 새 토큰 복사
  3. cloudflared 컨테이너 재생성
docker stop cloudflared
docker rm cloudflared
docker run -d --name cloudflared --restart unless-stopped \
  --network <네트워크> \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run --token <새_토큰>

토큰을 시크릿 파일로 분리

명령행에 토큰을 직접 노출하지 않으려면 환경 파일을 사용한다.

echo "TUNNEL_TOKEN=eyJhIjoi..." > /etc/cloudflared/.env
chmod 600 /etc/cloudflared/.env
docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  --network <네트워크> \
  --env-file /etc/cloudflared/.env \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run

이 방식은 docker inspect로 토큰이 보이지 않게 한다.

docker-compose로 관리

별도 명령으로 띄우는 대신 docker-compose에 포함하면 관리가 쉽다.

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}
    networks:
      - shared-network

networks:
  shared-network:
    external: true
    name: <기존_네트워크명>

.env 파일에 TUNNEL_TOKEN을 정의하고 docker compose up -d로 실행한다.

로그 모니터링

서비스 가용성을 위해 cloudflared 로그를 정기적으로 확인한다.

docker logs cloudflared --since 1h --tail 100

연결 끊김이 잦다면 서버의 네트워크 안정성을 점검한다. 데이터센터 간 자동 페일오버 덕분에 일시적인 끊김은 사용자 경험에 영향을 주지 않지만, 빈번하다면 원인 분석이 필요하다.

보안 강화: Cloudflare Access 결합

특정 엔드포인트(예: 관리 UI)를 인증된 사용자만 접근하게 하려면 Cloudflare Access를 추가로 설정한다.

  1. Zero Trust → Access → Applications → Add an application
  2. Self-hosted 선택
  3. 보호할 도메인 등록 (예: admin.example.com)
  4. 정책 설정 (이메일 OTP, Google SSO 등)

이렇게 하면 해당 도메인 접속 시 Cloudflare가 먼저 인증을 요구한다.

정리

Cloudflare Tunnel의 셋업은 크게 두 부분이다. Cloudflare 대시보드에서 터널 생성과 도메인 매핑, 서버에서 cloudflared 컨테이너 실행. 핵심 포인트는 cloudflared와 대상 컨테이너가 같은 Docker 네트워크에 있어야 한다는 점이다.

한 번 셋업하면 이후 도메인 추가는 대시보드에서 Public Hostname을 등록하는 것만으로 끝난다. 서버 측 설정 변경 없이 무중단으로 서비스 노출 구성을 조정할 수 있다.




댓글 남기기