Nginx Proxy Manager로 로컬 프록시 구성하기
왜 프록시가 따로 필요했는가
앞 글에서 mkcert로 로컬 HTTPS를 맞춘 뒤에도 문제가 끝나지는 않았다. 인증서를 만드는 것만으로는 여러 로컬 서비스를 실제처럼 붙여 테스트하기 어려웠기 때문이다.
당시 개발 환경은 프런트엔드, 백엔드 API, 모킹 서버처럼 여러 애플리케이션이 동시에 떠야 했고, 일부는 로컬에서, 일부는 이미 올라가 있는 개발 서버와 섞어서 검증해야 했다. 각 서비스가 제각각 다른 포트에서 실행되면 브라우저 입장에서는 서로 다른 origin으로 보이기 쉽고, 그 순간 쿠키, 인증 리디렉션, CORS, WebSocket 연결이 하나씩 어긋나기 시작했다.
이 문제를 가장 단순하게 푸는 방법은 앞단에 리버스 프록시를 두고, 브라우저가 보는 진입점을 하나로 만드는 것이었다.
이 글에서도 내부 도메인과 상세 라우팅은 공개용으로 일반화한다. 예시는 localhost.nike.com과 /api/* 형태로 적겠다.
포트가 흩어져 있으면 왜 테스트가 어려웠는가
브라우저는 도메인만이 아니라 포트까지 포함해서 origin을 판단한다. 그래서 아래처럼 보이는 서비스들은 사람이 보기에는 비슷해도 브라우저 입장에서는 같은 서비스가 아니다.
| 주소 | 브라우저가 보는 기준 | 생기기 쉬운 문제 |
|---|---|---|
https://localhost.nike.com:3000 | 프런트엔드 개발 서버 | HMR은 되지만 백엔드 호출과 origin이 달라짐 |
https://localhost.nike.com:8080 | 백엔드 API | CORS와 credentials 설정이 필요해짐 |
https://localhost.nike.com:8081 | 인증 서비스 | 리디렉션과 세션 흐름이 운영과 달라짐 |
쿠키 자체는 포트만으로 분리되지 않더라도, 실제 요청은 다른 origin으로 처리되기 때문에 결과적으로 쿠키가 기대대로 붙지 않거나 인증 흐름이 깨지는 일이 흔했다. 결국 필요한 것은 각 서비스를 각각 HTTPS로 띄우는 것이 아니라, 브라우저가 하나의 HTTPS 진입점으로 인식하게 만드는 구조였다.
Nginx Proxy Manager를 고른 이유
선택지는 여러 가지가 있었다. 직접 nginx.conf를 관리할 수도 있고, Traefik이나 Caddy 같은 대안도 있다. 다만 당시에는 빠르게 붙이고 자주 바꿔야 하는 로컬 개발 환경이었기 때문에, 설정 파일을 길게 유지하는 방식보다는 UI로 프록시 규칙과 인증서를 관리할 수 있는 쪽이 더 잘 맞았다.
Nginx Proxy Manager를 고른 이유는 단순했다.
- Docker로 바로 띄울 수 있다.
- SSL 인증서를 UI에서 등록할 수 있다.
- 프록시 규칙을 서비스별로 빠르게 수정할 수 있다.
- 로컬에서 임시로 붙였다 떼는 작업이 편하다.
즉, 이 선택은 “가장 강력한 프록시”를 찾기보다 로컬 멀티서비스 환경을 빠르게 정리할 수 있는 도구를 찾은 결과에 가까웠다.
어떤 구조로 붙였는가
핵심은 브라우저가 하나의 HTTPS 주소만 보게 하는 것이었다. 예를 들면 이런 식이다.
https://localhost.nike.com -> frontend dev server
https://localhost.nike.com/api/* -> local backend API
https://localhost.nike.com/auth/* -> local auth service이 구조에서는 브라우저가 https://localhost.nike.com 하나만 상대하고, 실제 라우팅은 프록시가 뒤에서 나눠서 처리한다. 그러면 각 서비스는 내부적으로 여전히 각자 다른 포트에서 떠 있어도, 브라우저 앞단에서는 더 일관된 origin으로 다룰 수 있다.
특히 이 방식이 좋았던 이유는 서비스마다 SSL 설정을 반복하지 않아도 된다는 점이었다. 이미 mkcert로 만든 인증서는 내 장비에서 신뢰하도록 설정해 둔 상태였고, 그 인증서를 물고 실행되는 프록시 서버 역시 브라우저 입장에서는 신뢰 대상이 됐다. 그래서 프록시 뒤에 붙는 프런트엔드 개발 서버나 로컬 백엔드는 별도로 인증서를 설정하지 않아도 됐다. HTTPS 종료는 프록시 한 곳에서만 맡기고, 뒤쪽 애플리케이션은 HTTP만 처리하면 충분했다.
Docker로 프록시를 먼저 띄웠다
Nginx Proxy Manager는 Docker로 실행했다. 로컬 개발용 도구였기 때문에 설치보다 재현 가능성이 더 중요했고, 컨테이너로 띄우는 편이 정리하기 쉬웠다.
Nginx Proxy Manager 컨테이너를 실행한다
관리 UI와 HTTP/HTTPS 포트를 호스트에 노출한다.
설정을 볼륨에 보관한다
프록시 규칙과 인증서 설정이 컨테이너 재시작 후에도 유지되게 한다.
관리 UI에 접속한다
관리 화면에서 프록시 호스트와 인증서를 등록한다.
아래 코드는 실제 파일을 그대로 옮긴 것이 아니라, 당시 구성을 설명하기 위한 개념 예시다.
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
ports:
- "80:80"
- "81:81"
- "443:443"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt이렇게 해두면 관리 UI는 보통 :81로 열고, 실제 브라우저가 보는 로컬 진입점은 :443에서 처리하게 된다.
mkcert 인증서를 프록시에 연결했다
앞 글에서 만든 mkcert 인증서는 여기서 비로소 실제로 쓰였다. 인증서를 브라우저에만 신뢰시키는 것으로는 부족했고, 프록시가 그 인증서를 받아 HTTPS를 종료해야 브라우저가 일관된 진입점을 볼 수 있었기 때문이다.
흐름은 단순했다.
mkcert로 로컬 도메인용 인증서를 만든다.- Nginx Proxy Manager의 SSL Certificates 메뉴에 인증서와 키를 등록한다.
- Proxy Host에서 해당 인증서를 선택한다.
- 필요하면
Force SSL같은 옵션을 켜서 HTTPS 경로를 강제한다.
이렇게 하면 각 애플리케이션은 인증서 파일 경로를 몰라도 되고, 프록시만 인증서를 관리하면 된다.
이게 실제로 편했던 이유는 책임이 분리되기 때문이다.
- 브라우저는
mkcert기반으로 신뢰된 프록시와만 통신한다. - 프록시는 앞단에서 HTTPS를 종료하고 뒤쪽 서비스로 요청을 넘긴다.
- 뒤에 붙은 애플리케이션은 인증서 설정 없이 기존 HTTP 서버처럼 동작하면 된다.
즉 인증서 신뢰는 프록시에서 끝내고, 애플리케이션은 본래 역할에만 집중하게 만든 셈이다.
컨테이너 안에서 호스트 서비스를 보는 방법
조금 헷갈렸던 부분은 프록시는 Docker 안에서 돌고 있는데, 실제 백엔드나 프런트엔드 개발 서버는 호스트 머신에서 돌고 있다는 점이었다. 이때 컨테이너에서 호스트 머신으로 붙기 위한 주소가 필요하다.
macOS에서는 보통 host.docker.internal을 쓸 수 있어서, 프록시 설정에서 이를 백엔드 호스트로 넣으면 됐다. 그래서 실제 프록시 규칙은 이런 개념에 가까웠다.
Domain: localhost.nike.com
Forward Hostname: host.docker.internal
Forward Port: 3000로컬에서 같이 띄운 API가 더 있다면 경로나 도메인별로 규칙을 나눠서 연결하면 된다.
경로 기반 라우팅이 왜 편했는가
로컬에서 서비스마다 서브도메인을 따로 두는 방법도 가능하다. 다만 당시에는 localhost.nike.com 하나 아래에서 프런트엔드와 API를 같이 붙이는 쪽이 더 단순했다. 브라우저가 보는 진입점이 줄어들고, 쿠키와 인증 흐름을 확인할 때도 덜 복잡했기 때문이다.
예를 들면 이런 식으로 나눌 수 있다.
| 경로 | 실제 대상 | 역할 |
|---|---|---|
/ | 프런트엔드 개발 서버 | 화면 렌더링 |
/api/* | 로컬 API | 백엔드 호출 |
/auth/* | 로컬 인증 서비스 또는 개발 서버 | 로그인/세션 관련 흐름 |
이렇게 해두면 브라우저는 하나의 도메인과 하나의 HTTPS 진입점만 인식하고, 내부 서비스 분기는 프록시가 담당한다.
실제로는 인증서보다 origin 정리가 더 큰 효과였다
처음에는 프록시를 “HTTPS 붙이는 도구” 정도로 생각하기 쉽다. 하지만 실제로 체감한 효과는 인증서보다 origin 정리에 가까웠다.
- 포트가 달라서 생기던 불필요한 CORS 설정이 줄었다.
- 로그인과 쿠키 흐름을 더 운영처럼 확인할 수 있었다.
- 로컬 프런트엔드와 일부 개발 서버 API를 함께 붙이기가 쉬워졌다.
- 서비스별 SSL 설정이 아니라 프록시 한 곳만 보면 돼서 관리가 단순해졌다.
즉 Nginx Proxy Manager의 역할은 단순 프록시가 아니라, 로컬에 흩어진 여러 서비스를 브라우저 앞에서는 하나처럼 보이게 정리하는 것이었다.
이 방식의 한계는 무엇이었나
물론 공짜는 아니었다.
- 프록시 규칙이 많아질수록 UI 관리가 번거로워질 수 있다.
- 내부 경로나 포트 구조가 바뀌면 프록시 설정도 같이 바꿔야 한다.
- WebSocket, HMR, 인증 리디렉션처럼 민감한 흐름은 추가 옵션 조정이 필요할 수 있다.
- 개발 환경이 익숙하지 않은 사람은 “왜 서비스가 직접 뜨지 않고 프록시를 거쳐야 하는지” 한 번 더 학습해야 한다.
그래도 각 서비스에 SSL을 직접 붙이고 포트별로 따로 테스트하는 방식보다는 훨씬 덜 고통스러웠다. 특히 Zero Trust 환경에서는 “HTTPS를 어떻게 만들까”보다 “여러 서비스를 어떻게 하나의 브라우저 조건으로 묶을까”가 더 중요한 문제였다.
mkcert 다음 단계로서의 프록시
돌이켜보면 mkcert는 로컬 HTTPS의 출발점이었고, Nginx Proxy Manager는 그 인증서를 실제 개발 흐름에 연결하는 단계였다.
mkcert는 로컬에서 신뢰 가능한 인증서를 만든다.- 프록시는 그 인증서를 받아 브라우저 앞단의 HTTPS를 종료한다.
- 그 위에서 여러 로컬 서비스와 개발 서버를 하나의 진입점으로 묶는다.
그래서 이 두 작업은 따로 떨어진 이야기가 아니라 한 세트에 가까웠다. 로컬 개발 환경을 운영과 비슷하게 맞추려면, 인증서와 프록시를 함께 봐야 했다.