Caddy – 자동 HTTPS 웹 서버 & 리버스 프록시 Docker 설치 가이드




개요

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 등)
  • 인증 모듈
  • 캐싱 모듈

유사 도구 비교

항목CaddyNginx Proxy ManagerTraefikNginx
자동 HTTPS✅ 내장 (핵심)❌ 수동
설정 방식CaddyfileWeb 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는 옵션이 아니라 기본값입니다.”


참고 자료




댓글 남기기