← posts/b.log()

blog92@web:~$ cat posts/nginx-caddy-05-caddy-basics.md

DEVOPS7 min read

Nginx & Caddy 완전 정복 5편 — Caddy 기초: Caddyfile과 자동 HTTPS

Caddy의 설계 철학과 구조, 설치와 CLI, Caddyfile 문법, 자동 HTTPS의 동작 원리, 정적 파일 서빙과 리버스 프록시 기본을 다룹니다.

1. Caddy는 어떤 서버인가

Caddy는 2015년 Matt Holt가 만든 Go 언어 기반 웹 서버로, 현재는 v2가 표준입니다(v1과 설정 체계가 완전히 다르므로 검색할 때 v2 문서인지 꼭 확인하세요).

Caddy의 정체성을 세 문장으로 요약하면 다음과 같습니다.

  1. HTTPS는 기본값이다. 도메인을 적으면 인증서 발급·갱신·HTTP→HTTPS 리다이렉트까지 자동으로 한다.
  2. 설정은 API로 관리되는 JSON이다. Caddyfile은 사람이 쓰기 편하게 만든 "어댑터"일 뿐이다.
  3. 모든 기능은 모듈이다. 표준 모듈 외 기능은 Go 플러그인으로 컴파일해 넣는다.

아키텍처: Nginx와 무엇이 다른가

text
         ┌─────────────── 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 (공식 저장소)

bash
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

bash
brew install caddy

Docker

bash
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는 설정 파일 없이 명령어 한 줄로 서버를 띄울 수 있습니다.

bash
# 현재 디렉터리를 정적 파일 서버로 (디렉터리 목록 표시)
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로 로컬 앱을 테스트하고 싶다"면 아주 유용합니다.

주요 명령어

bash
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 문법

기본 구조

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가 맞춰줍니다).
  • 세미콜론이 없습니다. 한 줄에 지시어 하나.
  • 주석은 #.

사이트 주소의 여러 형태

caddyfile
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 서비스를 하겠다"는 뜻으로 받아들입니다.

지시어와 매처 토큰

대부분의 지시어는 다음 형태입니다.

text
지시어 [매처] 인자들... {
	하위 지시어
}
caddyfile
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 환경에서 특히 편리합니다.

caddyfile
{$SITE_DOMAIN:localhost} {
	reverse_proxy {$UPSTREAM:app:3000}
}

5. 자동 HTTPS: 어떻게 동작하는가

Caddy의 간판 기능을 제대로 이해해 봅시다. 다음 설정 하나로:

caddyfile
example.com {
	respond "Hello HTTPS"
}

Caddy는 시작하면서 이런 일을 합니다.

  1. 도메인 판별: 사이트 주소가 공인 도메인처럼 보이면 공인 CA를, localhost·사설 IP·.internal/.local 같은 이름이면 내장 로컬 CA를 사용합니다.
  2. 인증서 발급: 기본적으로 Let's Encrypt에 요청하고, 실패하면 ZeroSSL로 자동 대체(fallback)합니다. 챌린지는 HTTP-01과 TLS-ALPN-01을 자동으로 시도합니다.
  3. HTTP → HTTPS 리다이렉트: 80 포트에 리다이렉트 서버를 자동으로 띄웁니다.
  4. 자동 갱신: 인증서 수명이 일정 비율 남으면 백그라운드에서 갱신합니다. 갱신 실패 시 재시도와 백오프도 알아서 합니다.
  5. OCSP 스테이플링: 인증서 폐기 상태 정보를 미리 받아 핸드셰이크에 포함합니다.
  6. 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

caddyfile
localhost {
	reverse_proxy localhost:3000
}

이렇게 하면 Caddy가 자체 루트 CA를 만들고 localhost 인증서를 발급합니다. 처음 실행 시 루트 인증서를 시스템 신뢰 저장소에 설치하려고 시도하며(권한이 필요할 수 있음), 수동으로는 caddy trust를 실행합니다. Secure Cookie, Service Worker, OAuth 콜백처럼 HTTPS가 필요한 기능을 로컬에서 테스트할 때 매우 편합니다.

폐쇄망 / 사내망에서는?

공인 CA에 접근할 수 없는 환경이라면 선택지는 두 가지입니다.

caddyfile
# (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 끄기

caddyfile
{
	auto_https off          # 전부 끄기
	# auto_https disable_redirects   # 리다이렉트만 끄기
}

또는 사이트 주소를 http://example.com이나 :80으로 쓰면 해당 사이트만 HTTP로 서비스합니다. Caddy 앞에 이미 TLS를 처리하는 로드 밸런서가 있을 때 이렇게 씁니다.

6. 정적 파일 서빙

caddyfile
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 등)

caddyfile
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. 리버스 프록시 기본

caddyfile
example.com {
	reverse_proxy localhost:3000
}

이 한 줄이 Nginx 3편에서 길게 설명한 내용 대부분을 기본값으로 처리합니다.

Nginx에서 수동으로 하던 것Caddy 기본 동작
proxy_set_header Host $hostHost 헤더를 원본 그대로 전달
X-Forwarded-For / -Proto / -Host자동 설정
WebSocket Upgrade/Connection 헤더자동 처리
proxy_http_version 1.1 + keepalive업스트림 커넥션 풀 기본 사용
HTTP/2 백엔드 (gRPC 등)h2c:// 스킴으로 지정

경로별 라우팅

caddyfile
example.com {
	# /api/* → 백엔드 (경로 유지: /api/users → :8080/api/users)
	reverse_proxy /api/* localhost:8080
 
	# 나머지 → 프런트엔드
	reverse_proxy localhost:3000
}

"/api를 떼고 넘기고 싶다"면 handle_path를 씁니다.

caddyfile
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의 차이는 다음 편에서 정리합니다.

업스트림 쪽 헤더 조작

caddyfile
reverse_proxy localhost:8080 {
	header_up X-Request-Start "{time.now.unix_ms}"   # 백엔드로 보내는 요청 헤더
	header_down -Server                              # 클라이언트로 가는 응답 헤더 제거
}

HTTPS 백엔드로 프록시할 때 백엔드가 자기 도메인의 Host를 기대한다면:

caddyfile
reverse_proxy https://api.partner.com {
	header_up Host {upstream_hostport}
}

8. 로그

Caddy는 기본 로그가 구조화된 JSON입니다. 단, 액세스 로그는 사이트별로 명시해야 켜집니다.

caddyfile
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을 쓰면 됩니다.

bash
journalctl -u caddy -f        # systemd로 실행 중인 Caddy의 프로세스 로그

9. Nginx 2~3편 내용을 Caddy로 옮기면

2~3편에서 만든 "도메인 2개 + 리다이렉트 + 정적 파일 + API 프록시 + WebSocket" 구성을 Caddy로 옮기면 이렇게 됩니다.

caddyfile
{
	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를 다룹니다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..