우리 내부 대시보드에서 Nginx 리버스 프록시를 사용한 App Hub 설정
Source: Dev.to
위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다. 코드 블록, URL 및 마크다운 형식은 그대로 유지하고, 본문만 번역해 드립니다.
배경
우리 팀은 내부 대시보드로 Flask‑기반 SPA를 운영하고 있습니다. 기능이 늘어나면서 서비스 간에 빠르게 이동할 수 있는 방법이 필요했으며, 그래서 App Hub 패널을 만들었습니다.
이번에 적용된 변경 사항:
- 3개의 폐기된 서비스를 제거했습니다
- 3개의 새로운 서비스(TechsFree Shop, TechsFree ERP, Accounting AI)를 추가했습니다
- 이동된 서비스의 URL을 업데이트했습니다
문제: IP‑기반 접근 시 Host 헤더
App Hub 링크에서 IP 주소로 다른 내부 서버의 서비스를 참조할 때, Nginx가 올바른 vhost로 라우팅되지 않았습니다.
Nginx 설정은 서브도메인 기반 라우팅(techsfree.com, blog.techsfree.com, 등)을 가정합니다. IP 주소로 직접 접근하면 Host 헤더가 해당 IP 자체가 되며, 이는 어떤 server_name과도 일치하지 않습니다.
해결책: 기본 서버에 위치 추가
# /www/server/panel/vhost/nginx/0.default.conf
server {
listen 80 default_server;
# ...
location /shop/ {
root /www/apps/techsfree-shop;
try_files $uri $uri/ /shop/index.html;
}
location /erp/ {
proxy_pass http://127.0.0.1:3101/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
핵심: default_server 가상 호스트에 경로 기반 location 블록을 추가합니다. 도메인 없이 내부 IP로 서비스를 직접 접근할 때 가장 깔끔한 방법입니다.
정적 파일 vs 리버스 프록시
| Service | Architecture | Nginx Config |
|---|---|---|
| TechsFree Shop | Vite‑built static files | root directive |
| TechsFree ERP | Backend API + Frontend | proxy_pass |
정적 파일 (Shop)
빌드 산출물은 /www/apps/techsfree-shop/에 배치되고 Nginx가 직접 제공한다. SPA이므로 try_files $uri $uri/ /shop/index.html이 클라이언트‑사이드 라우팅을 처리한다.
리버스 프록시 (ERP)
백엔드는 Node.js(ts‑node)에서 포트 3101로 실행된다. Nginx가 요청을 전달한다. proxy_pass의 뒤쪽 슬래시가 중요하다:
# Wrong: /erp/ prefix gets passed through to the backend
location /erp/ {
proxy_pass http://127.0.0.1:3101; # no trailing slash
}
# Right: /erp/ prefix is stripped before forwarding
location /erp/ {
proxy_pass http://127.0.0.1:3101/; # with trailing slash
}
proxy_pass URL에 뒤쪽 슬래시를 포함하느냐에 따라 경로 처리 방식이 달라진다. 놓치기 쉽고 디버깅이 번거롭다.
systemd와 PM2를 나란히 관리하기
또 다른 교훈: 프로세스 관리자를 혼합하면 혼란을 초래한다.
대시보드는 systemd가 설치된 Ubuntu에서 실행됩니다. 잠시 **aaPanel (PM2)**가 실행되는 서버로 옮겨보려 했지만 권한 문제에 부딪혀 되돌렸습니다.
# Check on systemd server
systemctl --user status task-dashboard.service
# Check on PM2 server (if migrated)
pm2 status
동일한 서비스가 두 환경에서 동시에 실행될 때, 변경 사항이 반영되지 않았습니다 — systemd가 기존 프로세스를 종료하지 않고 새 프로세스를 생성했기 때문입니다.
# Find stale processes
ps aux | grep server.py
# Kill explicitly, then restart
kill <pid>
systemctl --user restart task-dashboard.service
배포 후 항상 중복 프로세스가 있는지 확인하세요.
버그 수정: 일관되지 않은 JSON 필드 이름
“완료된 작업 일괄 삭제” 기능을 구현하던 중, 눈에 잘 띄지 않는 버그를 발견했습니다.
작업 JSON은 completed를 사용하지만, API의 필터 로직은 done을 확인하고 있었습니다:
# Buggy
active_tasks = [t for t in tasks if not t.get('done')]
# Correct
active_tasks = [t for t in tasks if not t.get('completed')]
필드 이름을 정했다면 프론트엔드, 백엔드, 문서 전반에 걸쳐 일관되게 사용하세요. 어느 하나라도 다르면 이런 버그가 발생합니다.
요약
- IP 기반 접근을 처리하기 위해 기본 서버에 위치를 추가합니다.
- 정적 파일과 리버스 프록시를 사용 사례에 따라 선택합니다 (SPA는
try_files가 필요합니다). proxy_pass의 트레일링 슬래시를 주의하세요 — 경로 처리 방식이 달라집니다.- 프로세스 매니저를 혼용할 때 배포 후 중복 프로세스가 있는지 확인합니다.
- 전체 스택에서 JSON 필드 이름을 일관되게 유지합니다.
작은 내부 도구라도 프로덕션과 유사한 환경에서 실행하면 실제 교훈을 얻을 수 있습니다.
Tags: nginx, reverse-proxy, spa, flask, systemd, deployment, webdev, infra