Caddy 실전 설정 실수 모음: Reverse Proxy·Rewrite·Docker·HTTPS에서 자주 틀리는 것들

1장 Caddy는 설정이 쉬운데 왜 실제 운영에서는 자꾸 막힐까#

Caddy의 가장 단순한 설정은 매우 쉽습니다.

example.com {
    reverse_proxy app:8080
}

이 정도만 보면 Caddy는 어렵지 않아 보입니다.

실제로도 기본 Reverse Proxy와 HTTPS 구성은 상당히 단순합니다.

문제는 서비스가 조금씩 복잡해질 때 시작됩니다.

예를 들어 다음 요구사항이 추가될 수 있습니다.

루트 URL은 애플리케이션 내부 특정 경로로 연결

API는 별도 컨테이너로 전달

관리자 서비스는 다른 서브도메인 사용

WebSocket 서비스 연결

컨테이너 여러 개 운영

HTTPS 자동 적용

장애 시 다른 Backend로 Failover

이때부터 다음 개념이 동시에 등장합니다.

flowchart LR
    C[Caddy]

    C --> RP[reverse_proxy]
    C --> RW[rewrite]
    C --> RD[redir]
    C --> H[handle]
    C --> HP[handle_path]
    C --> TLS[TLS]
    C --> LB[Load Balancing]
    C --> DK[Docker Network]

Caddy 자체가 어려워진다기보다 비슷해 보이는 기능의 역할을 정확히 구분해야 하는 단계가 되는 것입니다.


2장 가장 먼저 이해해야 할 것은 요청 흐름이다#

Caddy 설정을 작성하기 전에 다음 구조부터 생각하는 것이 좋습니다.

flowchart LR
    U[사용자]
    C[Caddy]
    A[Application]

    U -->|HTTPS 요청| C
    C -->|내부 요청| A

사용자가 보는 주소와 Caddy가 Backend에 전달하는 주소는 다를 수 있습니다.

예를 들어 사용자는:

https://example.com/

으로 접속하지만 Caddy가 Backend에는:

/share

를 전달할 수도 있습니다.

이 차이를 이해해야 rewrite, redir, handle_path를 올바르게 사용할 수 있습니다.


3장 실수 1: rewrite와 redir를 같은 기능으로 생각한다#

가장 흔한 실수 중 하나입니다.

애플리케이션의 실제 페이지가:

/share

에 있다고 하겠습니다.

사용자는:

https://example.com/

으로 접속하고 주소창은 그대로 유지하고 싶습니다.

이때 필요한 것은 rewrite입니다.

example.com {
    @root path /
    rewrite @root /share

    reverse_proxy app:8080
}

흐름은 다음과 같습니다.

sequenceDiagram
    participant U as 사용자
    participant C as Caddy
    participant A as Application

    U->>C: GET /
    C->>C: / 를 /share로 내부 변경
    C->>A: GET /share
    A-->>C: Response
    C-->>U: Response

사용자 브라우저는 여전히:

https://example.com/

을 표시합니다.


4장 redir를 사용하면 브라우저 주소가 바뀐다#

다음 설정은 결과가 다릅니다.

example.com {
    redir / /share 302

    reverse_proxy app:8080
}

이 경우:

sequenceDiagram
    participant U as Browser
    participant C as Caddy

    U->>C: GET /
    C-->>U: 302 /share
    U->>C: GET /share

브라우저 주소도:

https://example.com/share

로 변경됩니다.

따라서 핵심은 다음과 같습니다.

기능 브라우저 주소 목적
rewrite 유지 내부 요청 URI 변경
redir 변경 클라이언트를 다른 URL로 이동

5장 실수 2: rewrite 후 모든 요청까지 바꿔버린다#

루트 요청 하나만 /share로 바꾸고 싶은데 다음처럼 생각할 수 있습니다.

rewrite /share

하지만 조건을 명확하게 하지 않으면 의도하지 않은 요청에도 영향을 줄 수 있습니다.

따라서 특정 요청만 처리하려면 matcher를 사용하는 편이 명확합니다.

@root path /
rewrite @root /share

이 의미는 정확합니다.

요청 경로가 /
일 때만
/share 로 변경

6장 실수 3: handle과 handle_path를 혼동한다#

둘의 이름이 매우 비슷합니다.

하지만 동작에는 중요한 차이가 있습니다.

handle#

경로를 그대로 Backend에 전달합니다.

example.com {
    handle /api/* {
        reverse_proxy api:3000
    }
}

사용자가:

/api/users

를 요청하면 Backend도:

/api/users

를 받습니다.


7장 handle_path는 Prefix를 제거한다#

다음 설정을 보겠습니다.

example.com {
    handle_path /api/* {
        reverse_proxy api:3000
    }
}

사용자 요청:

/api/users

Backend 요청:

/users

입니다.

flowchart LR
    U["Client<br/>/api/users"]
    C["Caddy<br/>handle_path /api/*"]
    A["API Server<br/>/users"]

    U --> C --> A

따라서 애플리케이션이 /api 경로를 포함하도록 만들어졌다면 handle_path를 사용하면 오히려 장애가 발생할 수 있습니다.


8장 실제로 자주 생기는 Subpath 문제#

애플리케이션이 내부적으로 다음 주소를 사용한다고 하겠습니다.

/share/page1

그런데:

handle_path /share/* {
    reverse_proxy app:8080
}

를 사용하면 Backend에는:

/page1

이 전달됩니다.

애플리케이션이 /share를 기준 경로로 기대한다면 페이지가 깨질 수 있습니다.

이때는:

handle /share/* {
    reverse_proxy app:8080
}

처럼 경로를 그대로 유지해야 합니다.


9장 실수 4: 모든 요청을 하나의 reverse_proxy에 넣고 해결하려 한다#

처음에는 다음 설정으로 충분합니다.

example.com {
    reverse_proxy app:8080
}

하지만 서비스가 추가되면 모든 처리를 하나의 블록에 우겨넣는 것보다 요청을 명확하게 나누는 편이 좋습니다.

예:

example.com {

    handle /api/* {
        reverse_proxy api:3000
    }

    handle /admin/* {
        reverse_proxy admin:9000
    }

    handle {
        reverse_proxy app:8080
    }
}

구조는 다음과 같습니다.

flowchart TB
    R[Request]
    C{Caddy}

    A[Main App]
    API[API]
    ADM[Admin]

    R --> C

    C -->|/api/*| API
    C -->|/admin/*| ADM
    C -->|그 외| A

matcher가 없는 마지막 handle은 fallback 역할을 합니다.


10장 실수 5: localhost의 의미를 잘못 이해한다#

Docker 환경에서 가장 자주 발생하는 문제입니다.

다음 설정이 있다고 하겠습니다.

reverse_proxy localhost:8080

Caddy가 Host OS에서 직접 실행된다면 localhost는 Host입니다.

하지만 Caddy가 Docker 컨테이너 안에서 실행된다면:

localhost
=
Caddy 컨테이너 자신

입니다.

다른 컨테이너를 의미하지 않습니다.

flowchart TB
    subgraph Docker
        C[Caddy Container<br/>localhost]
        A[Application Container]
    end

    C -. "localhost:8080은<br/>Application이 아님" .-> A

11장 Docker에서는 서비스 이름을 사용하는 것이 일반적이다#

Docker Compose에:

services:
  caddy:
  app:

이 있고 같은 Docker Network에 있다면:

reverse_proxy app:8080

처럼 사용할 수 있습니다.

flowchart LR
    C[Caddy]
    D[Docker DNS]
    A[app:8080]

    C -->|app| D
    D --> A

Docker 내부 DNS가 app이라는 서비스 이름을 컨테이너 IP로 변환합니다.


12장 실수 6: 외부 포트와 내부 포트를 혼동한다#

Docker Compose에 다음과 같은 설정이 있다고 하겠습니다.

ports:
  - "9000:8080"

의미는:

Host 9000
→
Container 8080

입니다.

Caddy가 같은 Docker Network 안에 있다면 일반적으로 Host 9000이 아니라 컨테이너 내부 포트 8080을 사용합니다.

reverse_proxy app:8080

구조를 보면 이해하기 쉽습니다.

flowchart LR
    INTERNET[Internet]
    HOST["Host :9000"]
    APP["App Container :8080"]
    CADDY[Caddy Container]

    INTERNET --> HOST --> APP
    CADDY -->|Docker Network :8080| APP

Caddy와 App이 Docker 내부에서 직접 통신한다면 굳이 Host Port Mapping을 거칠 필요가 없습니다.


13장 실수 7: 컨테이너 이름만 맞으면 통신될 것이라고 생각한다#

다음 두 컨테이너가 있다고 하겠습니다.

caddy
app

이름이 정확하다고 항상 통신되는 것은 아닙니다.

두 컨테이너가 통신 가능한 Docker Network에 있어야 합니다.

정상:

flowchart TB
    subgraph app_network
        C[Caddy]
        A[Application]
    end

    C --> A

문제가 되는 구조:

flowchart TB
    subgraph network_a
        C[Caddy]
    end

    subgraph network_b
        A[Application]
    end

    C -. "직접 통신 불가" .-> A

따라서 Reverse Proxy 장애에서는 Docker Network를 반드시 확인해야 합니다.


14장 Docker Compose에서 네트워크를 명시적으로 구성하는 방법#

예:

services:

  caddy:
    image: caddy:latest
    networks:
      - app_network

  app:
    image: example-app
    networks:
      - app_network

networks:
  app_network:

이 구조라면:

reverse_proxy app:8080

으로 접근할 수 있습니다.


15장 실수 8: Host 파일 경로를 Container에서도 그대로 사용한다#

Caddy가 Docker 컨테이너에서 실행된다면 Host의 파일 경로를 자동으로 볼 수 없습니다.

Host:

/opt/web/data

가 있다고 하겠습니다.

Caddyfile에서 그대로:

root /opt/web/data

를 적어도 해당 경로가 Caddy Container 안에 없다면 파일을 읽을 수 없습니다.


16장 Volume Mount를 이해해야 한다#

Docker Compose에서:

volumes:
  - ./web-data:/srv/web-data:ro

라고 설정하면:

flowchart LR
    H["Host<br/>./web-data"]
    C["Caddy Container<br/>/srv/web-data"]

    H -->|Volume Mount| C

Caddyfile에서는 Host 경로가 아니라 컨테이너 내부 경로를 사용해야 합니다.

root * /srv/web-data
file_server

17장 실수 9: root를 설정하면 파일이 자동으로 제공된다고 생각한다#

예:

example.com {
    root * /srv/site
}

root는 파일 기준 위치를 지정합니다.

실제로 파일을 HTTP로 제공하려면:

file_server

가 필요합니다.

정상적인 기본 구조:

example.com {
    root * /srv/site
    file_server
}

역할을 나누면:

flowchart LR
    R[root]
    F[file_server]
    FILE[Static File]

    R -->|파일 위치 결정| F
    F -->|HTTP 응답| FILE

18장 실수 10: root에 파일 이름까지 넣는다#

다음처럼 생각하기 쉽습니다.

root * /srv/site/index.html

하지만 root는 일반적으로 파일 자체가 아니라 기준 디렉터리입니다.

root * /srv/site
file_server

사용자가 /index.html을 요청하면 Caddy가:

/srv/site/index.html

을 찾는 방식입니다.


19장 실수 11: directive가 작성한 순서대로 항상 실행된다고 생각한다#

Caddyfile은 단순한 Shell Script처럼 위에서 아래로 무조건 실행되는 구조가 아닙니다.

Caddyfile Adapter에는 directive 기본 정렬 순서가 있습니다.

일반적인 설정에서는 오히려 이 기능이 편리합니다.

하지만 처리 순서를 정확하게 직접 제어해야 한다면 route를 고려할 수 있습니다.


20장 route는 순서를 고정하고 싶을 때 사용한다#

예:

example.com {

    route {

        reverse_proxy /api/* api:3000

        try_files {path} /index.html

        file_server
    }
}

route 안에서는 작성한 순서가 중요합니다.

따라서 역할을 구분하면:

handle
= 요청을 기능별로 분기

handle_path
= Prefix 제거 후 분기

route
= 처리 순서를 직접 제어

라고 이해할 수 있습니다.


21장 실수 12: Caddy가 실행되면 Backend도 정상이라고 생각한다#

Caddy 자체가 정상이어도 Backend 연결은 실패할 수 있습니다.

전체 요청 경로는 다음처럼 여러 구간으로 나뉩니다.

flowchart LR
    U[사용자]
    DNS[DNS]
    FW[Firewall]
    C[Caddy]
    DN[Docker Network]
    A[Application]

    U --> DNS --> FW --> C --> DN --> A

사이트가 열리지 않는다고 무조건 Caddyfile만 수정하면 문제 해결이 더 늦어질 수 있습니다.


22장 장애를 구간별로 나누어 확인한다#

1단계#

DNS가 올바른 서버를 가리키는가

2단계#

80·443 포트가 접근 가능한가

3단계#

Caddy 프로세스 또는 컨테이너가 정상인가

4단계#

Caddyfile이 정상인가

5단계#

Caddy에서 Backend 이름을 해석할 수 있는가

6단계#

Docker Network 연결이 정상인가

7단계#

Backend가 실제 내부 포트에서 Listen하고 있는가

이 순서대로 확인하면 장애 범위를 빠르게 줄일 수 있습니다.


23장 실수 13: HTTPS 문제와 Backend 문제를 섞어서 본다#

외부 HTTPS와 내부 Backend 연결은 별개입니다.

가장 일반적인 구조는 다음과 같습니다.

flowchart LR
    U[사용자]
    C[Caddy<br/>TLS Termination]
    A[Application]

    U -->|HTTPS :443| C
    C -->|HTTP :8080| A

HTTPS가 정상이라고 해서 Backend 연결도 정상인 것은 아닙니다.

반대로 Backend가 정상이라고 해서 외부 TLS 설정까지 정상이라는 의미도 아닙니다.


24장 TLS Termination 위치를 이해해야 한다#

Caddy를 앞단에 두면 일반적으로 Caddy가 외부 HTTPS를 처리합니다.

Client
→ HTTPS
→ Caddy
→ HTTP
→ Backend

내부망에서도 암호화가 필요하다면:

Client
→ HTTPS
→ Caddy
→ HTTPS
→ Backend

구조도 가능합니다.


25장 실수 14: Backend HTTPS에서 인증서 검증 문제를 무조건 끈다#

Backend가 자체 인증서를 사용하는 경우 인증서 검증 오류가 발생할 수 있습니다.

이때 가장 쉬운 해결처럼 보이는 것이 TLS 검증을 끄는 것입니다.

하지만 운영 환경에서는 가능한 한 신뢰할 수 있는 CA와 인증서 구조를 만드는 편이 좋습니다.

단순히:

연결되니까 됐다

보다:

어떤 인증서를 누구를 기준으로 신뢰하는가

를 확인해야 합니다.


26장 실수 15: HTTP와 WebSocket을 별도 서버처럼 생각한다#

WebSocket 서비스를 운영한다고 Caddy에 완전히 별도의 프록시 구조가 필요한 것은 아닙니다.

일반적인 WebSocket은 HTTP Upgrade에서 시작합니다.

sequenceDiagram
    participant B as Browser
    participant C as Caddy
    participant W as WebSocket Server

    B->>C: HTTP Upgrade
    C->>W: Upgrade 전달
    W-->>C: Switching Protocols
    C-->>B: WebSocket 연결

일반적인 경우 reverse_proxy로 WebSocket을 프록시할 수 있습니다.


27장 실수 16: 모든 Proxy Header를 직접 넣으려고 한다#

Nginx 설정 경험이 있다면 다음 Header들을 수동으로 추가하려는 습관이 생길 수 있습니다.

X-Forwarded-For
X-Forwarded-Proto
Host

하지만 Caddy reverse_proxy는 일반적인 Proxy Header 처리를 기본적으로 수행합니다.

필요한 이유가 명확하지 않다면 모든 Header를 무조건 덮어쓰는 방식은 피하는 것이 좋습니다.


28장 Header를 직접 바꿔야 하는 경우#

특정 Backend가 별도 Host Header를 요구한다면:

reverse_proxy backend:8080 {
    header_up Host backend.internal
}

같은 구성이 필요할 수 있습니다.

중요한 것은:

왜 Header를 바꾸는지 알고 설정하는 것

입니다.


29장 실수 17: 모든 서비스 포트를 외부에 공개한다#

Docker 서비스가 다음과 같다고 하겠습니다.

app     8080
api     3000
admin   9000
grafana 3000

각 포트를 모두 Host에 공개할 필요는 없습니다.

Caddy가 외부 진입점이라면:

flowchart TB
    INTERNET[Internet]
    C[Caddy<br/>80 · 443]

    A[App :8080]
    API[API :3000]
    ADM[Admin :9000]
    G[Grafana :3000]

    INTERNET --> C

    C --> A
    C --> API
    C --> ADM
    C --> G

처럼 내부 Network로 접근할 수 있습니다.

외부에는 주로 Caddy의 80·443만 노출하는 구조가 더 단순합니다.


30장 실수 18: 관리자 서비스도 일반 서비스처럼 공개한다#

예:

admin.example.com {
    reverse_proxy admin:9000
}

이 자체로 동작은 합니다.

하지만 관리자 서비스라면 추가적인 접근 통제를 고려해야 합니다.

예:

admin.example.com {

    basic_auth {
        admin HASHED_PASSWORD
    }

    reverse_proxy admin:9000
}

또는 VPN·IP 제한·SSO 같은 별도 보호 구조를 사용할 수 있습니다.


31장 Basic Auth에서 평문 비밀번호를 넣으면 안 된다#

다음은 피해야 합니다.

basic_auth {
    admin password1234
}

Caddy Basic Auth는 Hash된 비밀번호를 사용합니다.

대표적으로:

caddy hash-password

를 이용해 Hash를 생성한 뒤 사용합니다.


32장 실수 19: 설정을 수정할 때 전체 Docker Stack을 내린다#

Caddyfile만 수정했는데 다음 작업을 매번 수행할 필요는 없는 경우가 많습니다.

docker compose down
docker compose up -d

애플리케이션까지 함께 내려갔다가 올라오기 때문에 불필요한 서비스 중단이 발생할 수 있습니다.


33장 Caddy 설정은 Reload가 핵심이다#

운영 환경에서는 다음 흐름이 좋습니다.

flowchart LR
    A[Caddyfile 수정]
    B[Format]
    C[Validate]
    D{정상?}
    E[Reload]
    F[수정]

    A --> B --> C --> D
    D -->|Yes| E
    D -->|No| F
    F --> A

대표 명령:

caddy fmt --overwrite /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
caddy reload --config /etc/caddy/Caddyfile

34장 Docker에서도 Reload를 생각해야 한다#

Caddy 컨테이너를 통째로 재시작할 수도 있습니다.

하지만 설정만 변경했다면 가능하면 Caddy Reload를 이용하는 편이 연결 유지와 운영 측면에서 유리할 수 있습니다.

핵심은:

Configuration 변경
≠
항상 Process Restart

입니다.


35장 실수 20: Validate 없이 바로 Reload한다#

사소한 오타 하나만 있어도 설정 적용이 실패할 수 있습니다.

예:

revers_proxy

정상:

reverse_proxy

사람이 눈으로 보는 것보다:

caddy validate

로 확인하는 것이 훨씬 안전합니다.


36장 Format도 생각보다 중요하다#

Caddyfile이 길어지면 들여쓰기와 블록 구조가 복잡해집니다.

caddy fmt --overwrite Caddyfile

을 사용하면 형식을 정리할 수 있습니다.

특히 여러 handle, matcher, reverse proxy 설정을 사용할 때 가독성이 크게 좋아집니다.


37장 실수 21: 로그 없이 감으로 장애를 추측한다#

다음과 같은 상황을 생각해보겠습니다.

502 Bad Gateway

이 에러가 나오면 무작정 Caddy 설정을 계속 바꾸는 것보다 로그를 먼저 보는 것이 좋습니다.

502는 흔히:

Caddy
→ Backend 연결 실패

계열의 문제일 수 있습니다.


38장 Docker에서는 먼저 Caddy 로그를 확인한다#

예:

docker logs caddy

실시간으로:

docker logs -f caddy

를 사용할 수 있습니다.

Host systemd 방식이라면:

journalctl -u caddy -f

같은 방법을 사용할 수 있습니다.


39장 502 Bad Gateway가 발생했을 때 확인할 것#

flowchart TB
    E[502 Bad Gateway]

    E --> A[Backend 실행 중?]
    E --> B[Port 맞음?]
    E --> C[Docker Network?]
    E --> D[Container Name?]
    E --> F[HTTP / HTTPS Protocol?]

예를 들어 Backend가 HTTP인데:

reverse_proxy https://app:8080

라고 설정했다면 연결에 실패할 수 있습니다.

반대로 Backend가 HTTPS인데 HTTP로 접근하는 경우도 문제입니다.


40장 실수 22: 404와 502를 같은 문제로 본다#

둘은 성격이 다릅니다.

404#

요청한 리소스 또는 경로를 찾지 못한 경우

502#

Caddy가 Backend와 정상적으로 통신하지 못한 경우에 흔히 발생

따라서:

404
→ Routing / Application Path

502
→ Backend Connection

방향으로 먼저 생각하면 문제를 좁히기 쉽습니다.


41장 실수 23: Caddy 자체 장애와 Backend 장애를 구분하지 않는다#

다음과 같이 확인할 수 있습니다.

flowchart LR
    U[User]
    C[Caddy]
    A[Application]

    U --> C --> A

Caddy 자체 접근 실패#

Caddy·Firewall·DNS·TLS 확인

Caddy는 응답하지만 502#

Backend 연결 확인

Backend는 연결되지만 404#

경로·Rewrite·애플리케이션 확인

이처럼 계층별로 구분하는 것이 중요합니다.


42장 실수 24: Load Balancing 설정만 하면 고가용성이 된다고 생각한다#

다음 설정을 만들 수 있습니다.

example.com {
    reverse_proxy app1:8080 app2:8080
}

그러면 여러 Backend에 요청을 분배할 수 있습니다.

flowchart LR
    U[User]
    C[Caddy]

    A1[App 1]
    A2[App 2]

    U --> C
    C --> A1
    C --> A2

하지만 이것만으로 완전한 고가용성 구조가 되는 것은 아닙니다.


43장 Health Check가 함께 필요하다#

애플리케이션 프로세스가 죽었는데 계속 요청을 보낸다면 Load Balancing의 의미가 줄어듭니다.

예:

example.com {

    reverse_proxy app1:8080 app2:8080 {

        health_uri /health
        health_interval 10s
        health_timeout 2s
    }
}

구조:

sequenceDiagram
    participant C as Caddy
    participant A1 as App1
    participant A2 as App2

    C->>A1: GET /health
    A1-->>C: 200

    C->>A2: GET /health
    A2-->>C: 오류

    Note over C,A2: App2를 비정상으로 판단

44장 실수 25: Health Check URL이 실제 업무 의존성을 확인하지 못한다#

다음 Endpoint가:

/health

무조건:

200 OK

를 반환한다고 하겠습니다.

애플리케이션 프로세스는 살아 있지만 DB 연결이 끊겨 있어도 Health Check가 정상으로 보일 수 있습니다.

따라서 Health Check를 설계할 때:

Process 상태
DB 연결
필수 서비스 연결

중 어느 범위까지 확인할 것인지 결정해야 합니다.

이 부분은 Caddy가 아니라 애플리케이션 측 설계와 연결됩니다.


45장 실수 26: Failover와 Load Balancing을 혼동한다#

Load Balancing#

여러 서버가 동시에 요청을 처리

flowchart LR
    C[Caddy]
    A[App A]
    B[App B]

    C --> A
    C --> B

Failover#

주 서버를 사용하다 장애가 나면 대기 서버 사용

flowchart LR
    C[Caddy]
    P[Primary]
    S[Standby]

    C -->|정상| P
    C -. 장애 .-> S

운영 목적이 다릅니다.


46장 Primary·Standby 구조 예#

예를 들어:

example.com {

    reverse_proxy primary:8080 standby:8080 {

        lb_policy first
        health_uri /health
    }
}

처럼 Primary 우선 구조를 구성할 수 있습니다.

단순히 Backend를 두 개 적는 것과는 목적이 다릅니다.


47장 실수 27: Caddy 하나가 죽으면 전체 서비스도 죽는다는 점을 잊는다#

Backend를 두 대로 이중화해도 Caddy가 한 대뿐이라면:

flowchart TB
    U[User]
    C[Caddy]
    A1[App 1]
    A2[App 2]

    U --> C
    C --> A1
    C --> A2

Caddy 자체가 SPOF가 될 수 있습니다.

따라서 매우 높은 가용성이 필요하다면 Caddy 자체의 이중화도 별도로 고려해야 합니다.


48장 한 대의 VPS에서는 현실적으로 어디까지 할까#

소규모 서비스라면 보통:

Internet
→ Caddy
→ 여러 Docker Application

구조만으로도 관리 효율이 매우 높습니다.

flowchart TB
    Internet[Internet]
    C[Caddy]

    T[Content App]
    API[API]
    ADM[Admin]
    M[Monitoring]

    Internet --> C

    C --> T
    C --> API
    C --> ADM
    C --> M

서비스 규모가 커질 때 Caddy 자체 이중화와 별도 L4 Load Balancer를 고민해도 늦지 않습니다.


49장 실수 28: Caddy의 L4와 L7을 혼동한다#

기본 Caddy의 대표적인 Reverse Proxy는 HTTP 기반 L7입니다.

Host
Path
Header
Method

같은 정보를 이용할 수 있습니다.

예:

flowchart TB
    U[HTTP Request]
    C{Caddy L7}

    A[Web]
    B[API]

    U --> C

    C -->|Host / Path| A
    C -->|Host / Path| B

50장 L4는 기본 reverse_proxy와 다른 영역이다#

TCP·UDP 같은 Layer 4 프록시가 필요하다면 caddy-l4 같은 별도 확장을 고려해야 합니다.

flowchart TB
    C[Caddy]

    L7[L7<br/>HTTP · HTTPS]
    L4[L4<br/>TCP · UDP]

    WEB[Web / API]
    DB[DB / 기타 TCP]

    C --> L7
    C -. 확장 .-> L4

    L7 --> WEB
    L4 --> DB

즉:

웹사이트
API
WebSocket

정도라면 대부분 기본 L7 기능으로 해결할 수 있습니다.


51장 실수 29: L4가 더 고급이니 무조건 사용하는 것이 좋다고 생각한다#

그렇지 않습니다.

HTTP 서비스를 처리하는데 굳이 L4로 내려가면 오히려 다음과 같은 L7 기능을 활용하기 어려워질 수 있습니다.

Host Routing
Path Routing
HTTP Header 처리
Redirect
Rewrite
HTTP Authentication

따라서 원칙은 단순합니다.

HTTP 서비스
→ L7

HTTP가 아닌 TCP·UDP
→ 필요한 경우 L4

입니다.


52장 실제 콘텐츠 서비스형 Caddy 구성 예제#

실제 운영 도메인이나 내부 이름을 노출하지 않은 예입니다.

요구사항:

메인 콘텐츠 앱

별도 API

관리자 서비스

모니터링 서비스

루트 URL 내부 Rewrite

설정:

example.com {

    @root path /
    rewrite @root /share

    reverse_proxy content-app:8080
}

api.example.com {
    reverse_proxy api:3000
}

admin.example.com {

    basic_auth {
        admin HASHED_PASSWORD
    }

    reverse_proxy admin:9000
}

monitor.example.com {
    reverse_proxy monitoring:3000
}

53장 전체 구조#

flowchart TB
    I[Internet]

    C[Caddy<br/>80 · 443]

    MAIN[Content App<br/>8080]
    API[API<br/>3000]
    ADM[Admin<br/>9000]
    MON[Monitoring<br/>3000]

    I --> C

    C -->|example.com| MAIN
    C -->|api.example.com| API
    C -->|admin.example.com| ADM
    C -->|monitor.example.com| MON

외부에서는 Caddy 하나만 보이고 내부 서비스 포트는 Docker Network 안에 둘 수 있습니다.


54장 실제 경로 Rewrite 예제#

사용자는:

https://example.com/

으로 접속합니다.

실제 Backend는:

/share

를 사용합니다.

sequenceDiagram
    participant U as User
    participant C as Caddy
    participant A as Content App

    U->>C: GET /
    C->>C: rewrite / → /share
    C->>A: GET /share
    A-->>C: Content
    C-->>U: Content

사용자는 내부 애플리케이션 구조를 알 필요가 없습니다.


55장 실전 장애 진단 예제: 사이트가 502다#

먼저 다음 순서로 봅니다.

flowchart TB
    E[502 발생]

    A[App Container 실행?]
    B[내부 Port 정확?]
    C[Docker Network 동일?]
    D[Service Name 정확?]
    F[HTTP/HTTPS 정확?]

    E --> A --> B --> C --> D --> F

예를 들어:

reverse_proxy app:8080

이라고 했는데 실제 애플리케이션은:

3000

에서 Listen하고 있다면 Caddy 문제가 아니라 포트 설정 문제입니다.


56장 실전 장애 진단 예제: 루트만 404다#

다른 페이지는 정상인데 /만 404라고 하겠습니다.

Backend의 실제 Root가 /share라면:

@root path /
rewrite @root /share

가 필요한지 확인할 수 있습니다.

이 경우 Reverse Proxy 연결 자체는 정상일 가능성이 높습니다.

즉:

전체 502
→ 연결 문제 가능성

특정 경로 404
→ Routing 문제 가능성

처럼 구분할 수 있습니다.


57장 실전 장애 진단 예제: Caddy는 App 이름을 못 찾는다#

로그에서 DNS resolution 관련 오류가 보인다면 Docker Network를 확인합니다.

caddy
app

두 컨테이너가 같은 Docker Network에 있는지 확인합니다.

서비스 이름을 정확하게 작성했는지도 봅니다.

reverse_proxy app:8080

에서 app은 Docker Compose의 실제 서비스 이름과 일치해야 합니다.


58장 실전 장애 진단 예제: 브라우저에서는 HTTPS인데 Backend Redirect가 이상하다#

Backend 애플리케이션이 자신을 HTTP 서비스라고 판단해 계속 이상한 Redirect를 만드는 경우가 있습니다.

이때 애플리케이션이 Reverse Proxy 환경에서 전달되는 scheme이나 proxy header를 제대로 신뢰하도록 설정되어 있는지 확인해야 합니다.

문제는 Caddy 자체가 아니라 Backend의 Reverse Proxy 인식 설정일 수도 있습니다.


59장 실전 장애 진단 예제: 경로가 두 번 붙는다#

사용자 요청:

/api/users

Caddy:

handle /api/* {
    reverse_proxy api:3000
}

Backend 자체도 /api를 자동 추가하는 구조라면 최종적으로:

/api/api/users

같은 문제가 생길 수 있습니다.

이 경우 Caddy와 Backend 중 누가 Prefix를 책임질지 명확히 정해야 합니다.


60장 Caddy 실전 운영에서 가장 중요한 사고방식#

Caddy 문제는 설정 구문만 보는 것보다 요청 흐름을 보는 것이 중요합니다.

항상 다음 네 단계를 그려보면 좋습니다.

flowchart LR
    A[사용자 URL]
    B[Caddy Routing]
    C[Docker Network]
    D[Backend URL]

    A --> B --> C --> D

예를 들어:

사용자
https://example.com/api/users

↓

Caddy
handle_path /api/*

↓

Docker
api:3000

↓

Backend
/users

이 흐름이 명확하다면 문제가 생겨도 어느 지점을 확인해야 하는지 바로 알 수 있습니다.


61장 자주 틀리는 설정을 한눈에 정리하면#

실수 실제 원인
/ 주소를 유지하려는데 redir 사용 rewrite 필요
Backend가 /api를 필요로 하는데 handle_path 사용 Prefix 제거됨
Docker에서 localhost로 다른 컨테이너 접근 localhost는 Caddy 자신
Host 공개 포트를 Backend 포트로 사용 Docker 내부 포트를 사용해야 할 수 있음
컨테이너 이름은 맞는데 연결 실패 Docker Network 확인
Host 경로를 Caddy에서 바로 사용 Volume Mount 필요
root만 설정 file_server 필요
설정마다 전체 Compose 재시작 Reload 검토
502를 경로 오류로 판단 Backend 연결부터 확인
404와 502를 같은 문제로 판단 Routing과 연결 장애 구분
Backend 두 대면 HA라고 판단 Health Check와 Caddy SPOF 고려
HTTP 서비스에 무조건 L4 사용 기본 L7이 더 적합할 수 있음

62장 실전 체크리스트#

Caddy 설정이 예상대로 동작하지 않는다면 다음 순서로 확인합니다.

  1. DNS와 80·443 접근이 정상인가
  2. Caddy 프로세스 또는 컨테이너가 살아 있는가
  3. Caddyfile Validate가 통과하는가
  4. rewrite와 redir를 혼동하지 않았는가
  5. handle과 handle_path를 올바르게 선택했는가
  6. Docker에서 localhost를 잘못 사용하지 않았는가
  7. Backend 서비스 이름이 정확한가
  8. Caddy와 Backend가 같은 Docker Network에 있는가
  9. 외부 포트와 내부 컨테이너 포트를 혼동하지 않았는가
  10. Backend의 HTTP·HTTPS 프로토콜이 맞는가
  11. 정적 파일이라면 root와 file_server가 모두 필요한가
  12. 502인지 404인지 먼저 구분했는가
  13. 로그를 확인했는가
  14. Load Balancing이라면 Health Check가 있는가
  15. Caddy 자체가 단일 장애점이 아닌지 고려했는가
  16. 설정 변경 후 Restart 대신 Reload가 가능한가

핵심 정리#

Caddy 실전 운영에서 가장 많이 발생하는 문제는 복잡한 기능이 아니라 서로 비슷한 개념을 혼동하는 것입니다.

가장 중요한 것만 압축하면 다음과 같습니다.

rewrite
= 내부 URL 변경

redir
= 브라우저 URL 이동
handle
= 경로 유지

handle_path
= Prefix 제거
Docker localhost
= Caddy 컨테이너 자신
Host Port
≠
Container 내부 Port
Container Name
+
같은 Docker Network
=
서비스 간 통신
root
= 정적 파일 기준 경로

file_server
= 실제 파일 제공

그리고 장애가 발생하면 무작정 Caddyfile부터 고치는 것이 아니라 다음 흐름을 확인해야 합니다.

flowchart LR
    U[사용자]
    DNS[DNS]
    C[Caddy]
    N[Docker Network]
    APP[Backend]

    U --> DNS --> C --> N --> APP

Caddy를 안정적으로 운영하는 가장 중요한 습관은 결국 하나입니다.

사용자의 요청이 Caddy에서 어떻게 변환되고, 어떤 네트워크를 거쳐, Backend의 어느 주소와 포트에 도착하는지를 끝까지 추적하는 것.

이 흐름만 정확히 이해하면 Reverse Proxy·Rewrite·Docker·HTTPS 문제 대부분을 훨씬 빠르게 해결할 수 있습니다.

이 페이지의 목차