자동 HTTPS와 함께 Caddy 웹 서버를 설치하는 방법
공식 저장소에서 Ubuntu에 Caddy를 설치하고, 정적 사이트와 리버스 프록시용 Caddyfile을 쓰고, 자동 인증서, systemd, 로그, Nginx에서 이전할 때 흔한 실수를 이해합니다.

Caddy는 기본적으로 TLS 인증서를 받고 갱신하는 웹 서버입니다. 도메인 하나나 둘을 호스팅하는 작은 VPS에서는 Certbot 타이머와 Nginx 스니펫 파일 한 종류가 사라집니다. 설정 언어(Caddyfile)는 짧습니다. 리버스 프록시, gzip, HTTP/2는 주말 내내 추가 모듈이 아니라 평범한 기능입니다.
Nginx는 이미 실전에서 검증된 설정이 있거나 매우 특정한 모듈이 필요한 팀에는 여전히 올바른 선택입니다. 이 가이드는 다른 흔한 경우용입니다. include 스니펫을 외우지 않고 새 Hiddence VPS에서 동작하는 HTTPS를 원할 때입니다. 공식 apt 저장소에서 Caddy를 설치하고, Let's Encrypt와 어떻게 대화하는지 설명하고, 정적 사이트를 제공하고, 로컬 앱을 프록시하고, 여러 도메인을 호스팅하고, 로그를 보고, Apache나 옛 Nginx와의 포트 80 싸움을 다룹니다.
Caddy가 잘 맞을 때
제목은 자동 HTTPS이지만, 일상의 이득은 움직이는 부품이 적다는 것입니다. Caddy는 80과 443을 듣고 HTTP를 HTTPS로 리다이렉트하며 인증서를 데이터 디렉터리에 저장합니다. 도메인이 VPS를 가리켜야 하는 것은 여전합니다. ACME를 깨지 않아야 하는 것도 여전합니다(방화벽 80/tcp, :80을 훔치는 다른 프로세스 없음). 와일드카드 인증서는 DNS 제공자 모듈이 필요합니다. 단일 호스트 인증서보다 긴 길입니다.
- 자동 HTTP→HTTPS와 인증서 갱신
- 긴 server 블록 대신 읽기 쉬운 Caddyfile
- Node, Python, 추가 설정의 PHP-FPM, Docker 백엔드용 충분한 리버스 프록시
- 암호 스위트 스프레드시트 없이 HTTP/2와 현대 TLS 기본값
- 공식 패키지의 systemd 유닛
요구 사항
Caddy가 ACME HTTP-01을 증명하기 전에 도메인이 이 VPS로 해석되어야 합니다. DNS가 아직 전파 중이면 Caddy는 발급에 실패하고 재시도합니다. 'Caddy가 고장'처럼 보이지만 DNS만일 수 있습니다. Apache나 Nginx가 80/443을 가지고 있으면 먼저 중지하세요.
- Ubuntu 22.04 또는 24.04
- VPS IP를 가리키는 도메인 A 레코드(IPv6를 쓰면 AAAA도)
- 포트 80과 443이 비어 있고 방화벽에서 허용됨
- root 또는 sudo
1단계: 공식 저장소에서 Caddy 설치
현재 TLS와 ACME 동작을 원하면 기본 Ubuntu universe의 오래된 Caddy를 무작위로 설치하지 마세요. Caddy 프로젝트는 apt 소스를 문서화합니다. 설치 후 caddy 사용자가 존재하고 서비스가 활성화됩니다.
ssh root@YOUR_VPS_IP
apt update && apt -y install debian-keyring debian-archive-keyring apt-transport-https curl gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt -y install caddy
caddy version
systemctl status caddy --no-pager2단계: 첫 Caddyfile — 정적 사이트
기본 Caddyfile은 /etc/caddy/Caddyfile입니다. 자신의 도메인으로 바꾸세요. 호스트 이름이 localhost가 아니면 설정이 로드되자마자 인증서를 받으려 합니다. 사이트 파일은 caddy 사용자가 읽을 수 있는 디렉터리에 두세요(www-data 스타일 권한이 흔하지만 패키지는 사용자 caddy를 씁니다).
mkdir -p /var/www/example
echo '<h1>It works</h1>' > /var/www/example/index.html
chown -R caddy:caddy /var/www/example
cat >/etc/caddy/Caddyfile <<'EOF'
example.com {
root * /var/www/example
file_server
encode gzip
}
EOF
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
# Watch issuance:
journalctl -u caddy -f3단계: 로컬 애플리케이션 리버스 프록시
Gunicorn, Node, Docker가 127.0.0.1:8000에서 들으면 Caddy가 유일한 공용 프로세스여야 합니다. reverse_proxy 지시문은 대부분의 앱에 타당한 방식으로 Host와 X-Forwarded-*를 전달합니다. 앱이 절대 HTTP URL을 만들면 앱에서 신뢰하는 프록시 / HTTPS 플래그를 설정하세요(Django SECURE_PROXY_SSL_HEADER, Express trust proxy 등).
app.example.com {
encode gzip
reverse_proxy 127.0.0.1:8000
}
# Several hosts in one file are normal:
# blog.example.com {
# root * /var/www/blog
# file_server
# }4단계: 방화벽과 systemd
80과 443을 허용하세요. Caddy 유닛은 caddy.service입니다. 프로세스가 건강하면 Caddyfile 편집 후 reload면 충분합니다. DNS 플러그인용 환경 변수를 바꾸면 전체 재시작이 더 분명합니다. 인증서는 기본적으로 /var/lib/caddy/.local/share/caddy/에 있습니다. 재설치 시 속도 제한이 걱정되면 그 경로를 백업에 넣으세요.
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
systemctl enable --now caddy
systemctl reload caddy
# Backup cert storage (path may vary slightly by version):
ls -la /var/lib/caddy/5단계: 로그, 압축, 헤더
봇이 경로를 두드리거나 404를 디버깅할 때 액세스 로그가 도움이 됩니다. 사이트별로 기록할 수 있습니다. 브라우저 앱을 호스팅하면 보안 헤더를 추가하세요. HSTS를 이해하지 않고 거대한 헤더 팩을 복사하지 마세요. 긴 max-age를 한 번 설정하면 브라우저가 기억합니다.
example.com {
root * /var/www/example
file_server
encode gzip
log {
output file /var/log/caddy/example.log
}
header {
X-Content-Type-Options nosniff
Referrer-Policy no-referrer-when-downgrade
-Server
}
}
mkdir -p /var/log/caddy
chown caddy:caddy /var/log/caddy
systemctl reload caddy6단계: PHP와 기타 추가
Caddy는 php_fastcgi 지시문으로 PHP-FPM과 이야기할 수 있습니다. 많은 WordPress나 Laravel 호스트에는 충분하지만 php-fpm 설치와 일치하는 소켓 경로는 여전히 필요합니다. 완벽한 Nginx PHP 설정이 이미 있으면 하룻저녁 이전은 선택입니다. Caddy가 빛나는 곳은 리버스 프록시 + 정적 + 자동 TLS입니다.
example.com {
root * /var/www/example
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}
# Confirm FPM is running:
systemctl status php8.3-fpm다운타임 놀라움 없이 Nginx에서 이전하기
Caddy를 시작하기 전에 Nginx를 중지하세요. 그렇지 않으면 80/443을 다툽니다. IP도 옮기면 전날 DNS TTL을 낮추세요. A 레코드를 바꾸기 전에 curl --resolve로 새 VPS를 치도록 테스트하세요. 롤백을 위해 Nginx 설정을 일주일 git에 남겨 두세요.
- systemctl stop nginx && systemctl disable nginx
- Caddy를 설치하고 Caddyfile을 검증한 뒤 Caddy 시작
- curl -I --resolve example.com:443:NEW_IP https://example.com
- IP가 새로울 때만 그다음에 DNS 변경
- 재발급은 자동입니다. 같은 호스트 이름에 Certbot도 돌리지 마세요
문제 해결
ACME 실패는 거의 항상 DNS, 방화벽, 또는 포트 80의 다른 서비스입니다. Caddy 로그 줄은 tls.obtain을 언급합니다. HTTP에서는 되고 HTTPS에서는 안 되면 발급이 완료되지 않은 것입니다. 인증서가 너무 많으면 Let's Encrypt 속도 제한입니다. 테스트 중에는 프로덕션이 아니라 staging CA를 쓰세요.
- validate 실패: Caddyfile 구문, 빠진 중괄호
- root에서 permission denied: 사이트 디렉터리를 caddy로 chown
- bind: address already in use — ss -tulpn | grep -E ':80|:443'
- 인증서 시간 초과: VPS에서 도메인을 dig +short, ufw allow 80
- 502 reverse_proxy: 백엔드 다운, 또는 컨테이너 네트워크에서 localhost로 잘못 프록시
보안 메모
Caddy의 TLS 기본값은 보수적입니다. 당신의 일은 애플리케이션 보안과 SSH입니다. 공용 인터페이스에서 Caddy 관리 API를 켜지 마세요. 기본 관리 엔드포인트는 로컬입니다. 그대로 두세요. 인터넷의 Caddyfile을 쓰면 모든 matcher를 읽으세요. /*를 내부 IP로 reverse_proxy하는 스니펫은 개방 프록시가 될 수 있습니다.
- 관리 API를 노출하지 않음
- 패키지를 업데이트된 상태로 유지
- 모든 호스트 이름에서 HTTPS가 확실한 뒤에만 HSTS
- ACME 속도 제한을 지키려면 테스트와 프로덕션 호스트 이름을 분리
팁
- caddy fmt --overwrite /etc/caddy/Caddyfile로 파일을 읽기 쉽게 유지
- 비슷한 사이트가 많으면 import 스니펫 사용
- Docker에서는 마운트된 Caddyfile이 있는 caddy:alpine 이미지가 흔함
- 와일드카드 인증서는 DNS 모듈과 API 토큰이 필요 — 그 토큰을 보호
- 파일 전체를 다시 쓰기 전에 journalctl -u caddy를 읽기
VPS의 Caddy는 공식 패키지, 짧은 Caddyfile, 열린 포트 80/443, 이미 서버를 가리키는 DNS, 편집 후 systemd reload입니다. 정적 파일 서버로, 또는 Gunicorn, Node, Docker 앞의 리버스 프록시로 쓰세요. ACME가 포트 80에서 답할 수 있을 때 자동 HTTPS가 동작합니다. 발급이 실패하면 Caddy를 탓하기 전에 DNS와 포트 충돌을 고치세요. 자주 재설치하면 /var/lib/caddy를 백업하세요.