일상 코딩
[윈도우 개발 환경 설정] 11편: Caddy 웹 서버 & 리버스 프록시 (리눅스 서버 기준) 본문
[윈도우 개발 환경 설정] 11편: Caddy 웹 서버 & 리버스 프록시 (리눅스 서버 기준)
polarcompass 2026. 3. 31. 22:2911편: Caddy 웹 서버 & 리버스 프록시 (리눅스 서버 기준)
시리즈: 윈도우 네이티브 개발 환경 구축부터 클라우드 배포까지 (11/14)
환경: Ubuntu 22.04+ / Debian 12+ (클라우드 리눅스 서버)
이전 편 전제: 1~10편까지 로컬 개발 환경 구축 완료
서론
10편까지 윈도우 로컬 환경에서 프론트엔드(React SPA), 백엔드(Go Gin, FastAPI), 데이터베이스(PostgreSQL), 오브젝트 스토리지(MinIO), 워크플로우 자동화(n8n)까지 모두 세팅했습니다. 이제 이 서비스들을 실제 사용자에게 제공할 차례입니다.
이번 편에서는 클라우드 리눅스 서버에 Caddy를 설치하고, React SPA 정적 파일 서빙, Go Gin / FastAPI 리버스 프록시, 자동 HTTPS까지 구성합니다. Caddy는 Nginx나 Apache에 비해 설정이 극도로 간결하고, Let's Encrypt 인증서를 자동으로 발급·갱신해 주기 때문에 소규모~중규모 프로젝트에 특히 적합합니다.
1. Caddy란?
Caddy는 Go로 작성된 오픈소스 웹 서버입니다. 핵심 특징은 세 가지입니다.
자동 HTTPS: 도메인을 지정하기만 하면 Let's Encrypt 인증서를 자동으로 발급하고 갱신합니다. 별도의 certbot 설치나 크론잡이 필요 없습니다.
간결한 설정: Nginx의 nginx.conf에 비해 Caddyfile 문법이 훨씬 직관적입니다. 몇 줄이면 리버스 프록시와 정적 파일 서빙을 구성할 수 있습니다.
단일 바이너리: 의존성 없이 바이너리 하나로 동작합니다. 배포와 관리가 단순합니다.
2. Caddy 설치 (Ubuntu/Debian)
2-1. apt 패키지 매니저로 설치 (권장)
공식 리포지토리를 등록한 뒤 apt로 설치합니다.
# 의존성 패키지 설치
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
# Caddy 공식 GPG 키 등록
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
# Caddy 공식 리포지토리 등록
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 -y caddy
설치가 완료되면 Caddy가 자동으로 systemd 서비스로 등록됩니다.
# 버전 확인
caddy version
v2.9.1 h1:...
2-2. 공식 바이너리로 직접 설치 (대안)
특정 버전이 필요하거나 apt를 사용하기 어려운 환경에서는 바이너리를 직접 다운로드합니다.
# 바이너리 다운로드 (amd64 기준)
curl -L "https://caddyserver.com/api/download?os=linux&arch=amd64" -o /usr/local/bin/caddy
# 실행 권한 부여
sudo chmod +x /usr/local/bin/caddy
# 버전 확인
caddy version
바이너리로 직접 설치한 경우 systemd 서비스 파일을 수동으로 등록해야 합니다. 이 내용은 8절에서 다룹니다.
3. Caddyfile 기본 문법
Caddy의 설정 파일은 /etc/caddy/Caddyfile에 위치합니다. 문법 구조를 먼저 익혀 두겠습니다.
3-1. 기본 구조
# 사이트 주소 (도메인 또는 :포트)
example.com {
# 지시어(directive)들
root * /var/www/html
file_server
}
Caddyfile은 사이트 블록 단위로 구성됩니다. 사이트 블록은 주소 { ... } 형태이고, 블록 안에 지시어를 나열합니다.
3-2. 핵심 지시어 요약
# 정적 파일 서빙
root * /var/www/html # 루트 디렉토리 지정
file_server # 파일 서버 활성화
# 리버스 프록시
reverse_proxy localhost:8080
# SPA 폴백 (존재하지 않는 경로를 index.html로 전달)
try_files {path} /index.html
# 응답 헤더 설정
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
# Gzip/Zstd 압축
encode gzip zstd
# 로그
log {
output file /var/log/caddy/access.log
}
3-3. 전역 옵션 블록
파일 맨 위에 중괄호 없이 { } 블록을 두면 전역 설정이 됩니다.
{
# 이메일 주소 (Let's Encrypt 인증서 발급용)
email admin@example.com
# 개발 중 HTTPS 비활성화가 필요할 때
# auto_https off
}
4. React SPA 정적 파일 서빙
React SPA를 빌드하면 dist/ 폴더에 정적 파일이 생성됩니다. 이를 Caddy로 서빙하는 설정입니다.
4-1. 빌드 파일 배치
로컬에서 빌드한 파일을 서버로 전송합니다. (13편에서 배포 자동화를 다루지만, 여기서는 수동 전송을 가정합니다.)
# 서버에 디렉토리 생성
sudo mkdir -p /var/www/myapp
# 로컬에서 scp로 전송 (로컬 PowerShell에서 실행)
# scp -r .\dist\* user@your-server:/var/www/myapp/
4-2. Caddyfile 설정
myapp.example.com {
root * /var/www/myapp
encode gzip zstd
# SPA 폴백: 파일이 존재하지 않으면 index.html로
try_files {path} /index.html
file_server
# 정적 자산 캐시 설정
@static path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ico
header @static Cache-Control "public, max-age=31536000, immutable"
# index.html은 캐시하지 않음
@html path /index.html
header @html Cache-Control "no-cache, no-store, must-revalidate"
log {
output file /var/log/caddy/myapp-access.log
}
}
핵심은 try_files {path} /index.html입니다. React Router 등 클라이언트 사이드 라우팅을 사용할 때, /about이나 /users/123 같은 경로로 직접 접근하면 서버에는 해당 파일이 존재하지 않습니다. 이 지시어가 해당 요청을 index.html로 넘겨주고, React Router가 클라이언트에서 라우팅을 처리합니다.
Vite로 빌드하면 JS, CSS 파일명에 해시가 포함되므로(예: index-3a7b2c.js) immutable 캐시를 적용해도 안전합니다. 반면 index.html은 항상 최신 버전을 가져와야 하므로 캐시를 비활성화합니다.
5. Go Gin / FastAPI 리버스 프록시 설정
5-1. 단일 백엔드 리버스 프록시
Go Gin 서버가 localhost:8080에서 동작한다면 다음과 같이 설정합니다.
api.example.com {
reverse_proxy localhost:8080
log {
output file /var/log/caddy/api-access.log
}
}
이것만으로 https://api.example.com에 대한 모든 요청이 Go Gin 서버로 전달됩니다. HTTPS 인증서는 Caddy가 자동으로 처리합니다.
5-2. 경로 기반 라우팅 (하나의 도메인에서 여러 백엔드)
프론트엔드, Go API, Python API를 하나의 도메인에서 경로로 구분하는 구성입니다.
myapp.example.com {
# /api/v1/* → Go Gin (포트 8080)
handle_path /api/v1/* {
reverse_proxy localhost:8080
}
# /api/v2/* → FastAPI (포트 8000)
handle_path /api/v2/* {
reverse_proxy localhost:8000
}
# 나머지 → React SPA
handle {
root * /var/www/myapp
try_files {path} /index.html
file_server
}
encode gzip zstd
log {
output file /var/log/caddy/myapp-access.log
}
}
handle_path는 매칭된 경로 접두사를 제거한 뒤 백엔드로 전달합니다. 예를 들어 /api/v1/users 요청은 Go Gin 서버에 /users로 전달됩니다. 접두사를 유지하고 싶다면 handle_path 대신 handle을 사용하면 됩니다.
5-3. 서브도메인 기반 라우팅
서비스별로 서브도메인을 분리하는 구성입니다.
# React SPA
myapp.example.com {
root * /var/www/myapp
try_files {path} /index.html
file_server
encode gzip zstd
}
# Go Gin API
go-api.example.com {
reverse_proxy localhost:8080
}
# FastAPI
py-api.example.com {
reverse_proxy localhost:8000
}
# n8n (워크플로우 자동화)
n8n.example.com {
reverse_proxy localhost:5678
}
# MinIO Console
minio.example.com {
reverse_proxy localhost:9001
}
각 서브도메인에 대해 Caddy가 개별적으로 HTTPS 인증서를 발급합니다. DNS 레코드에서 각 서브도메인이 서버 IP를 가리키도록 설정해 두어야 합니다.
6. 자동 HTTPS (Let's Encrypt)
6-1. 동작 원리
Caddy의 자동 HTTPS는 별도 설정 없이 기본으로 활성화됩니다. Caddyfile에 도메인을 적기만 하면 다음이 자동으로 수행됩니다.
첫째, Let's Encrypt에 ACME 프로토콜로 인증서 발급을 요청합니다. 둘째, HTTP-01 또는 TLS-ALPN-01 챌린지로 도메인 소유권을 검증합니다. 셋째, 인증서를 받아 HTTPS를 활성화합니다. 넷째, 만료 전에 자동으로 갱신합니다. 다섯째, HTTP 요청을 HTTPS로 자동 리다이렉트합니다.
6-2. 필수 전제 조건
자동 HTTPS가 동작하려면 두 가지 조건이 필요합니다.
DNS 설정: 도메인(또는 서브도메인)의 A 레코드가 서버의 공인 IP를 가리켜야 합니다.
포트 개방: 서버의 80(HTTP)번과 443(HTTPS)번 포트가 외부에서 접근 가능해야 합니다. 클라우드 방화벽(보안 그룹)과 OS 방화벽(ufw 등) 모두 확인합니다.
# ufw 방화벽 설정
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
6-3. 이메일 설정
Let's Encrypt에서 인증서 만료 알림을 받으려면 이메일을 등록합니다.
{
email admin@example.com
}
myapp.example.com {
# ...
}
6-4. 스테이징 환경에서 테스트
Let's Encrypt에는 요청 횟수 제한(Rate Limit)이 있습니다. 설정을 반복적으로 테스트할 때는 스테이징 CA를 사용합니다.
{
# 스테이징 CA (브라우저에서 신뢰하지 않는 인증서가 발급됨)
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
테스트가 끝나면 이 줄을 제거하거나 주석 처리하여 프로덕션 CA로 전환합니다.
7. 로컬 테스트용 vs 프로덕션 Caddyfile 분리
실제 운영에서는 로컬 테스트와 프로덕션 설정을 분리하는 것이 좋습니다.
7-1. 디렉토리 구조
/etc/caddy/
├── Caddyfile # 메인 설정 (프로덕션)
├── Caddyfile.dev # 로컬 테스트용
└── conf.d/ # 사이트별 분리 (선택)
├── myapp.caddy
├── go-api.caddy
└── py-api.caddy
7-2. 로컬 테스트용 Caddyfile (Caddyfile.dev)
서버에서 도메인 없이 빠르게 테스트하고 싶을 때 사용합니다.
# /etc/caddy/Caddyfile.dev
{
# 로컬 테스트: 자동 HTTPS 비활성화
auto_https off
}
:8080 {
root * /var/www/myapp
try_files {path} /index.html
file_server
encode gzip zstd
}
:8081 {
reverse_proxy localhost:3000 # Go Gin
}
:8082 {
reverse_proxy localhost:8000 # FastAPI
}
# 테스트용 설정으로 Caddy 실행
sudo caddy run --config /etc/caddy/Caddyfile.dev
포트 번호만 사용하면(도메인 없이) Caddy는 자동 HTTPS를 시도하지 않으므로 도메인이 연결되지 않은 상태에서도 테스트할 수 있습니다.
7-3. 프로덕션 Caddyfile (import로 분리)
사이트가 여러 개일 때는 import 지시어로 설정을 분리합니다.
# /etc/caddy/Caddyfile
{
email admin@example.com
}
import /etc/caddy/conf.d/*.caddy
# /etc/caddy/conf.d/myapp.caddy
myapp.example.com {
root * /var/www/myapp
try_files {path} /index.html
file_server
encode gzip zstd
@static path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ico
header @static Cache-Control "public, max-age=31536000, immutable"
@html path /index.html
header @html Cache-Control "no-cache, no-store, must-revalidate"
log {
output file /var/log/caddy/myapp-access.log
}
}
# /etc/caddy/conf.d/go-api.caddy
go-api.example.com {
reverse_proxy localhost:8080
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
}
log {
output file /var/log/caddy/go-api-access.log
}
}
# /etc/caddy/conf.d/py-api.caddy
py-api.example.com {
reverse_proxy localhost:8000
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
}
log {
output file /var/log/caddy/py-api-access.log
}
}
설정을 분리하면 사이트 추가·삭제 시 다른 설정에 영향을 주지 않아 관리가 편합니다.
8. systemd 서비스 등록 & 관리
8-1. apt로 설치한 경우
apt로 설치하면 systemd 서비스가 자동으로 등록됩니다. 바로 사용하면 됩니다.
# 서비스 시작
sudo systemctl start caddy
# 서비스 상태 확인
sudo systemctl status caddy
# 부팅 시 자동 시작 설정
sudo systemctl enable caddy
8-2. 바이너리로 직접 설치한 경우
systemd 서비스 파일을 수동으로 생성합니다.
# caddy 전용 사용자/그룹 생성
sudo groupadd --system caddy
sudo useradd --system \
--gid caddy \
--create-home \
--home-dir /var/lib/caddy \
--shell /usr/sbin/nologin \
caddy
# 서비스 파일 생성
sudo tee /etc/systemd/system/caddy.service > /dev/null <<'EOF'
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
EOF
# 설정 디렉토리 생성 & 권한 설정
sudo mkdir -p /etc/caddy
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy
# systemd 데몬 리로드 & 서비스 시작
sudo systemctl daemon-reload
sudo systemctl enable --now caddy
8-3. 일상적인 관리 명령어
# Caddyfile 문법 검증 (서비스 재시작 전에 반드시 실행)
caddy validate --config /etc/caddy/Caddyfile
# 설정 변경 후 무중단 리로드 (서비스 재시작 아님, 다운타임 없음)
sudo systemctl reload caddy
# 서비스 재시작 (설정 리로드로 해결이 안 될 때)
sudo systemctl restart caddy
# 로그 확인 (실시간)
sudo journalctl -u caddy -f
# 로그 확인 (최근 100줄)
sudo journalctl -u caddy -n 100 --no-pager
reload와 restart의 차이가 중요합니다. reload는 Caddy 프로세스를 유지한 채 설정만 교체하므로 다운타임이 없습니다. 일반적인 Caddyfile 수정 후에는 항상 reload를 사용합니다.
9. Docker 컨테이너로 Caddy 구동
systemd 서비스 대신 Docker로 Caddy를 운영할 수도 있습니다. 14편에서 다룰 통합 Docker Compose에 Caddy를 포함시키고 싶을 때 유용합니다.
9-1. 디렉토리 구조
~/caddy-docker/
├── docker-compose.yml
├── Caddyfile
├── site/ # React SPA 빌드 파일
│ └── index.html
├── caddy_data/ # 인증서 저장 (자동 생성)
└── caddy_config/ # Caddy 내부 설정 (자동 생성)
9-2. Caddyfile
{
email admin@example.com
}
myapp.example.com {
root * /srv/site
try_files {path} /index.html
file_server
encode gzip zstd
}
go-api.example.com {
# Docker 네트워크에서는 컨테이너 이름으로 접근
reverse_proxy go-app:8080
}
py-api.example.com {
reverse_proxy py-app:8000
}
Docker 컨테이너 환경에서 리버스 프록시 대상을 지정할 때는 localhost 대신 컨테이너 이름을 사용합니다. 같은 Docker 네트워크 안에 있으면 컨테이너 이름이 DNS처럼 동작합니다.
9-3. docker-compose.yml
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3 (QUIC) 지원
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./site:/srv/site:ro
- ./caddy_data:/data
- ./caddy_config:/config
networks:
- app-network
# 예시: Go Gin 앱 (이미지가 빌드되어 있다고 가정)
go-app:
image: my-go-app:latest
container_name: go-app
restart: unless-stopped
expose:
- "8080"
networks:
- app-network
# 예시: FastAPI 앱
py-app:
image: my-py-app:latest
container_name: py-app
restart: unless-stopped
expose:
- "8000"
networks:
- app-network
networks:
app-network:
driver: bridge
ports와 expose의 차이에 주의합니다. Caddy만 외부 포트(80, 443)를 열고, 백엔드 앱들은 expose로 Docker 네트워크 내부에서만 접근 가능하게 합니다. 이렇게 하면 백엔드에 직접 접근하는 것을 방지할 수 있습니다.
9-4. 실행 & 관리
# 실행
docker compose up -d
# Caddyfile 수정 후 리로드 (컨테이너 재시작 없이)
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# 로그 확인
docker compose logs -f caddy
# 중지
docker compose down
9-5. systemd vs Docker, 어떤 방식을 선택할까?
systemd 서비스 방식은 서버에 직접 Caddy를 설치하여 운영하는 전통적인 방식입니다. 서버에 이미 다양한 서비스가 systemd로 관리되고 있거나, 백엔드 앱을 Docker 없이 직접 실행하는 환경에 적합합니다.
Docker 방식은 Caddy와 백엔드 앱을 모두 컨테이너로 통일하여 관리합니다. docker compose up -d 한 번으로 전체 스택을 띄울 수 있어 재현성이 높고, 서버 이전도 간편합니다. 이 시리즈에서 PostgreSQL, MinIO, n8n을 이미 Docker로 운영하고 있으므로, Caddy도 Docker로 통합하면 14편의 통합 Docker Compose와 자연스럽게 연결됩니다.
두 방식 모두 프로덕션에서 문제없이 사용할 수 있으니, 프로젝트 상황에 맞게 선택하면 됩니다.
10. 프로덕션 Caddyfile 전체 예시
지금까지의 내용을 종합한 프로덕션 Caddyfile입니다.
# /etc/caddy/Caddyfile
{
email admin@example.com
}
# ─── React SPA ───
myapp.example.com {
root * /var/www/myapp
encode gzip zstd
try_files {path} /index.html
file_server
# 해시가 포함된 정적 자산: 장기 캐시
@static path *.js *.css *.png *.jpg *.jpeg *.gif *.svg *.woff *.woff2 *.ico
header @static Cache-Control "public, max-age=31536000, immutable"
# HTML: 캐시 없음
@html path /index.html
header @html Cache-Control "no-cache, no-store, must-revalidate"
# 보안 헤더
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
log {
output file /var/log/caddy/myapp-access.log {
roll_size 100mb
roll_keep 5
}
}
}
# ─── Go Gin API ───
go-api.example.com {
reverse_proxy localhost:8080
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
log {
output file /var/log/caddy/go-api-access.log {
roll_size 100mb
roll_keep 5
}
}
}
# ─── FastAPI ───
py-api.example.com {
reverse_proxy localhost:8000
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
}
log {
output file /var/log/caddy/py-api-access.log {
roll_size 100mb
roll_keep 5
}
}
}
# ─── n8n ───
n8n.example.com {
reverse_proxy localhost:5678
}
# ─── MinIO Console ───
minio.example.com {
reverse_proxy localhost:9001
}
# ─── MinIO S3 API ───
s3.example.com {
reverse_proxy localhost:9000
}
로그 설정에서 roll_size와 roll_keep은 로그 파일 로테이션을 의미합니다. 100MB에 도달하면 새 파일을 생성하고, 최대 5개 파일을 유지합니다. 디스크 용량 관리에 유용합니다.
11. 확인 체크리스트
Caddy 설정이 완료되었으면 아래 항목을 하나씩 확인합니다.
# 1. Caddy 버전 확인
caddy version
# 2. Caddyfile 문법 검증
caddy validate --config /etc/caddy/Caddyfile
# 3. Caddy 서비스 상태 확인
sudo systemctl status caddy
# 4. 80, 443 포트 리스닝 확인
sudo ss -tlnp | grep -E ':80|:443'
# 5. 방화벽 규칙 확인
sudo ufw status
# 6. HTTPS 인증서 확인 (도메인이 연결된 경우)
curl -I https://myapp.example.com
# HTTP/2 200 을 확인
# 7. SPA 폴백 테스트 (존재하지 않는 경로)
curl -s -o /dev/null -w "%{http_code}" https://myapp.example.com/any/random/path
# 200 이 출력되면 정상
# 8. 리버스 프록시 테스트
curl -I https://go-api.example.com/health
curl -I https://py-api.example.com/health
# 9. 로그 파일 생성 확인
ls -la /var/log/caddy/
체크리스트 요약
- Caddy 설치 완료 & 버전 확인
- Caddyfile 문법 검증 통과
- systemd 서비스 활성화 & 정상 동작
- 방화벽에서 80/443 포트 개방
- DNS A 레코드가 서버 IP를 가리킴
- HTTPS 자동 인증서 발급 확인
- React SPA 정적 파일 서빙 정상 동작
- SPA 폴백(try_files) 정상 동작
- Go Gin 리버스 프록시 정상 동작
- FastAPI 리버스 프록시 정상 동작
- 로그 파일 정상 기록
다음 편 예고
11편에서 클라우드 리눅스 서버에 Caddy를 구성하여 프론트엔드 서빙과 백엔드 리버스 프록시, 자동 HTTPS까지 설정했습니다.
12편: Claude Code 설치 & 활용에서는 다시 윈도우 로컬 환경으로 돌아와서, AI 코딩 어시스턴트인 Claude Code를 설치하고 프로젝트에 연동합니다. 터미널에서 자연어로 코드를 생성하고, 리팩토링하고, 디버깅하는 워크플로우를 다룹니다.
'Windows 개발환경 세팅' 카테고리의 다른 글
| [윈도우 개발 환경 설정] 13편: 클라우드 서버 비교 & 선택 가이드 (1) | 2026.03.31 |
|---|---|
| [윈도우 개발 환경 설정] 12편: Claude Code 설치 & 활용 (0) | 2026.03.31 |
| [윈도우 개발 환경 설정] 10편: n8n 워크플로우 자동화 설정 (0) | 2026.03.31 |
| [윈도우 개발 환경 설정] 9편: MinIO 컨테이너 설정 (0) | 2026.03.31 |
| [윈도우 개발 환경 설정] 8편: PostgreSQL 컨테이너 설정 (0) | 2026.03.31 |