← posts/b.log()

blog92@web:~$ cat posts/nginx-caddy-02-nginx-basics.md

DEVOPS6 min read

Nginx & Caddy 완전 정복 2편 — Nginx 기초: 아키텍처와 설정 파일 읽는 법

Nginx 설치, 마스터/워커 이벤트 기반 아키텍처, nginx.conf의 문법과 컨텍스트 구조, 정적 파일 서빙과 필수 명령어를 다룹니다.

1. Nginx는 왜 빠른가: 이벤트 기반 아키텍처

Nginx가 등장한 배경에는 C10K 문제(동시 접속 1만 개를 어떻게 처리할까)가 있습니다. 당시 주류였던 Apache(prefork/worker MPM)는 연결 하나당 프로세스나 스레드 하나를 배정했습니다. 동시 접속이 늘면 메모리와 컨텍스트 스위칭 비용이 폭증합니다.

Nginx는 다른 길을 택했습니다.

text
                ┌──────────────────┐
                │  master process  │  ← root 권한, 설정 로드, 포트 바인딩, 워커 관리
                └────────┬─────────┘
          ┌──────────────┼──────────────┐
   ┌──────▼─────┐ ┌──────▼─────┐ ┌──────▼─────┐
   │  worker 1  │ │  worker 2  │ │  worker N  │  ← 일반 권한, 실제 요청 처리
   │ (event loop)│ │ (event loop)│ │ (event loop)│
   └────────────┘ └────────────┘ └────────────┘
     수천 개 연결     수천 개 연결     수천 개 연결
  • 마스터 프로세스: 설정 파일을 읽고, 80/443 포트를 열고, 워커를 생성·관리합니다. 요청은 직접 처리하지 않습니다.
  • 워커 프로세스: 보통 CPU 코어 수만큼 띄웁니다. 각 워커는 싱글 스레드 이벤트 루프로 epoll(Linux)/kqueue(BSD, macOS) 같은 OS의 I/O 멀티플렉싱을 이용해 수천 개의 연결을 동시에 처리합니다.
  • 논블로킹 I/O: 어떤 연결이 데이터를 기다리는 동안 워커는 멈추지 않고 다른 연결을 처리합니다.

Node.js의 이벤트 루프를 아는 분이라면 익숙한 모델입니다. 차이는 Nginx가 이것을 C로, 여러 프로세스에 걸쳐 수행한다는 점입니다.

이 구조 덕분에 가능한 기능이 하나 더 있습니다. 무중단 설정 리로드입니다. nginx -s reload를 하면 마스터가 새 설정으로 새 워커를 띄우고, 기존 워커는 처리 중인 요청을 마무리한 뒤 종료(graceful shutdown)합니다.

그 외에도 캐시를 관리하는 cache manager, cache loader 프로세스가 있습니다. 4편 캐싱에서 다시 만납니다.

2. 설치

Ubuntu/Debian

배포판 기본 저장소의 패키지는 버전이 다소 오래될 수 있습니다. 최신 버전이 필요하면 nginx.org 공식 저장소를 등록하는 것을 권장합니다.

bash
# 간단하게: 배포판 패키지
sudo apt update
sudo apt install nginx
 
# 공식 저장소 (stable/mainline 선택 가능)
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
  | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \
  | sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update && sudo apt install nginx

stable vs mainline: Nginx는 짝수 마이너 버전(예: 1.28.x)이 stable, 홀수(1.29.x)가 mainline입니다. 이름과 달리 mainline도 충분히 안정적이며, 공식적으로는 새 기능과 버그 수정이 먼저 들어가는 mainline 사용을 권장합니다.

macOS

bash
brew install nginx
# 설정 파일: /opt/homebrew/etc/nginx/nginx.conf (Apple Silicon 기준)

Docker

bash
docker run -d --name web -p 8080:80 \
  -v $(pwd)/html:/usr/share/nginx/html:ro \
  nginx:stable-alpine

설치 확인

bash
nginx -v          # 버전
nginx -V          # 버전 + 컴파일 옵션 + 포함된 모듈 (중요!)
systemctl status nginx
curl -I localhost

nginx -V 출력의 --with-http_v2_module, --with-http_ssl_module 같은 항목은 어떤 기능이 컴파일되어 있는지 알려줍니다. Nginx는 대부분의 기능이 빌드 시점에 결정된다는 점을 기억해 두세요.

3. 디렉터리 구조

배포판마다 조금 다르지만, Debian/Ubuntu 계열 기준으로는 다음과 같습니다.

text
/etc/nginx/
├── nginx.conf              # 메인 설정 파일 (진입점)
├── mime.types              # 확장자 → Content-Type 매핑
├── conf.d/                 # *.conf 파일이 자동 include 됨 (공식 패키지 방식)
│   └── default.conf
├── sites-available/        # 사이트별 설정 보관 (Debian 계열 관례)
├── sites-enabled/          # 활성화할 사이트의 심볼릭 링크
└── snippets/               # 재사용 조각
/var/log/nginx/
├── access.log
└── error.log
/var/www/html/              # 기본 문서 루트 (공식 패키지는 /usr/share/nginx/html)

sites-available / sites-enabled는 Nginx 자체 기능이 아니라 Debian의 관례입니다. nginx.conf 안에 include /etc/nginx/sites-enabled/*;가 있기 때문에 동작합니다. 공식 nginx.org 패키지는 conf.d/*.conf만 씁니다. 어느 쪽이든 한 가지 방식으로 통일하는 게 좋습니다.

bash
# Debian 방식으로 사이트 활성화
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

4. 설정 문법: 지시어와 컨텍스트

Nginx 설정은 딱 두 가지 요소로 이루어집니다.

  1. 단순 지시어(simple directive): 이름 값; — 세미콜론으로 끝납니다.
  2. 블록 지시어(block directive): 이름 { ... } — 중괄호 안에 다른 지시어를 담습니다. 블록이 다른 지시어를 담으면 컨텍스트(context)라고 부릅니다.
nginx
# ─── main 컨텍스트 (최상위) ───
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /run/nginx.pid;
 
events {                       # ─── events 컨텍스트 ───
    worker_connections  1024;
}
 
http {                         # ─── http 컨텍스트 ───
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
 
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
 
    sendfile        on;
    keepalive_timeout  65;
 
    server {                   # ─── server 컨텍스트 (가상 호스트) ───
        listen       80;
        server_name  localhost;
 
        location / {           # ─── location 컨텍스트 ───
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
    }
 
    include /etc/nginx/conf.d/*.conf;
}

컨텍스트 계층을 그림으로 보면 이렇습니다.

text
main
├── events
├── http
│   ├── upstream
│   └── server
│       └── location
│           └── location (중첩 가능)
└── stream          (L4 프록시, 4편)
    └── server

상속 규칙: 바깥에서 안으로

대부분의 지시어는 바깥 컨텍스트에서 선언하면 안쪽이 물려받고, 안쪽에서 다시 선언하면 덮어씁니다.

nginx
http {
    gzip on;                   # 모든 server가 상속
 
    server {
        root /var/www/site;    # 이 server의 모든 location이 상속
 
        location /legacy/ {
            gzip off;          # 여기서만 덮어씀
        }
    }
}

⚠️ 단, 배열형 지시어(add_header, proxy_set_header 등)는 함정이 있습니다. 안쪽에서 하나라도 선언하면 바깥 것을 전부 상속받지 않습니다. 4편 보안 헤더 파트에서 자세히 다룹니다.

각 지시어는 정해진 컨텍스트에서만 쓸 수 있다

공식 문서의 각 지시어 항목에는 Context: http, server, location 같은 표시가 있습니다. 엉뚱한 곳에 쓰면 nginx -t에서 "xxx" directive is not allowed here 에러가 납니다. 처음엔 이 에러를 자주 보게 되는데, 문서의 Context 항목만 확인하면 바로 해결됩니다.

변수

Nginx에는 $로 시작하는 내장 변수가 많습니다. 자주 쓰는 것만 추려보면:

변수의미
$host요청의 Host 헤더 (없으면 server_name)
$uri정규화된 현재 URI (쿼리 스트링 제외)
$request_uri원본 URI 전체 (쿼리 스트링 포함, 디코딩 안 됨)
$args / $arg_name쿼리 스트링 전체 / 특정 파라미터
$remote_addr클라이언트 IP
$schemehttp 또는 https
$http_헤더이름임의의 요청 헤더 (예: $http_user_agent)
$status응답 상태 코드 (로그에서 사용)
$request_time요청 처리 시간 (초, ms 정밀도)

5. 첫 번째 사이트: 정적 파일 서빙

/etc/nginx/conf.d/mysite.conf를 만들어 봅시다.

nginx
server {
    listen 80;
    server_name mysite.local;
 
    root  /var/www/mysite;
    index index.html;
 
    access_log /var/log/nginx/mysite.access.log;
    error_log  /var/log/nginx/mysite.error.log warn;
 
    location / {
        try_files $uri $uri/ =404;
    }
 
    # 정적 자산은 브라우저 캐시를 길게
    location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
 
    error_page 404 /404.html;
}
bash
sudo mkdir -p /var/www/mysite
echo '<h1>Hello Nginx</h1>' | sudo tee /var/www/mysite/index.html
sudo nginx -t && sudo systemctl reload nginx
curl -H "Host: mysite.local" http://localhost

root vs alias

초보자가 가장 많이 헷갈리는 부분입니다.

nginx
# root: location 경로를 "붙여서" 찾는다
location /images/ {
    root /data;
}
# /images/cat.png → /data/images/cat.png
 
# alias: location 경로를 "대체해서" 찾는다
location /images/ {
    alias /data/pictures/;
}
# /images/cat.png → /data/pictures/cat.png

alias를 쓸 때는 location과 alias 양쪽의 끝 슬래시를 맞추는 것이 중요합니다. location /images + alias /data/pictures/ 같은 불일치는 경로 조작(path traversal) 취약점의 원인이 되기도 합니다.

try_files: 순서대로 시도하기

nginx
try_files $uri $uri/ /index.html;
  1. $uri 파일이 있으면 그것을 응답
  2. 없으면 $uri/ 디렉터리(그 안의 index)를 시도
  3. 그래도 없으면 마지막 인자로 내부 리다이렉트

마지막 인자는 URI(/index.html)나 상태 코드(=404), 또는 이름 붙은 location(@backend)이 될 수 있습니다. React/Vue 같은 SPA는 위처럼 모든 경로를 index.html로 보내야 클라이언트 라우팅이 동작합니다.

6. 반드시 알아야 할 명령어

bash
sudo nginx -t                # 설정 문법 검사 (리로드 전에 무조건!)
sudo nginx -T                # 검사 + include된 것까지 합친 전체 설정 출력
sudo nginx -s reload         # 무중단 설정 리로드 (= systemctl reload nginx)
sudo nginx -s quit           # 처리 중인 요청을 마치고 종료 (graceful)
sudo nginx -s stop           # 즉시 종료
sudo nginx -s reopen         # 로그 파일 다시 열기 (logrotate 후)

습관처럼 쓰세요.

bash
sudo nginx -t && sudo systemctl reload nginx

nginx -t가 실패하면 리로드되지 않으므로, 오타 하나로 서비스 전체가 내려가는 사고를 막을 수 있습니다. 참고로 reload 자체도 새 설정이 잘못되면 기존 워커를 유지하지만, restart는 그렇지 않습니다. 운영 중에는 restart보다 reload입니다.

nginx -T는 트러블슈팅의 일등 공신입니다. include가 여러 겹이면 "도대체 어떤 설정이 적용되는 거지?"가 헷갈리는데, 이 명령 하나로 최종 설정을 한 화면에서 볼 수 있습니다.

7. 로그 읽기

bash
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log

기본 combined 포맷의 access log 한 줄:

text
203.0.113.7 - - [06/Oct/2026:10:15:32 +0900] "GET /index.html HTTP/1.1" 200 612 "-" "curl/8.5.0"

클라이언트IP - 사용자 [시간] "요청라인" 상태코드 바이트 "리퍼러" "UA" 순서입니다.

error log의 레벨은 debug, info, notice, warn, error, crit, alert, emerg 순으로, 지정한 레벨 이상만 기록됩니다. 문제를 찾을 때는 잠시 info로 낮춰보면 도움이 됩니다.

자주 만나는 에러:

에러원인
bind() to 0.0.0.0:80 failed (98: Address already in use)이미 다른 프로세스(Apache, 다른 nginx)가 포트 사용 중 → sudo ss -tlnp | grep :80
open() "/var/www/..." failed (13: Permission denied)워커 실행 유저(nginx/www-data)가 파일 또는 상위 디렉터리에 접근 권한이 없음. SELinux 환경이면 컨텍스트도 확인
directive is not allowed here지시어를 허용되지 않는 컨텍스트에 작성
unknown directive "xxx"오타거나 해당 모듈이 컴파일되지 않음 → nginx -V 확인

정리

  • Nginx는 마스터/워커 + 이벤트 루프 구조로 적은 자원으로 많은 연결을 처리하고, 무중단 리로드를 지원한다.
  • 설정은 "지시어"와 "컨텍스트"의 계층 구조이며, 바깥에서 안쪽으로 상속된다.
  • root/alias, try_files는 정적 파일 서빙의 핵심이다.
  • nginx -t && reload는 몸에 익혀야 할 습관이고, nginx -T는 최고의 디버깅 도구다.

다음 편에서는 Nginx의 진짜 주력 기능인 location 매칭 규칙, 리버스 프록시, 로드 밸런싱, HTTPS 설정을 다룹니다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..