NoctisNode
← 가이드셀프호스팅2026-10-239분

Caddy Docker 리버스 프록시: 자동 HTTPS를 갖춘 Compose 설정

Caddy를 Docker 리버스 프록시로 실행하세요. 하나의 compose 스택, 공유 네트워크, 서비스 이름으로의 reverse_proxy와 자동 HTTPS, 그리고 caddy-docker-proxy 레이블이 유용한 경우를 설명합니다.

짧은 답변: 80번과 443번 포트를 게시하는 별도 컨테이너에서 Caddy를 실행하고, Caddy와 앱 컨테이너를 하나의 공유 Docker 네트워크에 연결한 뒤, `reverse_proxy`를 각 서비스 이름과 내부 포트로 지정하세요(예: `reverse_proxy app:3000`). Caddy는 Docker 내장 DNS로 이름을 해석하고, 모든 사이트 주소에 대해 TLS 인증서를 자동으로 받습니다.

이 가이드는 먼저 일반 Caddyfile 방식을, 이어서 레이블 기반 caddy-docker-proxy 방식을 보여 주고, 단일 VPS에서 각각 언제 적합한지 설명합니다. Docker 대신 호스트에서 직접 Caddy를 실행한다면 기본 Caddy 리버스 프록시 가이드를 참고하세요.

구성 요소가 맞물리는 방식

외부로 포트를 게시하는 것은 Caddy 컨테이너뿐입니다. 모든 백엔드 컨테이너는 사용자 정의 브리지 네트워크(여기서는 `web`)에 참여하고, 그 네트워크에서만 포트를 노출합니다. 사용자 정의 네트워크에서 제공되는 Docker 내장 DNS 서버 덕분에 Caddy는 서비스 이름이나 컨테이너 이름으로 `app:3000`에 접근할 수 있으므로, 재생성할 때마다 바뀌는 컨테이너 IP를 하드코딩할 필요가 없습니다.

백엔드를 게시하지 않는 것은 생각보다 중요합니다. Docker는 자체 iptables 규칙을 작성하므로 `ports:`로 게시한 포트는 ufw가 차단했다고 표시해도 인터넷에서 접근 가능할 수 있습니다. 프록시 뒤에 있어야 하는 것에는 `expose:`를 쓰거나 포트 매핑을 아예 하지 마세요.

구성 요소역할호스트에 게시?
caddy 컨테이너TLS 종료, 호스트 이름별 라우팅, HTTP에서 HTTPS로 리디렉션예: 80/tcp, 443/tcp, 443/udp
web 네트워크서비스 이름용 내장 DNS를 갖춘 공유 브리지 네트워크아니요
앱 컨테이너당신의 서비스(웹 앱, API, 대시보드)아니요, Caddy를 통해서만 접근
caddy_data 볼륨인증서, 개인 키, ACME 계정아니요, 하지만 반드시 유지되어야 함

단계별 안내: docker compose와 일반 Caddyfile

  1. 01.Caddy를 시작하기 전에 각 호스트 이름의 A 레코드(IPv6를 쓴다면 AAAA 레코드도)를 VPS로 지정하세요. 그렇지 않으면 인증서 발급이 실패합니다.
  2. 02.공유 네트워크를 한 번 생성하세요: `docker network create web`.
  3. 03.아래와 같이 `/opt/proxy/compose.yaml`과 `/opt/proxy/Caddyfile`을 만든 뒤 `/opt/proxy`에서 `docker compose up -d`를 실행하세요.
  4. 04.각 앱 스택을 외부 `web` 네트워크에 연결하고 `docker compose up -d`로 시작하세요.
  5. 05.`/opt/proxy`에서 `docker compose logs -f caddy`로 첫 인증서 발급 과정을 지켜보세요.
  6. 06.Caddyfile을 수정한 후에는 다운타임 없이 리로드하세요: `docker compose exec -w /etc/caddy caddy caddy reload`.
# /opt/proxy/compose.yaml
services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"   # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data      # certificates and ACME account keys
      - caddy_config:/config
    networks:
      - web

volumes:
  caddy_data:
  caddy_config:

networks:
  web:
    external: true

앱 연결과 Caddyfile 작성

각 애플리케이션 스택은 같은 외부 네트워크를 선언합니다. 서비스 이름이 Caddy가 접속하는 호스트 이름이 됩니다. `caddy_data` 볼륨은 절대 잃어서는 안 됩니다. 이를 삭제하면 Caddy가 모든 인증서를 다시 요청해야 하고, ACME 속도 제한에 걸릴 수 있습니다.

# /opt/app/compose.yaml
services:
  app:
    image: ghcr.io/example/app:latest
    restart: unless-stopped
    expose:
      - "3000"        # reachable on the network, NOT published on the host
    networks:
      - web

networks:
  web:
    external: true

# /opt/proxy/Caddyfile
{
	email [email protected]
}

app.example.com {
	reverse_proxy app:3000
}

grafana.example.com {
	reverse_proxy grafana:3000
}

api.example.com {
	reverse_proxy api-1:8080 api-2:8080 {
		lb_policy round_robin
		health_uri /healthz
		health_interval 10s
	}
}

Caddy 백엔드: 업스트림, 로드 밸런싱, 헬스 체크

Caddy 용어로 백엔드는 `reverse_proxy`의 업스트림입니다. 하나의 지시어에 여러 업스트림을 나열하고, `round_robin`, `least_conn`, `ip_hash` 같은 `lb_policy`를 선택하고, `health_uri`로 능동 헬스 체크를 추가해 실패한 컨테이너를 순환에서 제외할 수 있습니다. 앱에 헬스 엔드포인트가 없다면 수동 체크(`fail_duration`, `max_fails`)도 유용합니다.

Caddy는 프록시된 요청에 기본적으로 `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`를 설정합니다. 앱이 프록시의 네트워크 대역을 신뢰하도록 설정해야 실제 클라이언트 IP를 보고 `https://` 링크를 생성합니다. WebSocket과 gRPC는 추가 설정 없이 `reverse_proxy`를 통해 작동합니다. 평문 gRPC에는 `reverse_proxy h2c://service:50051`을 사용하세요.

Caddyfile 대신 레이블: caddy-docker-proxy

“Caddy proxy manager”를 검색하는 사람들은 대개 Nginx Proxy Manager가 제공하는 것, 즉 중앙 파일을 수정하지 않고 컨테이너를 추가하면 호스트 이름과 인증서가 생기는 방식을 원합니다. Caddy에는 공식 GUI가 없지만, 커뮤니티 프로젝트 caddy-docker-proxy(github.com/lucaslorentz/caddy-docker-proxy)는 컨테이너 레이블로 Caddyfile을 생성하고 컨테이너가 시작되거나 중지될 때 Caddy를 리로드합니다.

이 도구는 Docker 소켓 읽기 권한이 필요하며, 이는 사실상 호스트의 root 권한입니다. 읽기 전용으로 마운트하고, 프록시 이미지를 최신으로 유지하고, 서비스가 몇 개뿐이라면 일반 Caddyfile을 선호하세요.

services:
  caddy:
    image: lucaslorentz/caddy-docker-proxy:ci-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    environment:
      - CADDY_INGRESS_NETWORKS=web
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - caddy_data:/data
    networks:
      - web

  whoami:
    image: traefik/whoami
    networks:
      - web
    labels:
      caddy: whoami.example.com
      caddy.reverse_proxy: "{{upstreams 80}}"

volumes:
  caddy_data:

networks:
  web:
    external: true

일반 Caddyfile vs caddy-docker-proxy

일반 Caddyfilecaddy-docker-proxy 레이블
설정 위치git으로 버전 관리하는 하나의 파일여러 compose 파일에 흩어진 레이블
Docker 소켓 접근필요 없음필요(보안상 민감)
새 서비스 추가Caddyfile 수정 후 `caddy reload` 실행레이블 추가 후 컨테이너 시작
이미지공식 `caddy:2`Caddy 기반 커뮤니티 이미지
디버깅파일을 직접 읽음로그에서 생성된 설정 확인
적합한 경우소규모의 안정적인 VPS 구성수명이 짧거나 자주 바뀌는 서비스가 많은 경우

VPS에서 실행하기

NoctisNode KVM VPS에서는 전체 root 권한과 전용 IPv4가 제공되므로, 하나의 Caddy 컨테이너에서 80번과 443번을 게시하고 나머지를 모두 숨기기가 간단합니다. Wisp(1 vCPU, 1 GB RAM)는 Caddy와 가벼운 컨테이너 몇 개에 충분하며, 더 무거운 스택은 Pulse나 Umbra에 맞습니다. 인증서 발급 세부 사항과 실패 유형은 Caddy 자동 HTTPS의 작동 원리에서, 원시 TCP나 SSH 전달은 Caddy TCP 및 SSH 프록시에서 다룹니다. 호스트 SSH는 SSH 강화 가이드로 잠그세요.

FAQ

QCaddy가 Docker 컨테이너로 리버스 프록시하게 하려면 어떻게 하나요?

ACaddy와 컨테이너를 같은 사용자 정의 Docker 네트워크에 두고 Caddyfile에서 `reverse_proxy service-name:port`를 사용하세요. `localhost`는 사용하지 마세요. Caddy 컨테이너 안에서는 Caddy 자신을 가리키기 때문입니다.

QCaddy가 컨테이너에 대해 502 Bad Gateway를 반환하는 이유는?

A대개 컨테이너들이 공유 네트워크에 없거나, 포트가 잘못되었거나(게시된 호스트 포트가 아니라 컨테이너 내부 포트를 사용하세요), 앱이 컨테이너 안에서 127.0.0.1에서만 리슨하기 때문입니다. 앱이 0.0.0.0에 바인딩하도록 하고 `docker network inspect web`을 확인하세요.

Qcaddy-docker-proxy는 안전한가요?

A잘 작동하지만, 호스트에 대한 root 수준의 제어권을 주는 Docker 소켓이 필요합니다. 읽기 전용으로 마운트하고, 이미지를 최신으로 유지하고, Caddy 관리 엔드포인트를 노출하지 마세요.

QDocker에서 Caddy는 인증서를 어디에 저장하나요?

A공식 이미지 안의 `/data`에 저장하므로 그곳에 이름 있는 볼륨을 마운트하세요. 이를 잃으면 모든 인증서를 재발급해야 하고 ACME 속도 제한에 걸릴 수도 있습니다.

Q웹 UI가 있는 Caddy proxy manager가 있나요?

ACaddy 자체에는 공식 웹 UI가 없으며, Caddyfile, JSON, 또는 관리 API로 설정합니다. caddy-docker-proxy 같은 레이블 기반 도구는 컨테이너를 추가하면 바로 동작하는 비슷한 워크플로를 제공합니다.

Caddy Docker 리버스 프록시: 자동 HTTPS를 갖춘 Compose 설정 · NoctisNode