한 VPS에서 여러 도메인/앱을 nginx로 운영하는 구조

정적 사이트 + API 프록시 + PHP(FastCGI)까지 함께 올릴 때의 vhost/경로/캐시/보안 패턴

분야: DevOps/인프라 시리즈: Nginx nginxreverse-proxystaticphp-fpmdeployment

한 VPS에서 여러 서비스를 운영하는 가장 흔한 형태는 아래 조합입니다.

  • 정적 사이트(랜딩/문서) 1~N개
  • Node API(인증, 서비스 API) 1~N개
  • PHP 앱(예: MediaWiki 같은 레거시) 0~1개

이 글은 “다 같이 올려도, 설정이 꼬이지 않게” 하는 구조를 정리합니다.

예시는 모두 익명화되어 있으며, example.com을 사용합니다.

1) 디렉토리 레이아웃(권장)

서버의 “앱 루트”를 한 곳으로 모으면 운영이 편해집니다.

/srv/apps/
  dev.example.com/      # 정적(SSG) dist
  www.example.com/      # 정적 랜딩
  auth-api/             # Node API (127.0.0.1:4000)
  wiki/                 # PHP 앱 (php-fpm)
  social-api/           # Node API (127.0.0.1:4600)

Certbot webroot는 별도로 둡니다.

/var/www/letsencrypt

2) 사이트별 server 블록을 분리

파일을 “도메인 단위”로 나누면 사고가 줄어듭니다.

/etc/nginx/sites-available/
  dev.example.com.conf
  example.com.conf
  wiki.example.com.conf

3) 정적 사이트(vhost)의 기본 패턴

정적은 단순할수록 안정적입니다.

server {
  server_name dev.example.com;
  root /srv/apps/dev.example.com/dist;
  index index.html;

  location ^~ /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
  }

  location / {
    try_files $uri $uri/ =404;
  }
}

4) API 프록시(vhost) 패턴

Node API는 로컬에서만 띄우고(127.0.0.1), nginx가 프록시합니다.

location /api/ {
  proxy_pass http://127.0.0.1:4000;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
}

관련 레시피: nginx: Node API 리버스 프록시 기본

5) PHP(FastCGI) 앱(예: MediaWiki) 패턴

PHP 앱은 “정적 자산도 결국 index.php로 돌아갈 수 있다”는 전제를 두고 구성합니다.

root /srv/apps/wiki;
index index.php index.html;

location / {
  try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php8.3-fpm.sock;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

6) 캐시: “해시된 파일만” 길게

  • 해시가 붙는 번들(/assets/app.abc123.js)은 1년 캐시 + immutable
  • 해시가 없는 파일(예: /index.html)은 짧게 또는 no-cache

관련 레시피: nginx: 해시된 정적 자산에 immutable 캐시

7) 운영에서 많이 터지는 함정(체크리스트)

  • proxy_pass 끝 슬래시(/)로 경로가 의도치 않게 바뀜
  • /social vs /social/ 리다이렉트 누락(상대경로/쿠키 path 꼬임)
  • alias를 쓰면서 try_files 경로를 잘못 조합함
  • WebSocket 프록시에 Upgrade/Connection 헤더를 빼먹음
  • 보안 헤더/CSP를 한 사이트에만 적용하고 나머지를 놓침

같이 보면 좋은 문서:

관련 가이드