Caddy와 싸운 하루: 리다이렉트 루프 3연전

시리즈: AI 팀과 함께 기술블로그 만들기 | Episode 3


AI 에이전트 3명이 설계해준 아키텍처를 실제로 구현하기 시작했습니다. docker-compose.yml을 다시 쓰고, 보안 헤더를 추가하고, Redis를 연결하고. 대부분은 순조로웠습니다.

Caddy를 건드리기 전까지는요.

이 글은 Caddy Docker Proxy에서 발생한 3가지 장애와 디버깅 과정을 기록합니다. 모두 같은 날, 같은 서비스에서, 연쇄적으로 터졌습니다.

아키텍처 배경

먼저 우리 블로그의 요청 흐름을 이해해야 합니다.

[사용자] → [Cloudflare CDN/Proxy] → [Caddy Reverse Proxy] → [WordPress]
              (HTTPS)                    (HTTP)                 (HTTP)

핵심: Cloudflare가 SSL을 처리합니다. 사용자와 Cloudflare 사이는 HTTPS, Cloudflare에서 서버까지는 HTTP. Caddy는 리버스 프록시 역할만 합니다.

Caddy 설정은 docker-compose.yml의 labels로 관리합니다. Caddy Docker Proxy가 이 labels를 읽어서 자동으로 Caddyfile을 생성하는 구조입니다.


장애 1: 308 무한 리다이렉트

증상

https://yndl.dev 접속 시 “이 페이지는 작동하지 않습니다. ERR_TOO_MANY_REDIRECTS”. curl로 확인하면 308 Permanent Redirect가 무한 반복됩니다.

원인

AI 에이전트 2명이 동시에 “Caddy 레이블에서 http://를 제거하라”고 권장했습니다. auto-HTTPS를 활성화해야 한다면서요.

# AI 권장대로 변경
labels:
  caddy: "yndl.dev"  # http:// 제거 → auto-HTTPS 활성화

이 변경이 트리거한 연쇄 반응:

  1. Caddy가 auto-HTTPS 활성화
  2. Let’s Encrypt에 TLS 인증서 발급 요청
  3. HTTP-01 챌린지 시도
  4. DNS가 Cloudflare Proxy를 가리키고 있어서 챌린지 실패
  5. Caddy가 그래도 HTTPS로 리다이렉트 시도
  6. Cloudflare도 HTTPS로 리다이렉트
  7. 무한 루프: Cloudflare(308) → Caddy(308) → Cloudflare(308) → …

해결

labels:
  caddy: "http://yndl.dev"  # http:// 명시 → auto-HTTPS 비활성화

원칙: Cloudflare Proxy 뒤에서는 Caddy의 auto-HTTPS를 끄세요. SSL은 Cloudflare가 처리합니다.


장애 2: wp-login.php 무한 리다이렉트

증상

장애 1을 고친 후, 메인 페이지는 잘 뜹니다. 하지만 https://yndl.dev/wp-login.php에 접속하면 다시 “너무 많은 재이동” 에러가 발생합니다.

원인

WordPress wp-config.php에 FORCE_SSL_ADMIN = true가 설정되어 있습니다. 관리자 페이지는 반드시 HTTPS로 접속해야 한다는 뜻이죠.

그런데 요청 흐름을 보면:

[사용자] --HTTPS--> [Cloudflare] --HTTP--> [Caddy] --HTTP--> [WordPress]

WordPress: "HTTP네? HTTPS로 가!"
→ 브라우저: HTTPS로 재요청
→ Cloudflare → Caddy → WordPress: "또 HTTP네?"
→ 무한 반복

WordPress가 받는 요청은 항상 HTTP입니다. Caddy가 HTTP로 전달하니까요. WordPress는 매번 “HTTPS가 아니네?”라고 판단해서 리다이렉트합니다.

해결

Caddy가 WordPress에 “원래 이 요청은 HTTPS였어”라고 알려줘야 합니다.

labels:
  caddy.reverse_proxy.header_up: "X-Forwarded-Proto https"

WordPress의 wp-config.php가 이 헤더를 감지합니다:

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
    && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

원칙: 리버스 프록시 뒤의 WordPress에는 반드시 X-Forwarded-Proto를 전달하세요. Caddy, Nginx, Apache 모두 해당됩니다.


장애 3: 사이트가 증발했다

증상

장애 1, 2를 모두 고치고 보안 헤더와 xmlrpc.php 차단을 추가합니다. 사이트에 접속하면… 빈 페이지. HTTP 200 OK인데 content-length: 0. 아무것도 없습니다.

원인

Caddy 로그에서 범인을 찾았습니다:

$ docker logs caddy | grep ERROR
[ERROR] ... Removing invalid block for "yndl.dev"

“Removing invalid block” — Caddy가 yndl.dev 사이트 블록 자체를 제거해버렸습니다. 원인은 레이블 문법:

# 잘못된 문법
caddy.respond.@xmlrpc: "403"
# → Caddyfile: respond { @xmlrpc 403 }  ← 파싱 오류!

# 올바른 문법
caddy.respond: "@xmlrpc 403"
# → Caddyfile: respond @xmlrpc 403  ← 정상

dot notation에서 @xmlrpc를 키의 일부가 아니라 값의 일부로 넘겨야 합니다. Caddy Docker Proxy는 파싱에 실패하면 해당 사이트 블록 전체를 조용히 제거합니다.

원칙: Caddy Docker Proxy 레이블 변경 후에는 반드시 docker logs caddy | grep ERROR로 파싱 오류를 확인하세요. 오류가 있으면 사이트가 통째로 사라집니다.


최종 동작하는 설정

3번의 장애를 거쳐 완성된 Caddy 레이블:

labels:
  caddy: "http://yndl.dev"
  caddy.reverse_proxy: "{{upstreams 80}}"
  caddy.reverse_proxy.header_up: "X-Forwarded-Proto https"
  caddy.header.X-Content-Type-Options: "nosniff"
  caddy.header.X-Frame-Options: "SAMEORIGIN"
  caddy.header.Referrer-Policy: "strict-origin-when-cross-origin"
  caddy.header.-Server: ""
  caddy.header.-X-Powered-By: ""
  [email protected]: "/xmlrpc.php"
  caddy.respond: "@xmlrpc 403"
레이블역할
caddy: "http://yndl.dev"HTTP 모드 (Cloudflare가 SSL 처리)
caddy.reverse_proxy.header_upHTTPS 원본 프로토콜 전달
caddy.header.-Server서버 정보 헤더 제거
[email protected]xmlrpc.php 매처 정의
caddy.respond: "@xmlrpc 403"xmlrpc.php에 403 응답

디버깅 치트시트

1단계: Caddy 로그 확인

docker logs caddy 2>&1 | grep -E "ERROR|WARN|invalid"

2단계: 응답 헤더 확인

curl -sI https://yndl.dev
# server: Caddy → Caddy가 응답 (WordPress까지 안 감)
# link: wp-json → 정상 프록시
# content-length: 0 → 사이트 블록 제거됨

3단계: 프록시 체인 단계별 테스트

# WordPress 직접 테스트
docker exec wordpress curl -sI http://localhost:80/

# Caddy 설정 확인
curl -s localhost:2019/config/ | python3 -m json.tool

세 장애의 공통 패턴

세 장애 모두 “프록시 체인에서 각 노드가 서로의 존재를 모른다”는 같은 근본 원인을 가지고 있습니다.

장애모르는 관계결과
308 루프Caddy가 Cloudflare를 모름이중 SSL 시도
로그인 루프WordPress가 Cloudflare를 모름HTTP로 착각
사이트 증발Caddy가 문법 오류를 조용히 처리블록 삭제

리버스 프록시 체인을 설정할 때는 항상 “각 노드가 앞/뒤 노드에 대해 알아야 할 것이 무엇인가?”를 질문해야 합니다.

  • WordPress는 X-Forwarded-Proto로 원본 프로토콜을 알아야 합니다.
  • Caddy는 http:// 접두사로 SSL을 자기가 처리하지 않아야 함을 알아야 합니다.
  • 운영자는 Caddy 로그로 설정 파싱 상태를 알아야 합니다.

이 경험이 AI 보고서에 없었던 이유

Episode 2에서 AI 에이전트의 채택률이 86%였다고 했습니다. 그 14%의 미채택 중 하나가 바로 Caddy HTTPS 설정이었습니다. AI는 Caddy 공식 문서의 Best Practice를 정확히 알고 있었지만, Cloudflare → Caddy → WordPress라는 실제 인프라 체인을 모른 채 권장했습니다.

장애 2(X-Forwarded-Proto)와 장애 3(respond 레이블 문법)은 AI 보고서에 아예 언급되지 않았습니다. 이런 종류의 문제는 실제로 부딪혀봐야 알 수 있습니다. 문서에 적혀있지 않고, 에러 메시지도 직관적이지 않습니다.

AI의 설계도는 출발점으로서 훌륭합니다. 하지만 프로덕션 환경의 디버깅은 여전히 사람의 영역입니다.


다음 에피소드 예고

Episode 4 — Docker + WordPress + Redis: AI가 설계한 인프라 적용기


시리즈 안내: AI 팀과 함께 기술블로그 만들기

회차제목상태
1Agent Teams란? 3인 AI 팀 구성기발행
2AI 3인이 내 블로그를 진단했다발행
3Caddy와 싸운 하루: 리다이렉트 루프 3연전현재 글
4Docker + WordPress + Redis: AI가 설계한 인프라 적용기발행
5기술블로그 UX: 코드가 읽히는 블로그발행
6AI 코드 리뷰어가 찾아낸 22개 이슈발행
7마크다운으로 글 쓰고 자동 발행하기예정
8AI 팀 vs 혼자 코딩: 비용 대비 효과 분석예정

이 글은 Claude Code를 사용하여 작성되었습니다.
프로젝트: yndl.dev

댓글 남기기