Caddy 강의 2시간 · 소개 + 적용 로드맵 워크숍
localhost에 갇힌 서비스를 세상과 연결하기

Caddy,
서비스에 문을 달아주는 관문

자동 HTTPS 리버스 프록시의 이해부터, 내 시스템에 적용하는 단계별 로드맵까지.

Ch.1 문제 인식 · 1/5

실제로 돌아가는 시스템에서 시작합니다

홈서버 — 미니PC, 24시간 가동

Docker 컨테이너: 자동화 에이전트 · 스케줄러 · 내부 API
접근: Tailscale(사설 VPN)로만

데스크탑 — 작업 머신

Docker 컨테이너: 텔레그램 봇 6개 · 사용량 대시보드 · 세션 저장 파이프라인
접근: 같은 Tailscale 망

관리 경로는 하나 — Tailscale SSH

내 기기들끼리만 통하는 사설망. 외부 인터넷에서 이 시스템으로 들어오는 문은 0개다.

문이 없다는 건 가장 안전한 상태이자, 지금부터 볼 세 가지 증상의 원인이기도 합니다.
Ch.1 문제 인식 · 2/5

증상 1 — 봇들이 polling으로 도는 이유

지금 — polling

봇 6개가 각자 0.5초마다 텔레그램 서버에 "새 메시지 있어?"

메시지가 없어도 요청은 계속 발생
응답 지연 최대 0.5~1초 · CPU와 네트워크 낭비

원하는 것 — webhook

새 메시지가 오면 텔레그램이 내 서버로 즉시 POST

이벤트 기반 · 즉시 반응 · 낭비 없음
조건: 외부에서 부를 수 있는 공개 HTTPS 주소

내 서버에는 그 주소가 없다 — 그래서 6개의 루프가 오늘도 돈다.
Ch.1 문제 인식 · 3/5

증상 2 — 만든 것이 localhost에 갇힌다

사용량 대시보드

API 비용 모니터링 — 그 컴퓨터 앞에 앉아야만 볼 수 있다

세션 기록 검색

수백 개의 작업 기록 — 특정 앱(Obsidian)을 열어야만 검색된다

외부 앱 연동

웹 프론트엔드 · iOS 단축어 · 자동화 도구가 내 API를 호출할 방법 자체가 없다

기능이 부족한 게 아니라 주소가 없어서 못 쓰는 상태.
Ch.1 문제 인식 · 4/5

증상 3 — 문을 열자니 무섭고 번거롭다

집 IP 노출

포트포워딩은 DNS에 우리 집 공인 IP를 공개 등록해야 작동한다 — 회선 전체가 표적이 된다

포트 번호 관리

서비스마다 :3000, :8080, :5173… 접속자가 번호를 알아야 하고, 포워딩 규칙도 포트 수만큼

인증서 관리

HTTPS는 필수(웹훅 조건)인데, 인증서 발급·갱신을 서비스 수만큼 반복해야 한다

부담의 공통 원인 — 서비스마다 문을 하나씩 내려고 하기 때문.
Ch.1 문제 인식 · 5/5

필요한 것은 문 여러 개가 아니라 관문 하나

웹훅 수신
대시보드 API
세션 검색 API
관문 하나
리버스 프록시 + 자동 HTTPS
도메인 하나
api.mydomain.com

경로만 열리고 나머지는 계속 숨는다 · 인증서는 관문이 한 번에 처리
여러분의 localhost에도 세상에 내보내고 싶은 것이 하나쯤 있습니다

이 관문을 가장 적은 비용으로 세우는 도구 — 오늘의 주제, Caddy.
Ch.2 Caddy 소개 · 1/3

Caddy — 자동 HTTPS가 기본값인 웹 서버

api.mydomain.com { reverse_proxy app:3000 }

이것이 설정의 전부 — 인증서 발급 · 자동 갱신 · HTTP→HTTPS 리다이렉트 포함.
Go로 작성된 단일 실행 파일, 오픈소스(2015~), 설정 파일 이름은 Caddyfile.

Nginx에서 수십 줄 + certbot + cron이 필요하던 일이 두 줄이 된다.
Ch.2 Caddy 소개 · 2/3

핵심 기능 — 필요한 것이 전부 기본값

자동 HTTPS

인증서 발급·갱신 자동(ACME)

리버스 프록시

뒤의 서비스로 전달·분배

정적 파일 서빙

파일 서버 기능 내장

HTTP/2 · HTTP/3

최신 프로토콜 기본 지원

Caddyfile

간결한 설정 문법

JSON API

실행 중 설정 변경 가능

단일 바이너리

의존성 없는 실행 파일 하나

철학

합리적인 기본값 — 설정하지 않아도 안전한 쪽으로 동작

Ch.2 Caddy 소개 · 3/3

Nginx와 Caddy — 언제 무엇을 쓰나

항목NginxCaddy
HTTPS 설정certbot 별도 설치 + 갱신 cron자동 — 설정 자체가 없음
설정 문법강력하지만 장황간결, 대부분 몇 줄
성능최고 수준(C)높음(Go) — 개인·중소 규모엔 차이 체감 불가
생태계·레퍼런스매우 넓음성장 중
설정 반영reload 필요무중단 reload + 실행 중 API 변경
학습 곡선높음낮음
결론 — 이미 돌아가는 Nginx는 그대로. 새로 세우는 관문부터 Caddy.
Ch.3 패턴 카탈로그 · 1/5

기본 패턴 — 프록시정적 서빙

리버스 프록시 — 가장 흔한 패턴

api.mydomain.com { reverse_proxy app:3000 }

정적 사이트 / SPA

mydomain.com { root * /srv/site try_files {path} /index.html file_server }
Caddy는 순수 프록시가 아니라 웹 서버 겸용 — HTML 폴더 하나쯤은 별도 파일 서버 없이 직접 서빙한다.
Ch.3 패턴 카탈로그 · 2/5

경로 라우팅 — 도메인 하나로 서비스 전부

api.mydomain.com { handle /webhook/* { reverse_proxy bots:3000 } handle /usage/* { reverse_proxy dashboard:3001 } handle /sessions/* { reverse_proxy pkm:3002 } }

/webhook/* → 텔레그램 봇들 — 증상 1 해결

/usage/* → 대시보드 API — 증상 2 해결

/sessions/* → 세션 검색 — 증상 2 해결

문은 여전히 443 하나 — 증상 3 해결

Ch.3 패턴 카탈로그 · 3/5

운영 레시피 — 필요할 때 꺼내 쓰는 세 가지

로드밸런싱 + 헬스체크

reverse_proxy app1:3000 app2:3000 { lb_policy round_robin health_uri /health }

Basic Auth

basic_auth { admin $2a$14$...hash } reverse_proxy admin:8080

CORS 헤더

header { Access-Control-Allow-Origin * } reverse_proxy app:3000
전부 Caddyfile 몇 줄 — 관문에서 한 번에 처리하고 앱 코드는 건드리지 않는다.
Ch.3 패턴 카탈로그 · 4/5

기초 원리 — 모든 전달 규칙은 IP:포트 쌍

외부 요청
:443
공유기
포트포워딩
192.168.0.134:443
호스트
-p 8081:3000
172.17.0.2:3000
컨테이너 안
앱이 :3000에서 수신

포트는 프로그램이 정하는 값 — 80/443은 강제가 아니라 관례. 충돌은 같은 네트워크 공간 안에서만.

컨테이너는 각자 내부 IP를 받는다 — 둘 다 :3000을 써도 무충돌. 구분 기준은 포트가 아니라 IP:포트 쌍.

compose 네트워크 안에서는 이름이 주소가 된다(이름→IP 자동 변환) — 그래서 reverse_proxy app:3000이 가능하고, IP가 바뀌어도 깨지지 않는다.

Ch.3 패턴 카탈로그 · 5/5

Docker 구성 — 포트 공개는 Caddy 하나만

# docker-compose.yml services: caddy: image: caddy:2-alpine ports: ["80:80", "443:443"] # 유일한 공개 volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data # 인증서 보관 app: build: ./app # ports 없음 — 내부 네트워크 전용

노출 지점이 문 하나로

앱 컨테이너는 인터넷에서 구조적으로 도달 불가 — Caddy가 라우팅한 경로만 열린다

DNS 전략

기존 사이트 배포(Vercel 등)는 그대로 두고, api.mydomain.com A 레코드 하나만 서버로 추가

1부 끝 — 휴식 후 직접 띄워봅니다.
Ch.4 실습 · 1/2

실습 1 — 15분 안에 HTTPS 리버스 프록시

# 1. 실습 저장소 받기 git clone https://github.com/Dami-Shin-01/260724_caddy cd 260724_caddy/demo # 2. 실행 docker compose up -d # 3. 확인 https://localhost → 앱 응답 확인

인증서 경고가 떠도 정상입니다

localhost는 Caddy의 내부 CA(사설 발급 기관) 인증서 — 브라우저가 아직 모르는 발급자일 뿐. caddy trust 또는 "고급 → 이동"으로 통과

도메인 없이 되는 이유

localhost는 공인 인증서가 불가능하므로 Caddy가 스스로 발급한다 — 실전 도메인에서는 이 자리가 자동으로 공인 인증서가 된다

Ch.4 실습 · 2/2

실습 2 — 앱 두 개를 경로로 나눠 태우기

localhost { handle /a/* { reverse_proxy app-a:3000 } handle /b/* { reverse_proxy app-b:3000 } } # 반영 — 재시작 없이 docker compose exec caddy \ caddy reload -c /etc/caddy/Caddyfile

확인할 것

/a/b가 서로 다른 앱으로 — 두 앱 모두 내부 포트는 3000, 구분은 이름(IP)

방금 한 일의 의미

서비스 추가 = handle 블록 추가 — 문은 계속 하나, 포워딩 규칙도 인증서도 늘지 않는다

Ch.5 적용 로드맵 · 1/9

사례 완성편 — 세 증상이 풀린 구조

텔레그램 서버
웹훅 POST
웹 프론트 · 단축어
API 호출
입구
터널 또는 포트포워딩
Caddy
경로 라우팅
봇들 · 대시보드 · 세션 검색
내부 네트워크 전용

증상 1 — polling 6개 루프 → 웹훅 즉시 수신

증상 2 — 갇힌 API → 도메인 하나로 호출 가능

증상 3 — 문 여러 개 부담 → 관리된 문 하나

관리용(SSH)은 그대로 Tailscale — 공개용 대문과 관리용 문은 끝까지 분리.
Ch.5 적용 로드맵 · 2/9

보안 — 문이 없는 구조에서 관리된 문 하나

1 관리망 분리

SSH·관리자 대시보드는 Caddy 라우팅에 아예 넣지 않는다 — VPN(Tailscale) 전용 유지

2 웹훅 위조 방어

시크릿 토큰 검증(X-Telegram-Bot-Api-Secret-Token) + 추측 불가능한 랜덤 경로

3 앱이 진짜 리스크

조회형 API는 API 키·Basic Auth를 관문에서 강제 — 실행형 API는 공개 여부 자체를 신중히

4 컨테이너 격리

공개할 서비스만 Caddy의 Docker 네트워크에 연결 — 나머지는 구조적으로 도달 불가

노출은 서비스별 선택 — polling 유지도 "문을 안 여는" 정당한 선택지다.
Ch.5 적용 로드맵 · 3/9

왜 포트포워딩은 IP 노출인가

직접 접속 — 들어오는 연결

DNS: api.mydomain.com → 집 공인 IP
외부 → 공유기(포트 개방) → 서버

DNS 기록은 공개 정보 — nslookup 한 줄로 누구나 조회
공인 IP는 회선 전체의 주소 — 공격 시 집 인터넷이 함께
공개된 IP는 통상 수 분 내 자동 포트 스캔 대상

터널 — 나가는 연결의 재사용

DNS: api.mydomain.com → 중계 서버 IP
서버 →(먼저 나감)→ 중계 ←(타고 들어옴)← 외부

집 IP는 어디에도 기록되지 않는다
공유기에 여는 포트 0개 — 나가는 연결은 원래 허용
중계가 앞에서 공격 트래픽을 받아준다

노출 여부는 연결의 방향이 결정한다 — 다음 장에서 방식별로 비교.
Ch.5 적용 로드맵 · 4/9

입구 선택 — 꼭 Cloudflare일 필요는 없다

방식얻는 것감수할 것
포트포워딩 직결의존성 0 · 기존 네임서버에 A 레코드만집 IP 노출 · 포트 개방 · 유동 IP면 DDNS 필요
Cloudflare TunnelIP 은닉 · 무포트 · WAF · 무료네임서버를 Cloudflare로 이전 필수 · TLS 종단이 CF
Tailscale Funnel이미 쓰고 있다면 가장 간단 · 무료커스텀 도메인 불가(*.ts.net 고정) · 대역폭 제한
VPS 중계완전한 통제 · A 레코드만 · IP 은닉월 비용 · 관리할 서버 +1
선택 기준 — 커스텀 도메인이 필요한가 × 네임서버 이전을 감수하나 × IP 노출을 허용하나 × 비용
Ch.5 적용 로드맵 · 5/9

종속성 — "터널 업체가 터지면 나는?"

영향 범위는 분리되어 있다

터널 장애 = 공개 문만 닫힘. 관리망(Tailscale SSH)은 독립 경로라 서버 접속·운영은 계속된다

Caddy는 입구 중립

터널이든 포트포워딩이든 VPS든 Caddy 뒤 구성은 동일 — 종속은 입구 한 조각에 국한, 갈아끼우면 된다

폴백이 이미 있다

웹훅 장애 시 봇은 polling으로 즉시 복귀 가능 — 지금 쓰는 방식이 곧 백업이다

되돌리기 비용

네임서버 이전도 가역적 — "갇히는 종속"이 아니라 "교체 비용이 있는 선택"으로 판단하라

Ch.5 적용 로드맵 · 6/9

추천 로드맵 — 작게 검증하고 단계로 확장

1단계 — 검증

Tailscale Funnel로 웹훅 1개

도메인 · 네임서버 이전 · 비용 전부 불필요.
웹훅이 실제로 들어오는지 확인이 목표

2단계 — 확장

커스텀 도메인 도입

효용 확인 후 앞 장의 표에서 입구 선택.
갈림길: 네임서버 이전을 감수하는가

3단계 — 정착

경로 라우팅으로 서비스 추가

/webhook/* · /api/* · /sessions —
추가는 handle 블록 하나씩

종속·보안 고민이 무거워지는 건 2단계부터 — 1단계는 오늘 밤에 시작할 수 있다.
Ch.5 적용 로드맵 · 7/9

워크시트 — 내 시스템에 적용하기

1 서비스 인벤토리

내 컴퓨터·서버에서 도는 것 전부 — localhost에서만 도는 것 포함

2 공개 대상 선정

누가 부르나 · 조회형인가 실행형인가 · 유출 시 피해는 — 세 질문 통과한 것만

3 입구 선택

커스텀 도메인 필요 여부부터 결정 트리를 따라간다

4 단계별 로드맵

검증 → 확장 → 정착 — 1단계는 이번 주에 할 수 있는 크기로

정답은 없다 — "아직 공개 안 함"도 정당한 결론이다. 작성 15분.
Ch.5 적용 로드맵 · 8/9

공유 — 서로 다른 시스템이 교재가 된다

공유 포맷 — 1인 2분

1 내 시스템 한 줄 소개
2 밖으로 내보내기로 한 것 (그리고 안 내보내기로 한 것)
3 선택한 입구와 그 이유
4 1단계 — 이번 주에 할 일

같은 도구, 다른 시스템, 다른 선택 — 선택의 이유가 오늘 배운 것의 전부입니다.

Ch.5 적용 로드맵 · 9/9

정리 — Caddy가 맞는 경우, 아닌 경우

맞는 경우

서비스 몇 개~십여 개의 개인·소규모 인프라
HTTPS·인증서 관리를 자동화하고 싶다
정적 페이지와 API가 섞여 있다
설정을 한 파일에서 한눈에 보고 싶다

맞지 않는 경우

이미 잘 돌아가는 Nginx 체계가 있다
컨테이너가 수십 개씩 수시로 뜨고 진다(→ Traefik)
극한 트래픽·초저지연이 목표다
조직 표준이 이미 다른 도구다

오늘 밤의 첫 걸음 — Funnel로 웹훅 하나 검증하기. 자료·데모 저장소 링크는 배포 페이지에서.
질문이 있을 때만

부록

A1 Caddy vs Traefik · A2 유동 IP와 DDNS · A3 Cloudflare와 TLS 종단 · A4 홈서버 vs VPS vs PaaS

부록 A1

Caddy vs Traefik — 동적이면 Traefik, 아니면 Caddy

항목TraefikCaddy
설정 방식컨테이너 라벨 자동 감지 — 뜨고 지는 컨테이너를 스스로 라우팅Caddyfile 한 파일에 명시
유리한 환경컨테이너 수십 개, 수시 변동(오케스트레이터)서비스 몇 개~십여 개, 구성 안정적
학습 곡선라우터·서비스·미들웨어 개념 체계 필요낮음
정적 파일 서빙불가(순수 프록시) — 파일 서버를 별도로내장 — 혼자 다 한다
정적 사이트 + API 혼합의 개인 인프라라면 — 부품 수가 적은 Caddy.
부록 A2

유동 IP — 포트포워딩 경로의 전제 조건

통신사가 집 IP 변경
가정 회선 기본값
DDNS 클라이언트가 감지
DNS A 레코드 자동 갱신
접속 유지

DDNS 없이 포트포워딩 = IP가 바뀌는 순간 서비스 두절 — 원인도 한참 뒤에 알게 된다

터널 방식은 서버가 먼저 나가서 연결하므로 유동 IP여도 무관 — 이 고민 자체가 없다

부록 A3

"Cloudflare가 내 트래픽을 본다" — 맞습니다

클라이언트
암호화
Cloudflare
TLS 종단 — 여기서 한 번 풀린다
내 서버
재암호화 구간

Cloudflare는 구조적으로 내용을 읽을 수 있는 위치다 — WAF·캐시가 가능한 이유이자 대가

민감 데이터라면 종단간 통제가 되는 방식으로 — VPS 중계(WireGuard) 또는 포트포워딩 직결

숨길 것 없는 공개 API엔 합리적 거래 — 데이터 성격이 결정 기준이다.
부록 A4

"그냥 VPS 쓰면 되잖아?" — 절반은 맞는 말

홈서버VPSPaaS (Vercel 등)
비용이미 가진 하드웨어 + 전기월 5천 원~ (사양 비례)무료~ (사용량 비례)
자원램·스토리지 넉넉, 로컬 데이터 직접 접근저사양부터, 데이터는 올려야코드만 — 파일·하드웨어 접근 불가
상시 프로세스무제한 (봇 6개든 뭐든)가능 (사양 안에서)대부분 불가 — 요청 시에만 실행
노출 고민오늘 배운 설계가 필요없음 (고정 IP 기본)없음 (HTTPS·도메인 자동)
절충 — "VPS는 입구만"(중계) 조합. 그리고 워크시트의 결론이 "VPS로 이주"여도 정당하다.