로컬 개발용 통합 리버스 프록시 + 웹 관리 UI.
여러 dev 프로젝트의 로컬 포트를 *.test 같은 호스트명으로 묶고, 브라우저에서
신뢰된 HTTPS로 접속할 수 있게 해줍니다.
- 🌐 웹 UI 로
/etc/hostsDNS 항목 + 로컬 포트 매핑을 한 곳에서 관리 - 🔒 기존 Caddy CA Root 재사용 — 이미 시스템에 신뢰된 내부 CA를 그대로 사용하므로 브라우저 경고 없이 HTTPS 동작 (새 인증서/Root 생성 안 함)
- ⚡ Caddy Admin API 로 동적 설정 — 사이트 추가/삭제 시 재시작 불필요
- ♻️ 한 번 띄워두면 모든 dev 프로젝트에서 재사용
미디어 미리보기 기능을 위해 Caddy의 기본 자격증명 가림(redaction)을 끄고 (
should_log_credentials) 프록시를 거치는 모든 요청의Cookie/Authorization헤더가 평문 그대로data/access.log에 기록됩니다. 즉 access 로그를 읽을 수 있는 사람은 세션 쿠키·인증 토큰을 그대로 탈취할 수 있습니다.
- 이 도구는 반드시 로컬 개발 환경(loopback)에서만 사용하세요. 운영/스테이징/공유 서버나 신뢰할 수 없는 네트워크에 노출하지 마세요.
data/access.log(와 백업 파일)은 민감 정보이므로 공유·커밋·업로드 금지입니다. (이미.gitignore대상이지만, 외부로 보내지 않도록 주의하세요.)- 쿠키 로깅을 원치 않으면
lib/caddy.js의should_log_credentials: true를 제거하고 컨트롤 서버를 재시작하세요. 단, 이 경우 인증이 필요한 자산의 미리보기는 동작하지 않습니다.
브라우저 ──HTTPS(internal CA)──▶ Caddy(:443) ──HTTP──▶ localhost:<포트> (dev 서버)
▲
컨트롤 서버(Node, root) ┘ ← 웹 UI(:7879) / Admin API(:2019) / /etc/hosts 관리
-
컨트롤 서버(
server.js)가 root 권한으로 실행되어:- 기존 Caddy 프로세스를 종료하고, 이 디렉토리에서
caddy run으로 Caddy를 자식 프로세스로 실행/감독 - 사이트 목록(
data/sites.json)으로부터 Caddy JSON 설정을 만들어 Admin API로 push /etc/hosts의 관리 블록(# BEGIN/END caddymap)을 동기화- 웹 UI 제공 (
http://127.0.0.1:7879)
- 기존 Caddy 프로세스를 종료하고, 이 디렉토리에서
-
CA Root 재사용: Caddy 설정의
storage.root를 기존 데이터 디렉토리 (~/Library/Application Support/Caddy)로 지정 →tls internal이 기존pki/authorities/local의 root CA를 그대로 사용합니다.
./start.sh/etc/hosts 수정과 :443 바인딩을 위해 sudo 비밀번호를 한 번 묻습니다.
실행되면 브라우저에서 웹 UI를 엽니다:
http://127.0.0.1:7879
웹 UI에서:
- 호스트명 입력 (예:
myapp.test,local-foo.example.com) - 대상 포트 입력 (로컬 dev 서버 포트, 예:
3000) - 추가 클릭
→ /etc/hosts 에 127.0.0.1 myapp.test 자동 추가, Caddy에 라우트 등록,
내부 CA로 인증서 자동 발급. 바로 https://myapp.test 접속 가능.
원본 Host 헤더 유지 옵션: 기본 켜짐(원본 호스트를 업스트림에 전달). Vite 등 host 검사를 하는 dev 서버에서 문제가 있으면 끄세요(업스트림에
localhost:포트로 전달).
한 호스트의 특정 경로를 다른 업스트림으로 보낼 수 있습니다. 폼의 경로 라우팅에서
+ 경로 추가 후 경로 접두사(예 /api)와 포트(예 8080)를 입력하세요.
/api,/api/*둘 다 매칭됩니다.- strip 체크 시 전달 전에 접두사를 제거합니다 (
/api/users→/users). - 경로 라우팅에 매칭되지 않는 나머지는 기본 대상 포트로 갑니다.
- 모노레포에서
app.test/api → :8080, 그 외→ :3000같은 구성에 유용합니다.
dev 서버 없이 디렉토리를 바로 HTTPS로 서빙합니다. 폼 상단에서 📁 정적 파일 모드를
선택하고 디렉토리 절대 경로를 입력하세요. 디렉토리 목록 표시를 켜면 인덱스가 없는
폴더에서 파일 목록(browse)을 보여줍니다. 빌드 산출물(dist/)·문서 미리보기에 좋습니다.
- CORS 허용 체크 시
Access-Control-Allow-*응답 헤더를 주입하고OPTIONS프리플라이트에 자동으로 204를 응답합니다 (Origin 반사 + credentials 허용). 프론트-백 분리 개발 시 CORS 우회. - 커스텀 응답 헤더 에서 임의의 응답 헤더(
키: 값)를 추가로 주입할 수 있습니다.
상단 포트 탭에서 Docker가 실행 중이면 🐳 Docker 컨테이너 패널이 나타나, 포트를
게시한 실행 중인 컨테이너 목록을 보여줍니다. 포트 칩(:5432 → 5432)을 클릭하면
사이트 탭으로 이동하며 폼이 자동으로 채워집니다 (호스트명 = 컨테이너명.test).
↻ 새로고침으로 갱신합니다.
상단 포트 탭에서 로컬에서 LISTEN 중인 dev 서버 포트(비-Docker 프로세스 포함)를
보여줍니다. lsof 로 loopback/전체 인터페이스 리스너만 추려서 프로세스명·PID와 함께
표시하고, 칩(+ vite-5173.test)을 클릭하면 사이트 탭으로 이동하며 호스트명·포트가
폼에 자동으로 채워집니다. 도구 자신의 포트(:7879/:443/:2019)는 제외됩니다.
↻ 새로고침으로 갱신합니다.
폼의 🔒 Basic 인증 에서 사용을 켜고 사용자명/비밀번호를 입력하면 해당 사이트가
HTTP Basic 인증으로 보호됩니다. 스테이징 흉내·인증 게이트 테스트에 유용합니다.
- 비밀번호는 저장 시
caddy hash-password(bcrypt)로 해싱되어 저장됩니다. 평문은 디스크에 남지 않습니다. - 수정 시 비밀번호를 비워두면 기존 비밀번호가 유지됩니다(사용자명만 변경 가능).
- CORS 프리플라이트(
OPTIONS)는 인증을 거치지 않으므로 CORS와 함께 써도 됩니다.
프록시 대상(dev 서버)이 꺼져 있어 연결에 실패하면, 502 raw 대신 친절한 메인터넌스 안내 페이지(호스트명·오류코드 포함)를 표시합니다.
상단 통계 탭에서 access.log를 집계해 총 요청 / 에러율 / 평균 응답시간과
호스트별 요청 수·에러·평균/최대 응답시간·전송량·상태코드 분포 막대를 보여줍니다.
↻ 새로고침으로 최신 집계를 가져옵니다.
상단에는 시간대별 트래픽 차트가 함께 표시됩니다. access.log의 타임스탬프를 시간 범위에 맞춰 자동 버킷팅(초~시간 단위 자동 선택)하여 요청량을 막대로, 5xx 에러를 빨간색으로 겹쳐 보여줍니다. 막대에 마우스를 올리면 해당 구간의 요청 수·에러·평균 응답시간을 봅니다. 막대를 클릭하면 그 시간 구간에 해당하는 로그가 차트 아래에 펼쳐져, 트래픽 급증·에러 구간을 바로 드릴다운해 볼 수 있습니다(‘미디어 제외’ 토글 지원).
웹 UI 없이 터미널에서 사이트를 관리합니다 (컨트롤 서버가 실행 중이어야 함):
uc list # 사이트 + 업스트림 상태
uc add myapp.test 3000 # 프록시 사이트 추가 (--cors, --auth user:pass 옵션)
uc add docs.test --static ./dist --browse # 정적 디렉토리 서빙
uc rm myapp.test # 삭제
uc enable|disable myapp.test # 활성/비활성
uc open myapp.test # 브라우저에서 열기
uc docker # 포트 게시 컨테이너 목록
uc ports # 로컬 리스닝 포트 목록 (dev 서버 탐지)전역에서
uc를 쓰려면 프로젝트 루트에서npm link하세요.UI_HOST/UI_PORT환경변수를 따릅니다.
mcp-server.js 는 컨트롤 서버의 REST API를 MCP(Model Context Protocol) 툴로 노출합니다.
Claude Code 등 MCP 클라이언트에서 사이트를 추가/조회하고, access 로그·통계·헬스를 읽을 수 있습니다.
stdio 트랜스포트(JSON-RPC over stdin/stdout)로 동작하며, 내부적으로는 Caddy admin이 아니라
컨트롤 서버(http://127.0.0.1:7879)에 붙으므로 컨트롤 서버가 실행 중이어야 합니다.
Claude Code 에 등록:
claude mcp add caddymap -- node /절대경로/caddymap/mcp-server.js
# 또는 npm link 후: claude mcp add caddymap -- caddymap-mcp노출되는 툴:
| 툴 | 설명 |
|---|---|
get_status |
전체 상태(Caddy 동작 여부·사이트 수·CA·admin) — 먼저 호출 |
list_sites |
등록된 사이트 목록 |
add_site |
사이트 추가 (hostname 필수; 프록시는 targetPort, 정적은 type:"static"+root) — routes·headers·auth·cors·preserveHost·https·browse·enabled 옵션 |
update_site |
사이트 수정 (id 필수; headers/auth에 null 전달 시 제거, auth.password 전달 시 재해싱) |
toggle_site / remove_site |
활성 토글 / 삭제 (id 필수) |
tail_access_log |
access 로그 tail(tail/host/method/status/errorsOnly 필터) |
logs_in_range |
시간 구간(from/to, epoch 초) access 로그 — 차트 스파이크 드릴다운 |
clear_access_log |
access 로그 비우기(되돌릴 수 없음) |
get_stats |
전체/호스트별 트래픽 통계 + 시계열(series/bucketSec) |
check_health |
업스트림 TCP 헬스(502 원인 진단) |
list_docker / list_ports |
포트 게시 컨테이너 / 로컬 리스닝 포트 |
export_sites / import_sites |
사이트 목록 내보내기 / 가져오기(merge:true 시 병합, 기본 전체 교체) |
sync |
Caddy 설정 + /etc/hosts 강제 재적용 |
UI_HOST/UI_PORT환경변수를 따릅니다. 진단 출력은 stderr로만 나가고 stdout은 MCP 프로토콜 전용입니다.
각 사이트 카드에 대상 포트의 실시간 상태 점이 표시됩니다 (5초마다 TCP 연결로 확인):
- 🟢 응답 — dev 서버가 떠 있음
- 🔴 닫힘 — 포트가 닫힘(dev 서버 미실행) → 502가 떠도 프록시가 아니라 내 서버 문제임을 바로 구분
- ⚪ 비활성 — 꺼둔 사이트
각 카드의 ↗ 열기 버튼으로 https://호스트명 을 새 탭에서 바로 엽니다.
등록된 사이트 헤더의 ⬇ 내보내기 / ⬆ 가져오기 로 사이트 매핑을 JSON 파일로 주고받습니다.
새 머신·팀원과 설정을 공유할 때 사용하세요. 가져올 때 병합(같은 호스트는 덮어씀)과
전체 교체 중 선택할 수 있습니다.
웹 UI 상단의 로그 탭에서 프록시를 거쳐 가는 모든 요청을 실시간으로 볼 수 있습니다.
- 시간 · 메서드 · 상태코드(색상) · 호스트 · 경로 · 응답시간 · 크기 · 클라이언트 IP
- 라이브: 1.5초마다 새 요청을 증분으로 가져와 자동 추가 (
tail -f방식) - 자동 스크롤: 새 로그가 오면 맨 아래로 따라감
- 필터: host/경로/메서드/상태코드로 즉시 필터링
- 미디어 제외: 이미지/동영상 요청을 숨겨 핵심 트래픽만 보기
- 업스트림 에러 강조: 502/503/504(대상 서버 연결 실패)는 행 전체가 빨갛게(⚠) 표시
- 지우기: access.log 비우기
행을 클릭하면 요약 / 요청 헤더 / 응답 헤더 / RAW 탭으로 전체 정보를 봅니다.
- 미디어 미리보기: 이미지/동영상 요청 행을 클릭하면 해당 자산을 다이얼로그에서
바로 미리봅니다. 컨트롤 서버가 로그에 남은 원본 요청(쿠키 포함)을 Caddy로 재생해
가져오므로, 브라우저의 서드파티 쿠키 차단 없이 인증이 필요한 자산도 표시됩니다.
이 기능 때문에 access 로그에 쿠키가 평문으로 남습니다 — 위의
⚠️ 경고를 참고하세요. (Caddy가 가린 쿠키는 재생할 수 없어 401/403이면 “↗ 새 탭”으로 직접 열라고 안내합니다.)
Caddy access 로그는 data/access.log 에 JSON 형식으로 기록되며,
5MB마다 자동 로테이션(백업 1개, 최대 3일)되어 디스크를 무한히 차지하지 않습니다.
⚠️ 자격증명 로깅: 미디어 미리보기를 위해should_log_credentials가 켜져 있어Cookie/Authorization헤더가 access 로그에 평문으로 남습니다. 로컬 개발 전용으로만 쓰고 로그 파일을 외부에 공유하지 마세요.
로그 탭을 처음 추가한 뒤에는 컨트롤 서버를 재시작해야 access 로깅이 적용됩니다 (
./daemon.sh restart또는 포그라운드면Ctrl+C후./start.sh). 이후 브라우저 새로고침.
./stop.sh # 컨트롤 서버 + Caddy 종료 (/etc/hosts 블록은 유지)
./uninstall.sh # 위 + /etc/hosts 관리 블록 제거포그라운드(./start.sh) 대신 macOS launchd LaunchDaemon으로 등록하면,
터미널을 닫아도 계속 실행되고 재부팅 시 자동 시작됩니다. root 데몬으로 돌기 때문에
/etc/hosts 수정과 :443 바인딩이 그대로 동작합니다.
⚠️ 포그라운드(./start.sh)가 켜져 있다면 먼저Ctrl+C로 종료하세요 (포트 충돌 방지).
./daemon.sh install # 데몬 설치 + 시작 (부팅 시 자동 실행)
./daemon.sh status # 상태 확인 (launchd + 웹 UI 응답)
./daemon.sh restart # 재시작
./daemon.sh logs # 로그 실시간 보기 (tail -f data/daemon.log)
./daemon.sh stop # 정지 (재부팅해도 안 뜸)
./daemon.sh start # 다시 시작
./daemon.sh uninstall # 데몬 정지 + plist 제거
sudo없이 실행하세요 — 권한이 필요한 부분(plist 복사, launchctl)만 자동으로 sudo 합니다. (npm run daemon:install,daemon:status,daemon:logs등도 동일하게 동작)
동작 방식
- plist 위치:
/Library/LaunchDaemons/com.caddymap.daemon.plist(root:wheel, 644) KeepAlive=true— 컨트롤 서버가 죽으면 launchd가 자동 재시작. Caddy가 비정상 종료되면 컨트롤 서버도 함께 종료시켜 전체 스택을 깨끗하게 재시작합니다.- plist에
CADDY_STORAGE(기존 CA Root 경로)와 node/caddy 절대 경로를 박아두어, launchd가 root로 실행해도 기존 CA를 정확히 재사용합니다. - 로그:
data/daemon.log - node 경로가 plist에 고정되므로, nvm 으로 node 버전을 바꾸면
./daemon.sh install을 다시 실행해 경로를 갱신하세요.
| 변수 | 기본값 | 설명 |
|---|---|---|
UI_PORT |
7879 |
웹 UI 포트 |
UI_HOST |
127.0.0.1 |
웹 UI 바인드 주소 |
CADDY_ADMIN |
http://localhost:2019 |
Caddy Admin API |
CADDY_STORAGE |
~/Library/Application Support/Caddy |
Caddy 데이터 디렉토리(=기존 CA Root 위치) |
CADDY_BIN |
caddy |
caddy 실행 파일 경로 |
UI_ALLOWED_HOSTS |
(없음) | 관리 API 접근을 허용할 추가 host:port (쉼표 구분). LAN 노출 시에만 |
컨트롤 서버(:7879)는 root 권한으로 돌고 인증이 없으므로, 사용자가 브라우저에 열어둔
악성 페이지가 이를 조작하지 못하도록 두 가지 검사를 합니다 (/api/* 한정):
- Host allowlist —
127.0.0.1:7879·localhost:7879·[::1]:7879만 허용. DNS-rebinding(악성 호스트명을127.0.0.1로 재해석하는 공격)을 차단합니다. - Origin 검사 — 상태를 바꾸는 요청(POST/PUT/PATCH/DELETE)에서
Origin이 있으면 allowlist와 일치해야 합니다(브라우저발 CSRF 차단). 없으면 통과 — 그래서ucCLI·curl· 스크립트는 영향 없이 그대로 동작합니다.
프록시 트래픽(
:443→ dev 서버)은 이 검사와 무관합니다.https://*.test접속은 그대로입니다. LAN(폰 등)에서 관리 UI를 열어야 하면UI_ALLOWED_HOSTS=192.168.0.x:7879처럼 추가하세요.
- macOS, Caddy 설치 (
brew install caddy) - Node.js 18+ (의존성 없음 —
npm install불필요) - 기존에 Caddy 내부 CA가 시스템에 신뢰되어 있어야 함. 안 되어 있다면 한 번:
sudo caddy trust
caddymap/
├── server.js # 컨트롤 서버 (root 실행) — HTTP API + 정적 UI + 사이트 검증 + 라이프사이클
├── cli.js # `uc` CLI — REST API 위의 얇은 터미널 클라이언트
├── mcp-server.js # MCP stdio 서버 — REST API를 MCP 툴로 노출 (AI 에이전트 연동)
├── config.js # 설정/경로 (기존 CA storage 경로 계산 포함)
├── lib/
│ ├── caddy.js # Caddy 프로세스 관리 + 설정 생성(경로라우팅/정적/CORS/인증/에러) + Admin API
│ ├── hosts.js # /etc/hosts 관리 블록 편집
│ ├── store.js # sites.json 읽기/쓰기
│ ├── health.js # 업스트림 TCP 헬스 체크
│ ├── logs.js # access.log 증분 tail
│ ├── docker.js # docker ps 파싱 (포트 게시 컨테이너 탐지)
│ ├── ports.js # lsof 파싱 (로컬 리스닝 포트 탐지)
│ └── stats.js # access.log 트래픽 집계
├── public/ # 웹 UI (index.html / app.js / style.css)
├── data/sites.json # 사이트 매핑 (영속 상태, 단일 소스 오브 트루스)
├── data/caddy.json # buildCaddyConfig() 산출물 (생성물 — 직접 편집 금지)
├── daemon.sh # launchd LaunchDaemon 설치/관리
├── start.sh / stop.sh / uninstall.sh
├── CLAUDE.md # Claude Code 작업 가이드
└── INSTALL.md # 처음부터 설치 가이드 (macOS)