feat(docker): 리버스 프록시가 다른 서버에 있을 때 — frontend 를 사설 IP 에만 게시하는 선택 파일

운영 메일 서버를 NCP 에 따로 두고, 포털의 TLS 는 기존 서버의 Nginx Proxy Manager(다른 사이트들과 80 · 443 을 함께 쓴다)가
같은 VPC 사설망으로 넘기기로 했다. 메일 엔진이 그 서버의 443 을 직접 써야 해서(ACME · MTA-STS) 프록시를 같은 호스트에 둘 수 없다.

- docker-compose.remote-proxy.yml: frontend 를 proxy 네트워크에서 빼고 8080 을 FRONTEND_PRIVATE_BIND(사설 IPv4:포트)에만 게시한다.
  compose 의 ${…:?} 는 빈 값만 막아 포트뿐인 값이면 모든 주소에 열리므로, 한 번짜리 frontend-bind-check 가 값을 먼저 보고
  아니면 frontend 를 띄우지 않는다(네트워크 없음 · 비-root · DB 이미지의 셸).
- .env.production 의 COMPOSE_FILE 로 켠다 — 운영 명령(--env-file .env.production)을 그대로 쓰고 -f 를 빠뜨려 기본 구성으로 도는 일이 없게.
- TRUST_PROXY 는 <EDGE_NETWORK_SUBNET>,<프록시 서버 사설 IP> 주소 목록, NPM 은 v2.16.0 소스로 기본값을 확인해 고급 칸 세 줄
  (읽기 150s · 접근 로그 끔 · 사설 대역의 X-Real-IP 믿지 않기)과 Block Common Exploits 끄기(메일 검색이 403)를 적었다.
- NCP 는 공인 IP 를 사설 IP 로 NAT 하므로 게시 주소로는 바깥을 막지 못한다 — 8080 은 ACG 가 프록시 서버 사설 IP 에서만 받는다.
- CI infra 잡이 이 조합의 config 와 frontend-bind-check 의 받을 값 · 거절할 값을 본다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 14:07:15 +09:00
parent 867543c49f
commit 3029bb20f2
4 changed files with 313 additions and 5 deletions
+20 -1
View File
@@ -59,13 +59,15 @@ REDIS_DATA_DIR=./data/redis
#
# - TRUST_PROXY : 백엔드 앞 프록시 — 실제 클라이언트 IP(로그인 실패 한도의 기준)를 읽는다. 운영 필수
# 이 compose 의 운영 경로는 클라이언트 → 외부 리버스 프록시(TLS) → 프론트 nginx → 백엔드 하나뿐이다.
# (프론트 nginx 는 호스트 포트를 열지 않고 TLS 도 하지 않으므로 외부 프록시 없이는 들어올 수 없다 — 1 은 해당 없음)
# (프론트 nginx 는 TLS 를 하지 않고 호스트 포트도 열지 않으므로(다른 서버의 프록시용 구성은 사설 IP 에만) 외부 프록시 없이는 쓸 수 없다 — 1 은 해당 없음)
# 2 = 홉 수. proxy 네트워크를 이 프로젝트의 외부 프록시만 쓸 때
# 주소 목록 = <EDGE_NETWORK_SUBNET>,<외부 프록시 IP> (예: 172.16.72.0/24,172.20.0.10) — proxy 네트워크를
# 다른 프로젝트와 함께 쓴다면 이쪽을 권장한다. 홉 수는 "몇 번째 주소"만 믿어서, 같은 네트워크의 다른
# 컨테이너가 외부 프록시를 건너뛰고 frontend 에 바로 붙으면 X-Forwarded-For 를 꾸며 IP 를 바꿀 수 있다.
# 외부 프록시 IP 는 frontend 가 보는 주소(proxy 네트워크 안 주소)이며 고정해 둔다(ipv4_address 등).
# 넓은 사설 대역을 통째로 넣지 않는다.
# 외부 프록시가 다른 서버에 있으면(아래 "리버스 프록시가 다른 서버에 있을 때") 주소 목록으로 —
# <EDGE_NETWORK_SUBNET>,<프록시 서버의 사설 IP> (예: 172.16.72.0/24,10.0.1.11). 사설 IP 는 frontend 접근 로그의 첫 칸이다
# true·0·false 는 운영에서 기동이 거부된다.
# ==========================================
BACKEND_API_URL=https://portal.arasystem.it.kr/api
@@ -99,6 +101,23 @@ DB_SYNCHRONIZE=false
# ==========================================
COMPOSE_PROFILES=mail
# ==========================================
# 리버스 프록시가 다른 서버에 있을 때 (선택 — mail/README.md "리버스 프록시가 다른 서버에 있을 때")
#
# 기본은 외부 리버스 프록시가 이 호스트에서 external proxy 네트워크로 <PROJECT_NAME>-frontend:8080 에 붙는 구성이다.
# 다른 서버의 프록시(예: 같은 VPC 의 Nginx Proxy Manager)가 사설망으로 붙는다면 COMPOSE_FILE 의 주석을 풀고 FRONTEND_PRIVATE_BIND 를 채운다 —
# docker-compose.remote-proxy.yml 이 frontend 를 proxy 네트워크에서 빼고 8080 을 이 서버의 사설 IP 에만 게시한다.
# COMPOSE_FILE : compose 파일 목록 (리눅스는 : 로 잇는다). 이 파일에 두면 문서의 운영 명령(sudo docker compose --env-file .env.production …)이
# -f 없이 그대로 두 파일을 읽는다 (compose v2.40 · 5.4 에서 확인). 쓰지 않을 때는 주석으로 둔다 —
# 빈 값(COMPOSE_FILE=)은 compose 가 파일 목록으로 읽다 실패한다
# FRONTEND_PRIVATE_BIND : <이 서버의 사설 IPv4>:<포트> (예: 10.0.1.12:8080 — ip -4 addr 로 본다).
# 비어 있으면 compose 가 멈추고, 포트뿐인 값 · 0.0.0.0 · 공인 IP · IPv6 면 frontend 가 뜨지 않는다 (frontend-bind-check)
# TRUST_PROXY 는 위 "화면·API 주소" 의 주소 목록으로 바꾼다 — <EDGE_NETWORK_SUBNET>,<프록시 서버의 사설 IP>
# ⚠️ 게시 주소만으로는 바깥을 막지 못한다 — NCP 는 공인 IP 를 사설 IP 로 NAT 하므로 8080 은 방화벽(ACG)이 프록시 서버의 사설 IP 에서만 받게 한다
# ==========================================
# COMPOSE_FILE=docker-compose.yml:docker-compose.remote-proxy.yml
FRONTEND_PRIVATE_BIND=
# ==========================================
# 파일 업로드 (로컬 디스크)
#