1. Caddy는 어떤 서버인가
Caddy는 2015년 Matt Holt가 만든 Go 언어 기반 웹 서버로, 현재는 v2가 표준입니다(v1과 설정 체계가 완전히 다르므로 검색할 때 v2 문서인지 꼭 확인하세요).
Caddy의 정체성을 세 문장으로 요약하면 다음과 같습니다.
- HTTPS는 기본값이다. 도메인을 적으면 인증서 발급·갱신·HTTP→HTTPS 리다이렉트까지 자동으로 한다.
- 설정은 API로 관리되는 JSON이다. Caddyfile은 사람이 쓰기 편하게 만든 "어댑터"일 뿐이다.
- 모든 기능은 모듈이다. 표준 모듈 외 기능은 Go 플러그인으로 컴파일해 넣는다.
아키텍처: Nginx와 무엇이 다른가
┌─────────────── caddy (단일 프로세스, 단일 바이너리) ───────────────┐
│ │
Caddyfile ──adapt──▶ JSON 설정 ──▶ ┌──────────────┐ │
│ │ Admin API │ ◀── localhost:2019 │
│ └──────┬───────┘ │
│ ┌───────────┼────────────┐ │
│ ┌─────▼────┐ ┌────▼─────┐ ┌────▼─────┐ │
│ │ http app │ │ tls app │ │ pki app │ ... │
│ └──────────┘ └──────────┘ └──────────┘ │
│ goroutine들이 요청 처리 (Go 런타임 스케줄러) │
└──────────────────────────────────────────────────────────────────┘- 단일 프로세스 + goroutine: Nginx의 마스터/워커 프로세스 대신, Go 런타임이 수많은 goroutine을 OS 스레드에 스케줄링합니다. 개발자 입장에서는 프로세스 수 튜닝을 신경 쓸 일이 거의 없습니다.
- 의존성 없는 단일 바이너리: OpenSSL 같은 시스템 라이브러리에 의존하지 않고, TLS도 Go 표준 라이브러리(
crypto/tls)를 씁니다. - 메모리 안전성: Go로 작성되어 C에서 흔한 버퍼 오버플로 계열 취약점이 구조적으로 적습니다.
- 무중단 설정 변경: 설정을 바꾸면 새 설정을 먼저 준비하고, 성공하면 원자적으로 교체합니다. 실패하면 기존 설정을 그대로 유지합니다.
2. 설치
Ubuntu/Debian (공식 저장소)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy패키지로 설치하면 systemd 서비스가 등록되고, 설정 파일은 /etc/caddy/Caddyfile, 실행 유저는 caddy입니다.
macOS
brew install caddyDocker
docker run -d --name caddy \
-p 80:80 -p 443:443 -p 443:443/udp \
-v $(pwd)/Caddyfile:/etc/caddy/Caddyfile \
-v caddy_data:/data \
-v caddy_config:/config \
caddy:2⚠️ /data 볼륨은 반드시 영속화하세요. 발급받은 인증서와 ACME 계정 키가 여기 저장됩니다. 컨테이너를 지울 때마다 인증서를 새로 받으면 Let's Encrypt의 발급 횟수 제한(rate limit)에 걸릴 수 있습니다.
단일 바이너리
GitHub Releases나 공식 다운로드 페이지에서 바이너리 하나만 받아도 됩니다. 폐쇄망 서버에 반입할 때도 파일 하나만 옮기면 된다는 게 은근히 큰 장점입니다.
3. CLI 맛보기: 설정 파일 없이 쓰기
Caddy는 설정 파일 없이 명령어 한 줄로 서버를 띄울 수 있습니다.
# 현재 디렉터리를 정적 파일 서버로 (디렉터리 목록 표시)
caddy file-server --listen :8080 --browse
# 리버스 프록시
caddy reverse-proxy --from :8080 --to localhost:3000
# 도메인을 주면 HTTPS까지 자동 (공인 DNS + 80/443 접근 가능해야 함)
caddy reverse-proxy --from example.com --to localhost:3000로컬 개발 중에 "빌드 결과물을 잠깐 띄워보고 싶다"거나 "HTTPS로 로컬 앱을 테스트하고 싶다"면 아주 유용합니다.
주요 명령어
caddy run # 포그라운드 실행 (현재 디렉터리의 Caddyfile 사용)
caddy start # 백그라운드 실행
caddy stop
caddy reload # 무중단 설정 리로드 (Admin API 경유)
caddy validate --config /etc/caddy/Caddyfile # 설정 검증 (= nginx -t)
caddy fmt --overwrite Caddyfile # 포매터 (들여쓰기 정리)
caddy adapt --config Caddyfile --pretty # Caddyfile → JSON 변환 결과 보기
caddy list-modules # 포함된 모듈 목록 (= nginx -V)
caddy trust # 로컬 CA 루트 인증서를 시스템 신뢰 저장소에 설치
caddy hash-password # basic_auth용 비밀번호 해시 생성systemd로 운영한다면 Nginx와 마찬가지로 systemctl reload caddy를 쓰면 됩니다.
4. Caddyfile 문법
기본 구조
# ① 전역 옵션 블록 (선택, 반드시 맨 위)
{
email admin@example.com
# debug
}
# ② 스니펫 (재사용 조각, 6편)
(common) {
encode zstd gzip
}
# ③ 사이트 블록: 주소 { 지시어들 }
example.com {
import common
root * /var/www/example
file_server
}
www.example.com {
redir https://example.com{uri} permanent
}- 들여쓰기는 탭이 관례입니다(
caddy fmt가 맞춰줍니다). - 세미콜론이 없습니다. 한 줄에 지시어 하나.
- 주석은
#.
사이트 주소의 여러 형태
example.com # HTTPS 자동 (80/443)
example.com, www.example.com # 여러 도메인을 한 블록에
*.example.com # 와일드카드 (DNS 챌린지 필요, 6편)
localhost # 로컬 CA로 HTTPS
:8080 # 포트만 → HTTP, 모든 호스트
http://example.com # 명시적 HTTP (자동 HTTPS 끔)
https://example.com:8443 # 커스텀 포트의 HTTPS
192.168.0.10 # IP → 로컬 CA로 HTTPS주소 형태가 곧 HTTPS 정책이라는 점이 핵심입니다. 스킴과 포트를 생략하고 도메인만 쓰면 Caddy는 "이 도메인으로 HTTPS 서비스를 하겠다"는 뜻으로 받아들입니다.
지시어와 매처 토큰
대부분의 지시어는 다음 형태입니다.
지시어 [매처] 인자들... {
하위 지시어
}root * /var/www/site # * = 모든 요청
root /blog/* /var/www/blog # /blog/ 이하 요청만
reverse_proxy /api/* localhost:8080경로 매처는 정확히 일치가 기본입니다. /api는 /api만 매칭하고 /api/users는 매칭하지 않습니다. 하위 경로까지 잡으려면 /api/*처럼 와일드카드를 써야 합니다. Nginx의 접두사 매칭과 다른 부분이라 처음에 자주 실수합니다.
플레이스홀더
Nginx의 $변수에 해당하는 것이 {플레이스홀더}입니다.
| Caddyfile 축약형 | 의미 | Nginx 대응 |
|---|---|---|
{host} | 요청 호스트 | $host |
{uri} | 경로 + 쿼리 | $request_uri |
{path} | 경로 | $uri |
{query} | 쿼리 스트링 | $args |
{query.name} | 특정 쿼리 파라미터 | $arg_name |
{header.X-Name} | 요청 헤더 | $http_x_name |
{remote_host} | 클라이언트 IP | $remote_addr |
{scheme} | http / https | $scheme |
{env.VAR} | 환경 변수 (런타임) | — |
{$VAR} | 환경 변수 (설정 파싱 시점 치환) | — |
환경 변수를 쓸 수 있다는 점이 Docker/CI 환경에서 특히 편리합니다.
{$SITE_DOMAIN:localhost} {
reverse_proxy {$UPSTREAM:app:3000}
}5. 자동 HTTPS: 어떻게 동작하는가
Caddy의 간판 기능을 제대로 이해해 봅시다. 다음 설정 하나로:
example.com {
respond "Hello HTTPS"
}Caddy는 시작하면서 이런 일을 합니다.
- 도메인 판별: 사이트 주소가 공인 도메인처럼 보이면 공인 CA를,
localhost·사설 IP·.internal/.local같은 이름이면 내장 로컬 CA를 사용합니다. - 인증서 발급: 기본적으로 Let's Encrypt에 요청하고, 실패하면 ZeroSSL로 자동 대체(fallback)합니다. 챌린지는 HTTP-01과 TLS-ALPN-01을 자동으로 시도합니다.
- HTTP → HTTPS 리다이렉트: 80 포트에 리다이렉트 서버를 자동으로 띄웁니다.
- 자동 갱신: 인증서 수명이 일정 비율 남으면 백그라운드에서 갱신합니다. 갱신 실패 시 재시도와 백오프도 알아서 합니다.
- OCSP 스테이플링: 인증서 폐기 상태 정보를 미리 받아 핸드셰이크에 포함합니다.
- HTTP/2, HTTP/3 활성화: 별도 설정 없이 기본으로 켜집니다(HTTP/3는 UDP 443이 열려 있어야 함).
인증서는 데이터 디렉터리에 저장됩니다(Linux 패키지 설치 시 보통 /var/lib/caddy/.local/share/caddy, Docker는 /data/caddy).
자동 HTTPS 성공 조건
- 도메인의 DNS A/AAAA 레코드가 이 서버의 공인 IP를 가리킬 것
- 외부에서 80, 443 포트로 접근 가능할 것 (HTTP-01, TLS-ALPN-01 챌린지)
- Caddy가 80/443에 바인딩할 권한이 있을 것 (패키지 설치 시
CAP_NET_BIND_SERVICE가 부여됨) - 데이터 디렉터리가 쓰기 가능하고 영속적일 것
방화벽이나 NAT 때문에 80/443을 외부에 열 수 없다면 DNS 챌린지(6편)를 써야 합니다.
로컬 HTTPS
localhost {
reverse_proxy localhost:3000
}이렇게 하면 Caddy가 자체 루트 CA를 만들고 localhost 인증서를 발급합니다. 처음 실행 시 루트 인증서를 시스템 신뢰 저장소에 설치하려고 시도하며(권한이 필요할 수 있음), 수동으로는 caddy trust를 실행합니다. Secure Cookie, Service Worker, OAuth 콜백처럼 HTTPS가 필요한 기능을 로컬에서 테스트할 때 매우 편합니다.
폐쇄망 / 사내망에서는?
공인 CA에 접근할 수 없는 환경이라면 선택지는 두 가지입니다.
# (1) Caddy 내장 CA 사용 → 클라이언트에 Caddy 루트 인증서 배포 필요
app.internal.corp {
tls internal
reverse_proxy 10.0.0.11:8080
}
# (2) 사내 CA가 발급한 인증서 파일 직접 지정
app.corp.example {
tls /etc/caddy/certs/app.crt /etc/caddy/certs/app.key
reverse_proxy 10.0.0.11:8080
}인증서 파일을 직접 지정하면 그 사이트의 자동 발급은 꺼지지만, HTTP→HTTPS 리다이렉트 같은 나머지 자동 기능은 유지됩니다.
자동 HTTPS 끄기
{
auto_https off # 전부 끄기
# auto_https disable_redirects # 리다이렉트만 끄기
}또는 사이트 주소를 http://example.com이나 :80으로 쓰면 해당 사이트만 HTTP로 서비스합니다. Caddy 앞에 이미 TLS를 처리하는 로드 밸런서가 있을 때 이렇게 씁니다.
6. 정적 파일 서빙
example.com {
root * /var/www/example
encode zstd gzip
file_server
# 정적 자산 장기 캐시
@static path *.css *.js *.png *.jpg *.svg *.woff2
header @static Cache-Control "public, max-age=31536000, immutable"
handle_errors {
rewrite * /{err.status_code}.html
file_server
}
}root: 문서 루트를 지정합니다(Nginx의root). 그 자체로 파일을 서빙하지는 않습니다.file_server: 실제로 파일을 응답합니다.browse옵션을 주면 디렉터리 목록을 보여줍니다.encode: 응답 압축. zstd와 gzip이 내장되어 있습니다. 미리 압축된 파일은file_server { precompressed zstd br gzip }으로 서빙할 수 있습니다.@static: 이름 있는 매처(named matcher). 6편에서 자세히 다룹니다.
SPA (React, Vue 등)
app.example.com {
root * /var/www/spa/dist
encode zstd gzip
try_files {path} /index.html
file_server
}Nginx의 try_files $uri $uri/ /index.html과 같은 역할입니다.
7. 리버스 프록시 기본
example.com {
reverse_proxy localhost:3000
}이 한 줄이 Nginx 3편에서 길게 설명한 내용 대부분을 기본값으로 처리합니다.
| Nginx에서 수동으로 하던 것 | Caddy 기본 동작 |
|---|---|
proxy_set_header Host $host | Host 헤더를 원본 그대로 전달 |
X-Forwarded-For / -Proto / -Host | 자동 설정 |
WebSocket Upgrade/Connection 헤더 | 자동 처리 |
proxy_http_version 1.1 + keepalive | 업스트림 커넥션 풀 기본 사용 |
| HTTP/2 백엔드 (gRPC 등) | h2c:// 스킴으로 지정 |
경로별 라우팅
example.com {
# /api/* → 백엔드 (경로 유지: /api/users → :8080/api/users)
reverse_proxy /api/* localhost:8080
# 나머지 → 프런트엔드
reverse_proxy localhost:3000
}"/api를 떼고 넘기고 싶다"면 handle_path를 씁니다.
example.com {
handle_path /api/* {
reverse_proxy localhost:8080 # /api/users → :8080/users
}
handle {
reverse_proxy localhost:3000
}
}Nginx의 proxy_pass http://backend/;(끝 슬래시) 규칙을 외울 필요 없이, 이름만 봐도 의도가 드러납니다. handle, handle_path, route의 차이는 다음 편에서 정리합니다.
업스트림 쪽 헤더 조작
reverse_proxy localhost:8080 {
header_up X-Request-Start "{time.now.unix_ms}" # 백엔드로 보내는 요청 헤더
header_down -Server # 클라이언트로 가는 응답 헤더 제거
}HTTPS 백엔드로 프록시할 때 백엔드가 자기 도메인의 Host를 기대한다면:
reverse_proxy https://api.partner.com {
header_up Host {upstream_hostport}
}8. 로그
Caddy는 기본 로그가 구조화된 JSON입니다. 단, 액세스 로그는 사이트별로 명시해야 켜집니다.
example.com {
log {
output file /var/log/caddy/example.access.log {
roll_size 100MiB
roll_keep 10
roll_keep_for 720h
}
format json
}
reverse_proxy localhost:3000
}로그 로테이션이 내장되어 있어 logrotate 설정이 필요 없습니다. 콘솔에서 사람이 읽기 좋게 보고 싶다면 format console을 쓰면 됩니다.
journalctl -u caddy -f # systemd로 실행 중인 Caddy의 프로세스 로그9. Nginx 2~3편 내용을 Caddy로 옮기면
2~3편에서 만든 "도메인 2개 + 리다이렉트 + 정적 파일 + API 프록시 + WebSocket" 구성을 Caddy로 옮기면 이렇게 됩니다.
{
email admin@example.com
}
www.example.com {
redir https://example.com{uri} permanent
}
example.com {
encode zstd gzip
handle /ws/* {
reverse_proxy localhost:4000
}
handle_path /api/* {
reverse_proxy localhost:8080
}
handle {
root * /var/www/example
try_files {path} /index.html
file_server
}
log {
output file /var/log/caddy/example.log
}
}인증서 발급·갱신, HTTP→HTTPS 리다이렉트, HTTP/2·3, WebSocket 헤더 처리, 프록시 헤더가 모두 이 안에 암묵적으로 들어 있습니다.
정리
- Caddy는 Go로 작성된 단일 바이너리 서버로, "HTTPS 기본값", "API 기반 JSON 설정", "모듈 구조"가 정체성이다.
- 사이트 주소의 형태(도메인/IP/포트/스킴)가 곧 HTTPS 정책이다.
- 자동 HTTPS는 DNS와 80/443 접근이 전제이며, 폐쇄망에서는
tls internal이나 인증서 파일 지정으로 대응한다. reverse_proxy한 줄이 프록시 헤더, WebSocket, 커넥션 재사용을 기본으로 처리한다.- 경로 매처는 정확 일치가 기본이므로
/api/*처럼 와일드카드를 붙여야 한다.
다음 편에서는 Caddy를 운영 수준으로 쓰기 위한 매처, 지시어 순서, 스니펫, 로드 밸런싱과 액티브 헬스 체크, Admin API, 플러그인 빌드, On-Demand TLS를 다룹니다.