@extends('layouts.app') @section('title', 'Nginx') @push('styles') @endpush @section('content')
아카데미 Nginx

Nginx

기초부터 실전까지 — SAM 프로젝트의 Nginx 설정을 완전 해부하는 교육 가이드

Nginx 히어로
{{-- ============================================================ --}} {{-- 1. 웹 서버란 무엇인가 --}} {{-- ============================================================ --}}

1 웹 서버란 무엇인가

웹 서버의 3가지 역할

비유: 24시간 접수 창구

웹 서버는 24시간 열려 있는 접수 창구다. 손님(클라이언트)이 "이 서류 주세요"라고 요청하면, 접수 담당자(웹 서버)가 서류 창고에서 꺼내주거나, 다른 부서(PHP-FPM)에 처리를 맡기거나, 수상한 손님을 돌려보낸다.

웹 서버 = 접수 창구 비유

웹 서버의 3가지 역할: 정적 파일 서빙, 동적 요청 전달, 보안 검사

정적 파일 서빙

HTML, CSS, JS, 이미지 등
서류 창고에서 바로 꺼내줌

동적 요청 전달

PHP-FPM에 처리 위임
"다른 부서에 맡기겠습니다"

보안 검사

수상한 요청 차단
"출입 금지 명단" 확인

Apache vs Nginx 비교

Apache vs Nginx 비교

프로세스 기반(Apache) vs 이벤트 기반(Nginx)

항목 Apache Nginx
처리 방식프로세스/스레드 기반이벤트 기반 (비동기)
동시 접속접속마다 프로세스 생성하나의 워커가 수천 개 처리
메모리 사용높음 (접속 비례 증가)낮음 (거의 일정)
정적 파일보통매우 빠름
리버스 프록시가능 (추가 모듈)기본 내장, 뛰어남

SAM이 Nginx를 선택한 이유

SAM은 5개 도메인(React, API, MNG, Sales, 5130)을 하나의 서버에서 운영한다. Nginx의 리버스 프록시낮은 메모리 사용량이 이 구조에 최적이다. 2코어 3.8GB RAM 서버에서도 안정적으로 동작한다.

{{-- ============================================================ --}} {{-- 2. Nginx란 무엇인가 --}} {{-- ============================================================ --}}

2 Nginx란 무엇인가

탄생 배경: C10K 문제

Nginx(엔진엑스)는 2004년 러시아 개발자 Igor Sysoev가 만들었다. 당시 웹 서버들은 C10K 문제(동시 1만 개 연결 처리)를 해결하지 못했다. Apache는 접속마다 프로세스를 생성하여 메모리가 폭발했고, Nginx는 이벤트 기반 아키텍처로 이 문제를 해결했다.

비유: 식당 주문 방식의 차이

Apache: 손님마다 전담 웨이터를 배정. 100명이 오면 웨이터 100명 필요.
Nginx: 웨이터 1명이 번호표를 나눠주고, 주문이 준비되면 호출. 수천 명도 처리 가능.

2004

최초 공개

34%

글로벌 시장 점유율

C10K

해결한 핵심 문제

이벤트

비동기 처리 방식

설정 파일 계층 구조

Nginx 설정 계층 구조

마트료시카처럼 중첩되는 설정 블록

비유: 건물 → 층 → 방

Nginx 설정은 마트료시카 인형처럼 중첩된다. 건물 전체 규칙(main) 안에 층별 규칙(http), 층 안에 방별 규칙(server), 방 안에 공간별 규칙(location)이 있다.

# main 컨텍스트 (건물 전체) worker_processes auto; events { # 연결 관리 (관리사무소) worker_connections 1024; } http { # HTTP 설정 (건물 1층~) server { # 도메인별 설정 (각 방) listen 443 ssl; server_name mng.sam.kr; location / { # 경로별 설정 (방 안 공간) try_files $uri /index.php; } } }

계층 정리

mainevents / httpserverlocation

상위 설정은 하위로 상속된다. 하위에서 같은 설정을 하면 덮어쓴다.

{{-- ============================================================ --}} {{-- 3. 리버스 프록시 이해하기 --}} {{-- ============================================================ --}}

3 리버스 프록시 이해하기

Forward Proxy vs Reverse Proxy

비유: 안내 데스크

Forward Proxy: 회사 직원이 외부 인터넷에 접속할 때 거치는 "사내 인터넷 관문". "나 대신 밖에 나가서 가져와줘."
Reverse Proxy: 외부 손님이 회사 내부 서비스에 접근할 때 거치는 "안내 데스크". "손님을 적절한 담당자에게 연결해드리겠습니다."

Forward vs Reverse Proxy 비교

Forward Proxy는 클라이언트 대리, Reverse Proxy는 서버 대리

항목 Forward Proxy Reverse Proxy
위치클라이언트 쪽서버 쪽
목적클라이언트 익명성서버 보호/로드 밸런싱
예시회사 프록시, VPNNginx, Cloudflare
SAM 사용사용 안 함외부 Nginx가 이 역할

SAM 5개 도메인 라우팅

SAM 5개 도메인 라우팅 맵

외부 Nginx가 도메인별로 각 서비스로 분배하는 구조

SAM의 외부 Nginx는 *.sam.kr 와일드카드 SSL 인증서를 사용하여 5개 도메인을 하나의 서버에서 처리한다. 각 도메인은 server_name 디렉티브로 구분되어 서로 다른 내부 서비스로 전달된다.

도메인 서비스 전달 방식 설명
dev.sam.krReact (Next.js)proxy_pass http://react:3000프론트엔드 SPA
api.sam.krAPI (Laravel)fastcgi_pass api:9000REST API 서버
mng.sam.krMNG (Laravel)fastcgi_pass mng:9000관리자 웹
sales.sam.krSales (PHP)proxy_pass http://sales:80영업 시스템
5130.sam.kr레거시 (PHP 7.3)proxy_pass http://php73:80기존 5130 시스템

프록시 헤더 해설

리버스 프록시는 클라이언트의 원래 정보를 백엔드 서버에 전달해야 한다. 그렇지 않으면 백엔드는 모든 요청이 "Nginx에서 온 것"으로만 보인다.

proxy_set_header Host $host; # 원래 도메인명 전달 proxy_set_header X-Real-IP $remote_addr; # 클라이언트 실제 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 거쳐온 IP 목록 proxy_set_header X-Forwarded-Proto $scheme; # HTTP or HTTPS

비유: 택배 송장

안내 데스크(Nginx)가 택배를 내부 담당자에게 전달할 때, "원래 보낸 사람 이름과 주소"를 송장에 적어 붙여야 한다. 그래야 담당자가 "이 택배가 어디서 왔는지" 알 수 있다. Host는 수신자, X-Real-IP는 발송자 주소에 해당한다.

{{-- ============================================================ --}} {{-- 4. SAM Nginx 설정 완전 해부 --}} {{-- ============================================================ --}}

4 SAM Nginx 설정 완전 해부

2계층 Nginx 아키텍처

비유: 건물 정문 경비실 + 각 층 접수 창구

SAM은 Nginx를 2단계로 사용한다. 1단계(외부 Nginx)는 건물 정문 경비실로, 어떤 손님이 어느 층으로 가야 하는지 안내한다. 2단계(내부 Nginx)는 각 층의 접수 창구로, 해당 층의 PHP-FPM에게 실제 업무를 맡긴다.

2계층 Nginx 아키텍처

외부 Nginx(도메인 라우팅 + SSL) → 내부 Nginx(PHP-FPM 통신)

── SAM 요청 흐름 ──

사용자외부 Nginx (SSL 종료, 도메인 라우팅)

├→ react:3000 (proxy_pass) → Next.js

├→ api:9000 (fastcgi_pass) → PHP-FPM

├→ mng:80내부 Nginx → mng:9000 (fastcgi_pass)

├→ sales:80내부 Nginx → sales:9000 (fastcgi_pass)

└→ php73:80내부 Nginx → 5130:9000 (fastcgi_pass)

외부 Nginx 설정 분석

docker/nginx/nginx.conf — 외부 Nginx는 SSL 인증서를 관리하고 5개 도메인을 각 서비스로 라우팅한다.

글로벌 설정

client_max_body_size 100M; # 최대 업로드 크기 100MB # SSL 공통 설정 (*.sam.kr 와일드카드 인증서) ssl_certificate /etc/nginx/ssl/sam.kr.crt; ssl_certificate_key /etc/nginx/ssl/sam.kr.key; ssl_protocols TLSv1.2 TLSv1.3;

React 도메인 (dev.sam.kr) — WebSocket 지원

server { listen 443 ssl; server_name dev.sam.kr; location / { proxy_pass http://react:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # WebSocket (HMR용) proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; # 24시간 (개발 HMR) } }

MNG 도메인 (mng.sam.kr) — 정적 캐싱 + FastCGI

server { listen 443 ssl; server_name mng.sam.kr; # 테넌트 스토리지 이중 프록시 location ^~ /tenant-storage/ { proxy_pass http://mng:80/tenant-storage/; expires 7d; add_header Cache-Control "public, immutable"; } # 정적 자산 30일 캐싱 location ~* \.(js|css|png|jpg|svg|ico|woff2?)$ { expires 30d; add_header Cache-Control "public"; access_log off; } # PHP 처리 location ~ \.php$ { fastcgi_pass mng:9000; fastcgi_read_timeout 300; # 5분 (장시간 작업) } }

핵심 디렉티브 해설

try_files $uri $uri/ /index.php?$query_string — 파일이 있으면 서빙, 없으면 Laravel 라우터로 전달

fastcgi_pass mng:9000 — PHP-FPM 프로세스에 요청 전달 (FastCGI 프로토콜)

proxy_pass http://react:3000 — HTTP 프록시로 요청 전달 (일반 HTTP)

^~ /tenant-storage/ — 정규식보다 우선 매칭 (prefix match)

내부 Nginx 설정 분석

각 컨테이너 내부의 Nginx는 단순하다. listen 80으로 외부 Nginx의 요청을 받고, fastcgi_pass 127.0.0.1:9000으로 같은 컨테이너의 PHP-FPM에 전달한다.

MNG 내부 Nginx (특수: 테넌트 스토리지)

disable_symlinks off; # 심볼릭 링크 허용 (Laravel storage:link) # 테넌트 파일 직접 서빙 (alias 사용) location /tenant-storage/ { alias /var/www/shared-storage/tenants/; expires 7d; add_header Cache-Control "public, immutable"; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_read_timeout 300; }

Sales / 5130 내부 Nginx (공통 패턴)

server { listen 80; root /var/www/sales; client_max_body_size 100M; location ~ /\. { deny all; # 숨김 파일(.env, .git) 접근 차단 } }

왜 2계층인가?

Docker 컨테이너는 격리된 환경이다. 외부 Nginx가 직접 컨테이너 안의 PHP-FPM에 접근하려면 복잡한 네트워크 설정이 필요하다. 내부 Nginx를 두면 각 컨테이너가 자체적으로 요청을 처리할 수 있어 관리가 쉽다. MNG의 테넌트 스토리지 alias처럼 컨테이너 고유의 설정도 독립적으로 관리할 수 있다.

{{-- ============================================================ --}} {{-- 5. 보안 설정 --}} {{-- ============================================================ --}}

5 보안 설정

SSL/TLS 암호화

SSL/TLS 봉투 자물쇠 비유

편지를 봉투에 넣고 자물쇠로 잠그는 것 = SSL

비유: 편지 봉투와 자물쇠

HTTP는 엽서처럼 내용이 공개된 채 전달된다. 누구나 중간에 읽을 수 있다.
HTTPS는 편지를 봉투에 넣고 자물쇠(SSL)로 잠근 것이다. 열쇠를 가진 수신자만 열 수 있다.

# SAM SSL 설정 (외부 Nginx) ssl_certificate /etc/nginx/ssl/sam.kr.crt; # 와일드카드 인증서 ssl_certificate_key /etc/nginx/ssl/sam.kr.key; # 개인키 ssl_protocols TLSv1.2 TLSv1.3; # 최신 프로토콜만 ssl_ciphers HIGH:!aNULL:!MD5; # 강력한 암호화만

SAM의 SSL 구조

*.sam.kr 와일드카드 인증서 하나로 모든 서브도메인을 커버한다. SSL 종료(termination)는 외부 Nginx에서만 이루어지고, 내부 통신은 평문 HTTP를 사용한다. Docker 내부 네트워크는 격리되어 있으므로 안전하다.

보안 필터링

보안 필터 입장 불가 명단

수상한 요청은 경비원이 차단

경로 트래버설 & 민감 파일 차단 (API 서버)

# 악의적 경로 접근 차단 if ($request_uri ~* "(\.\.\/|\.\.\\|etc\/passwd|\.env|\.git|\.htaccess|\.sql|@fs\/)") { return 403; }

../ 경로 트래버설

상위 디렉토리 접근 시도

.env 환경 파일

DB 비밀번호 등 노출 위험

.git 소스 코드

전체 코드 히스토리 노출

etc/passwd 시스템

서버 사용자 정보 노출

해킹 도구 User-Agent 차단

# 자동 스캔 도구 차단 if ($http_user_agent ~* "(sqlmap|nikto|nmap|masscan|metasploit|nessus)") { return 403; }

숨김 파일 차단 (Sales/5130 내부 Nginx)

location ~ /\. { deny all; # .env, .git, .htaccess 등 모두 차단 }

보안 헤더

SAM의 Sales 서버에는 다음 보안 헤더가 적용되어 있다. 브라우저에게 "이렇게 동작하라"고 지시하는 보안 규칙이다.

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; add_header X-Frame-Options SAMEORIGIN; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block";
헤더 역할 비유
HSTS항상 HTTPS 강제"이 건물은 정문으로만 출입"
X-Frame-Optionsiframe 삽입 방지"내 간판을 남의 가게에 걸지 마"
X-Content-TypeMIME 타입 고정"라벨 대로만 내용물 취급"
X-XSS-ProtectionXSS 공격 차단"수상한 스크립트 자동 격리"
{{-- ============================================================ --}} {{-- 6. 성능 최적화 --}} {{-- ============================================================ --}}

6 성능 최적화

정적 자산 캐싱

정적 자산 캐싱 비유

캐시는 자주 쓰는 서류를 책상 위에 두는 것

비유: 냉장고에 자주 먹는 반찬 보관

매번 마트에 가서 반찬을 사오면 시간이 오래 걸린다(서버에서 매번 전송). 자주 먹는 반찬은 냉장고(브라우저 캐시)에 넣어두면 바로 꺼내먹을 수 있다. expires 30d는 "이 반찬은 30일간 신선하다"는 유통기한 표시다.

# 정적 자산 30일 캐싱 (API, MNG 공통) location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|map)$ { expires 30d; add_header Cache-Control "public"; access_log off; # 정적 파일은 로그 불필요 } # 테넌트 스토리지 7일 캐싱 (MNG) location ^~ /tenant-storage/ { expires 7d; add_header Cache-Control "public, immutable"; # 변경 안 됨 보장 }

immutable이란?

immutable은 "이 파일은 절대 변하지 않는다"는 선언이다. 브라우저가 "혹시 바뀌었나요?"라는 재검증 요청조차 보내지 않아 더 빠르다. 테넌트 파일(로고, 첨부파일)은 업로드 후 변경되지 않으므로 적합하다.

타임아웃 설정

서비스마다 처리 시간이 다르므로 타임아웃도 다르게 설정한다. 타임아웃이 너무 짧으면 정상 요청도 끊기고, 너무 길면 오류 상황에서 자원이 낭비된다.

서비스 설정 이유
Reactproxy_read_timeout86400s (24시간)HMR WebSocket 연결 유지
MNGfastcgi_read_timeout300s (5분)대량 데이터 처리, 엑셀 내보내기
Salesproxy_read_timeout60s (1분)일반 웹 페이지 응답
5130-기본값레거시 (별도 설정 없음)

WebSocket과 HTTP/2

WebSocket: React(HMR)과 Sales에서 사용. Upgrade: websocket 헤더로 HTTP 연결을 WebSocket으로 전환한다.
HTTP/2: Sales와 5130에서 활성화 (http2 on). 하나의 TCP 연결에 여러 요청을 동시에 전송하는 "한 통화에 여러 주문" 방식이다.

{{-- ============================================================ --}} {{-- 7. 트러블슈팅 & 명령어 사전 --}} {{-- ============================================================ --}}

7 트러블슈팅 & 명령어 사전

4대 Nginx 에러

502/504/403/413 에러 카드

Nginx에서 가장 흔한 4가지 에러와 원인

502 Bad Gateway

PHP-FPM이 응답하지 않음

원인

  • PHP-FPM 프로세스가 죽었거나 미시작
  • fastcgi_pass 주소/포트 불일치
  • PHP 코드에서 메모리 초과

해결

docker restart sam-api-1

504 Gateway Timeout

처리 시간 초과

원인

  • PHP 스크립트가 너무 오래 실행
  • DB 쿼리 무한 대기
  • 타임아웃 설정이 너무 짧음

해결

fastcgi_read_timeout 값 확인

403 Forbidden

접근 권한 없음

원인

  • 파일/디렉토리 권한 부족
  • 보안 필터에 의해 차단
  • directory index 설정 없음

해결

파일 권한 확인: ls -la

413 Request Entity Too Large

업로드 크기 초과

원인

  • 업로드 파일이 client_max_body_size 초과
  • SAM 기본값: 100MB

해결

client_max_body_size 200M;

진단 명령어 사전

# 설정 문법 검사 (가장 먼저 실행) nginx -t # syntax ok / failed docker exec sam-nginx-1 nginx -t # Docker 환경에서 # 전체 설정 출력 (include 파일 포함) nginx -T # 에러 로그 실시간 모니터링 tail -f /var/log/nginx/error.log docker logs -f sam-nginx-1 # Docker 환경에서 # 엑세스 로그 확인 tail -100 /var/log/nginx/access.log # Nginx 프로세스 상태 ps aux | grep nginx # 설정 리로드 (무중단) nginx -s reload docker exec sam-nginx-1 nginx -s reload # Docker 환경에서

로그 읽는 법

access_log: IP - 시간 "요청" 상태코드 바이트 "Referer" "User-Agent"

error_log: 시간 [레벨] PID#TID: *연결번호 에러메시지, client: IP, server: 도메인

레벨: emerg > alert > crit > error > warn > notice > info

FAQ

Q1. 502 Bad Gateway가 발생했다. 어떻게 해야 하나?

1. docker ps로 컨테이너 실행 상태 확인

2. docker logs sam-api-1로 PHP-FPM 에러 확인

3. PHP-FPM이 죽었다면 docker restart sam-api-1

4. 반복된다면 PHP 메모리 제한(memory_limit) 확인

Q2. 이미지가 표시되지 않는다. (깨진 이미지)

1. 브라우저 개발자 도구 → Network 탭에서 이미지 URL과 응답 코드 확인

2. 403이면 파일 권한 문제 → ls -la로 확인

3. 404면 경로 문제 → storage:link 심볼릭 링크 확인

4. MNG 테넌트 이미지면 /tenant-storage/ 프록시 설정 확인

Q3. HTTPS 인증서 오류가 나타난다.

1. 인증서 만료 확인: openssl x509 -in sam.kr.crt -noout -dates

2. 와일드카드 인증서가 해당 도메인을 커버하는지 확인

3. 인증서 파일 경로가 /etc/nginx/ssl/에 올바르게 마운트되었는지 확인

4. nginx -t로 설정 문법 오류 확인

Q4. 파일 업로드가 실패한다. (413 에러)

1. Nginx client_max_body_size 확인 (SAM 기본값: 100M)

2. PHP upload_max_filesizepost_max_size 확인

3. 외부 Nginx와 내부 Nginx 모두에서 크기 제한을 확인해야 함

4. Laravel의 validation 규칙에서도 파일 크기 제한 확인

Q5. Nginx 설정을 변경했는데 반영이 안 된다.

1. nginx -t로 문법 오류 확인 (오류 시 리로드 실패)

2. nginx -s reload로 설정 리로드

3. Docker 볼륨 마운트 확인 (docker-compose.yml에서 설정 파일 경로)

4. 브라우저 캐시 때문에 안 보일 수 있음 → Ctrl+Shift+R (강력 새로고침)

Nginx 핵심 정리 인포그래픽

Nginx의 6가지 핵심 역할 — SAM 프로젝트의 수문장

핵심 정리

Nginx는 SAM 프로젝트의 "수문장"이다. 외부에서 들어오는 모든 요청을 받아 SSL 암호화를 처리하고, 5개 도메인을 각각의 서비스로 안내하며, 수상한 요청을 걸러내고, 정적 파일을 빠르게 서빙한다. 문제가 생기면 nginx -t로 설정을 검사하고, error.log를 확인하는 것이 첫 단계다.

@include('components.academy-glossary', ['domain' => 'nginx-encyclopedia']) @endsection