개요
Caddy는 자동 HTTPS를 기본 제공하는 모던 오픈소스 웹 서버이자 리버스 프록시입니다. Go 언어로 작성되어 단일 바이너리로 배포되며, Let’s Encrypt 인증서를 자동으로 발급하고 갱신합니다. 간결한 Caddyfile 문법으로 복잡한 설정 없이 안전한 웹 서비스를 구축할 수 있습니다.
GitHub: https://github.com/caddyserver/caddy
공식 사이트: https://caddyserver.com
GitHub Stars: 61,000+
라이선스: Apache 2.0
Caddy란?
Caddy는 “The Ultimate Server with Automatic HTTPS”를 표방하는 현대적인 웹 서버입니다. 2015년 첫 출시 이후 자동 HTTPS의 편리함으로 빠르게 인기를 얻었으며, 현재는 완전한 기능의 리버스 프록시, 로드 밸런서, 파일 서버로 발전했습니다.
핵심 철학
- Secure by Default: HTTPS가 기본값
- Simple Configuration: 인간이 읽기 쉬운 Caddyfile
- Zero Dependencies: 단일 바이너리, 외부 의존성 없음
- Production Ready: 엔터프라이즈급 안정성
주요 특징
1. 자동 HTTPS (핵심 기능)
- Let’s Encrypt 인증서 자동 발급
- 인증서 자동 갱신 (만료 전)
- HTTP → HTTPS 자동 리다이렉트
- OCSP Stapling 자동 적용
- 와일드카드 인증서 지원 (DNS Challenge)
2. 간결한 설정 (Caddyfile)
example.com {
reverse_proxy localhost:8080
}
이것이 전부입니다. HTTPS, 인증서 발급, 갱신이 모두 자동!
3. 현대적인 프로토콜 지원
- HTTP/1.1, HTTP/2, HTTP/3 (QUIC)
- WebSocket
- gRPC
- FastCGI (PHP)
4. 강력한 리버스 프록시
- 로드 밸런싱 (Round Robin, Least Conn 등)
- Health Check (Active/Passive)
- 동적 업스트림
- 헤더 조작
- 요청/응답 버퍼링
5. 유연한 설정 방식
- Caddyfile: 인간 친화적인 설정 파일
- JSON API: 프로그래밍 방식 설정
- 어댑터: Nginx, YAML, TOML 등 다양한 형식 지원
6. 모듈형 아키텍처
- 플러그인으로 기능 확장
- DNS 프로바이더 모듈 (Cloudflare, Route53 등)
- 인증 모듈
- 캐싱 모듈
유사 도구 비교
| 항목 | Caddy | Nginx Proxy Manager | Traefik | Nginx |
|---|---|---|---|---|
| 자동 HTTPS | ✅ 내장 (핵심) | ✅ | ✅ | ❌ 수동 |
| 설정 방식 | Caddyfile | Web UI | 레이블/파일 | 설정 파일 |
| 설정 난이도 | 매우 쉬움 | 쉬움 | 어려움 | 중간 |
| 자동 서비스 발견 | ⚠️ 플러그인 | ❌ | ✅ 내장 | ❌ |
| 대시보드 | ❌ | ✅ | ✅ | ❌ |
| HTTP/3 | ✅ | ❌ | ✅ | ⚠️ 별도 |
| 성능 | 좋음 | 매우 좋음 | 좋음 | 매우 좋음 |
| 메모리 사용량 | 적음 | 적음 | 많음 | 적음 |
| 학습 곡선 | 낮음 | 매우 낮음 | 높음 | 중간 |
언제 Caddy를 선택할까?
Caddy가 적합한 경우:
- HTTPS를 가장 쉽게 설정하고 싶을 때
- 간결한 설정 파일을 선호할 때
- 빠르게 리버스 프록시를 구성하고 싶을 때
- 소규모~중규모 프로젝트
- 홈서버/개인 프로젝트
다른 도구가 적합한 경우:
- GUI로 관리하고 싶다면 → Nginx Proxy Manager
- 컨테이너 자동 발견이 필요하다면 → Traefik
- 최고의 성능/세밀한 제어가 필요하다면 → Nginx
- 대규모 마이크로서비스 환경이라면 → Traefik
Docker Compose 설치
사전 준비
- Docker 및 Docker Compose 설치
- 도메인 (예:
example.com) – HTTPS 사용 시 - 포트 80, 443 개방
1. 디렉토리 구조 생성
mkdir -p ~/docker/caddy/{conf,site,data,config}
cd ~/docker/caddy
2. docker-compose.yml 작성
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
cap_add:
- NET_ADMIN # HTTP/3 성능 최적화
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./conf/Caddyfile:/etc/caddy/Caddyfile:ro
- ./site:/srv
- ./data:/data
- ./config:/config
environment:
- TZ=Asia/Seoul
networks:
- caddy-network
networks:
caddy-network:
driver: bridge
3. Caddyfile 작성 (기본)
conf/Caddyfile 파일 생성:
# 전역 설정
{
email your-email@example.com
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory # 테스트용
}
# 기본 사이트 (정적 파일)
example.com {
root * /srv
file_server
encode gzip zstd
}
4. 컨테이너 실행
docker compose up -d
5. 로그 확인
docker compose logs -f caddy
Caddyfile 예시
예시 1: 기본 리버스 프록시
app.example.com {
reverse_proxy localhost:8080
}
예시 2: Docker 컨테이너로 프록시
# Docker 네트워크 내에서는 컨테이너 이름으로 접근
app.example.com {
reverse_proxy app-container:3000
}
예시 3: 여러 서비스
# Portainer
portainer.example.com {
reverse_proxy portainer:9000
}
# Nextcloud
cloud.example.com {
reverse_proxy nextcloud:80
}
# API 서버
api.example.com {
reverse_proxy api:8080
}
예시 4: 경로 기반 라우팅
example.com {
# /api/* 요청은 백엔드로
reverse_proxy /api/* api-server:8080
# /admin/* 요청은 관리자 서버로
reverse_proxy /admin/* admin-server:9000
# 나머지는 정적 파일
root * /srv
file_server
}
예시 5: 로드 밸런싱
example.com {
reverse_proxy {
to backend1:8080
to backend2:8080
to backend3:8080
lb_policy round_robin
health_uri /health
health_interval 10s
}
}
예시 6: Basic Auth
admin.example.com {
basicauth {
# htpasswd 형식: caddy hash-password 명령으로 생성
admin $2a$14$...hashed_password...
}
reverse_proxy admin-app:8080
}
예시 7: 헤더 조작
example.com {
header {
# 보안 헤더
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
-Server # Server 헤더 제거
}
reverse_proxy backend:8080
}
예시 8: 로그 설정
example.com {
log {
output file /var/log/caddy/access.log {
roll_size 10mb
roll_keep 10
}
format json
}
reverse_proxy backend:8080
}
전체 설정 예시
여러 서비스 + 공통 설정
# 전역 설정
{
email admin@example.com
# 로그 설정
log {
output stdout
level INFO
}
}
# 공통 스니펫 정의
(common) {
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
-Server
}
}
# 메인 웹사이트
example.com {
import common
root * /srv/main
file_server
}
# 블로그
blog.example.com {
import common
reverse_proxy ghost:2368
}
# API 서버
api.example.com {
import common
reverse_proxy api:8080
}
# Portainer (관리자 전용)
portainer.example.com {
import common
# IP 제한
@allowed remote_ip 192.168.1.0/24
handle @allowed {
reverse_proxy portainer:9000
}
respond "Forbidden" 403
}
# 미디어 서버
media.example.com {
import common
reverse_proxy jellyfin:8096
}
DNS Challenge (와일드카드 SSL)
Cloudflare DNS를 사용한 와일드카드 인증서:
1. 커스텀 Dockerfile
FROM caddy:2-builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare
FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
2. docker-compose.yml 수정
services:
caddy:
build: .
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
environment:
- TZ=Asia/Seoul
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
volumes:
- ./conf/Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data
- ./config:/config
networks:
- caddy-network
3. Caddyfile 수정
{
email admin@example.com
}
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
@portainer host portainer.example.com
handle @portainer {
reverse_proxy portainer:9000
}
@nextcloud host cloud.example.com
handle @nextcloud {
reverse_proxy nextcloud:80
}
# 기본 응답
handle {
respond "Not Found" 404
}
}
4. .env 파일
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token
다른 서비스와 연동
Docker 네트워크 설정
다른 컨테이너와 연동하려면 같은 네트워크에 있어야 합니다.
# caddy/docker-compose.yml
services:
caddy:
# ...
networks:
- caddy-network
networks:
caddy-network:
external: true
# 다른 서비스의 docker-compose.yml
services:
app:
image: my-app
# ports 노출 불필요! Caddy가 내부 네트워크로 접근
networks:
- caddy-network
networks:
caddy-network:
external: true
네트워크 생성
docker network create caddy-network
컨테이너 관리
기본 명령어
# 시작
docker compose up -d
# 로그 확인
docker compose logs -f caddy
# 설정 리로드 (재시작 없이)
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# 설정 검증
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
# 재시작
docker compose restart
# 업데이트
docker compose pull
docker compose up -d
인증서 확인
# 인증서 목록
docker compose exec caddy caddy list-modules
# 저장된 인증서 확인
ls -la data/caddy/certificates/
트러블슈팅
1. 인증서 발급 실패
원인: 포트 80이 차단되었거나 DNS 설정 오류
해결:
- 포트 80이 외부에서 접근 가능한지 확인
- DNS A 레코드가 서버 IP를 가리키는지 확인
- 방화벽 설정 확인
2. 502 Bad Gateway
원인: 업스트림 서비스 연결 실패
해결:
- 업스트림 컨테이너가 실행 중인지 확인
- 같은 Docker 네트워크에 있는지 확인
- 컨테이너 이름이 정확한지 확인
3. 설정 변경이 적용되지 않음
해결:
# 설정 리로드
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
4. HTTP/3가 작동하지 않음
해결:
cap_add: NET_ADMIN추가- UDP 포트 443 노출:
443:443/udp
로컬 개발 환경 (자체 서명 인증서)
내부 네트워크나 localhost에서 HTTPS 사용:
{
# 자체 서명 인증서 사용
local_certs
}
localhost {
tls internal
reverse_proxy app:3000
}
# 또는 .local 도메인
myapp.local {
tls internal
reverse_proxy app:3000
}
유용한 Caddyfile 패턴
www 리다이렉트
www.example.com {
redir https://example.com{uri} permanent
}
특정 경로 차단
example.com {
@blocked path /admin/* /wp-admin/*
respond @blocked "Forbidden" 403
reverse_proxy backend:8080
}
CORS 설정
api.example.com {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET, POST, OPTIONS"
header Access-Control-Allow-Headers "Content-Type"
reverse_proxy api:8080
}
정적 파일 캐싱
example.com {
@static path *.css *.js *.png *.jpg *.gif *.ico *.woff2
header @static Cache-Control "public, max-age=31536000"
root * /srv
file_server
}
마무리
Caddy는 “설정이 간단한 웹 서버”의 대명사입니다. 특히 자동 HTTPS 기능은 다른 어떤 웹 서버도 따라올 수 없는 편리함을 제공합니다. Nginx의 복잡한 설정이나 Traefik의 학습 곡선이 부담스러웠다면, Caddy를 강력히 추천합니다.
추천 대상
- HTTPS를 가장 쉽게 적용하고 싶은 분
- 간결한 설정 파일을 선호하는 분
- 홈서버/개인 프로젝트 운영자
- 빠르게 프로토타입을 구축하고 싶은 개발자
- Nginx 설정에 지친 분
Caddy의 철학
“Caddy는 기본적으로 안전하게 작동합니다. HTTPS는 옵션이 아니라 기본값입니다.”