사설IP 만으로 포트포워딩 없이 caddy쓰기
Cloudflare Tunnel + Caddy로 포트포워딩 없이 홈서버 운영하기
사설 IP VM에서 공인 IP, 포트포워딩 없이 외부에 서비스를 노출하는 방법을 정리합니다.
왜 Cloudflare Tunnel인가?
홈서버같은 내부 VM을 외부에 노출하려면 보통 공유기에서 80/443 포트포워딩을 해야 합니다. 하지만 다음과 같은 상황에서는 포트포워딩 자체가 불가능하거나 불편합니다.
- 사무실 네트워크 (관리자 권한 없음)
- ISP에서 포트 차단
- 보안 정책으로 인바운드 포트 오픈 금지
Cloudflare Tunnel을 사용하면 VM 내부에서 Cloudflare 엣지로 아웃바운드 연결만 맺기 때문에 인바운드 포트를 전혀 열 필요가 없습니다.
최종 구성도
사용자 (HTTPS)
↓
Cloudflare Edge (TLS 처리)
↓
cloudflared 컨테이너 (터널)
↓
caddy 컨테이너 (reverse proxy, Host 헤더로 서비스 분기)
↓
각 서비스 컨테이너
환경
- VM(리눅스 서버): uman (Proxmox 위의 Ubuntu 24.04)
- 네트워크: Tailscale로 홈랩 노드들과 연결, 사무실 환경 (포트포워딩 불가)
- Docker: Standalone Swarm (단일 노드 manager)
- 도메인: miestas.co.kr (Cloudflare DNS)
1단계: Cloudflare Zero Trust 터널 생성
Cloudflare 대시보드에서 Zero Trust → Networks → Tunnels → 터널 만들기로 이동합니다.

터널 유형은 Cloudflared를 선택하고 이름을 지정합니다. (여기서는 uman)
설치 방식은 Docker를 선택하면 다음과 같은 실행 명령이 나옵니다.
docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token [TOKEN]
이 토큰은 잠시 후 Docker secret으로 안전하게 등록합니다. 메모.
2단계: DNS 레코드 정리
기존에 miestas.co.kr에 A 레코드가 있으면 Tunnel 등록 시 충돌이 납니다.
Cloudflare DNS 관리 페이지에서 기존 A 레코드와 CNAME을 모두 삭제합니다.

삭제 대상:
miestas.co.krA 레코드wwwA 레코드*CNAME 레코드
Tunnel Public Hostname을 등록하면 Cloudflare가 CNAME 레코드를 자동으로 생성해줍니다. ip등록을 하지 않아도 cloudflare가 터널로 연결 해줌
3단계: Docker Swarm 초기화 및 네트워크 구성
uman VM에서 Swarm을 초기화하고 overlay 네트워크를 만듭니다.
# Swarm 초기화 (Tailscale IP 사용)
docker swarm init --advertise-addr [TAILSCALE_IP]:2377
# proxy 오버레이 네트워크 생성
docker network create --driver=overlay --attachable proxy
4단계: Cloudflare 터널 토큰 Docker secret 등록
토큰을 yml 파일에 직접 넣으면 보안상 위험합니다. Docker secret으로 관리합니다.
echo "터널토큰값" | docker secret create cloudflared_token -
# 확인
docker secret ls
5단계: docker-compose(stack) 파일 작성
caddy.yml 파일을 작성합니다. 핵심은 cloudflared와 caddy가 같은 proxy 네트워크에 있어야 한다는 점입니다. cloudflared가 caddy:80으로 트래픽을 넘길 수 있어야 하기 때문입니다.
version: "3.8"
services:
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN_FILE=/run/secrets/cloudflared_token
secrets:
- cloudflared_token
networks:
- proxy
deploy:
replicas: 1
placement:
constraints:
- node.hostname == uman
restart_policy:
condition: on-failure
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
ports:
- "80:80"
- "443:443"
volumes:
- /data/caddy/data:/data
- /data/caddy/config:/config
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
environment:
- TZ=Asia/Seoul
- CADDY_INGRESS_NETWORKS=proxy
- CADDY_DOCKER_PROXY_SERVICE_TASKS=false
deploy:
replicas: 1
placement:
constraints:
- node.hostname == uman
restart_policy:
condition: on-failure
labels:
caddy.admin: "0.0.0.0:2019"
caddy.auto_https: "off" # Tunnel 사용 시 caddy TLS 불필요
whoami:
image: traefik/whoami
networks:
- proxy
deploy:
replicas: 1
restart_policy:
condition: on-failure
labels:
caddy: "http://who.miestas.co.kr"
caddy.reverse_proxy: "{{upstreams 80}}"
networks:
proxy:
external: true
secrets:
cloudflared_token:
external: true
핵심 포인트
| 항목 | 설명 |
|---|---|
caddy.auto_https: "off" |
Cloudflare가 TLS 처리하므로 caddy ACME 불필요 |
http:// prefix |
auto_https off 상태에서 HTTP로 명시 |
| 같은 proxy 네트워크 | cloudflared → caddy:80 통신을 위해 필수 |
볼륨 디렉토리 생성:
mkdir -p /data/caddy/data
mkdir -p /data/caddy/config
스택 배포:
docker stack deploy -c caddy.yml uman
6단계: Cloudflare Tunnel Public Hostname 등록
Zero Trust → Tunnels → uman → 게시된 애플리케이션 경로 추가
각 서비스별로 추가합니다.


| 하위 도메인 | 도메인 | 유형 | URL |
|---|---|---|---|
| who | miestas.co.kr | HTTP | caddy:80 |
| caddy | miestas.co.kr | HTTP | caddy:80 |
URL을localhost:80이 아닌caddy:80으로 설정해야 합니다.
Docker 컨테이너끼리는 localhost를 공유하지 않습니다. 같은 overlay 네트워크에서 서비스명으로 통신합니다.
등록하면 Cloudflare가 자동으로 CNAME DNS 레코드를 생성합니다.

7단계: SSL/TLS 암호화 모드 변경
Cloudflare SSL/TLS → 개요에서 암호화 모드를 Flexible로 변경합니다.

| 모드 | 설명 |
|---|---|
| Full | Cloudflare → 원본 서버도 HTTPS 요구 → caddy가 HTTP만 받으면 502 발생 |
| Flexible | 사용자 ↔ Cloudflare는 HTTPS, Cloudflare → 원본은 HTTP 허용 |
Tunnel 구조에서는 Flexible이 적합합니다.

동작 확인
curl -v http://who.miestas.co.kr
< HTTP/1.1 200 OK
< via: 1.1 Caddy
< Server: cloudflare
< CF-RAY: ...
via: 1.1 Caddy 헤더가 보이면 성공입니다.
새 서비스 추가 방법
이후 서비스를 추가할 때는 두 가지만 하면 됩니다.
1. caddy.yml에 서비스 추가
myservice:
image: myimage
networks:
- proxy
deploy:
replicas: 1
labels:
caddy: "http://myservice.miestas.co.kr"
caddy.reverse_proxy: "{{upstreams 8080}}"
2. Cloudflare Tunnel에 hostname 추가
myservice.miestas.co.kr→ HTTP →caddy:80
끝입니다. 포트포워딩, 인증서 발급, DNS 수동 설정 모두 불필요합니다.
트러블슈팅
502 Bad Gateway
- Cloudflare SSL 모드가 Full인지 확인 → Flexible로 변경
- cloudflared URL이
localhost:80으로 되어 있는지 확인 →caddy:80으로 변경
DNS NXDOMAIN (인증서 발급 실패)
- caddy에
caddy.auto_https: "off"설정 확인 - 서비스 label에
http://prefix 확인
cloudflared → caddy 통신 불가
두 컨테이너가 같은 overlay 네트워크에 있는지 확인
docker inspect [container_id] | grep -A 10 Networks
마무리
Cloudflare Tunnel을 사용하면 공인 IP나 포트포워딩 없이도 안전하게 서비스를 외부에 노출할 수 있습니다. 특히 사무실처럼 네트워크 제약이 있는 환경에서 매우 유용합니다.
lucaslorentz/caddy-docker-proxy와 조합하면 새 서비스를 label 몇 줄로 바로 추가할 수 있어 관리도 편합니다.
Member discussion