[V1] Nginx 설정 - 100-hours-a-week/KTB4-15th-CLOUD GitHub Wiki
Nginx 설정
1. default.conf
# /opt/lookddak/nginx/conf.d/default.conf
# nginx.conf의 http { } 안에 include되는 파일
# Docker DNS
resolver 127.0.0.11 valid=10s ipv6=off;
# IP별 요청 속도 제한
limit_req_zone $binary_remote_addr zone=api_ip:20m rate=20r/s;
limit_req_zone $binary_remote_addr zone=static_ip:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=web_ip:30m rate=30r/s;
# IP별 동시 요청 제한
limit_conn_zone $binary_remote_addr zone=api_conn_ip:10m;
limit_conn_zone $binary_remote_addr zone=web_conn_ip:10m;
# 모든 IP를 합친 API 전체 동시 요청 제한
limit_conn_zone $server_name zone=api_total:1m;
# 제한 초과 응답 코드
limit_req_status 429;
limit_conn_status 429;
# 응답시간 및 rate limit 결과 기록
log_format lookddak_timing
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'rt=$request_time urt=$upstream_response_time '
'uaddr="$upstream_addr" ustatus="$upstream_status" '
'req_limit="$limit_req_status" conn_limit="$limit_conn_status"';
# Next.js 정적 파일 캐시
proxy_cache_path /var/cache/nginx/next_static
levels=1:2
keys_zone=next_static:10m
max_size=1g
inactive=7d
use_temp_path=off;
# Spring upstream
upstream spring_backend {
zone spring_backend 64k;
server spring:8080 resolve;
keepalive 32;
keepalive_timeout 30s;
}
# Next.js upstream
upstream nextjs_frontend {
zone nextjs_frontend 64k;
server nextjs:3000 resolve;
keepalive 16;
keepalive_timeout 30s;
}
# HTTP
server {
listen 80;
server_name lookddak.com www.lookddak.com;
access_log /var/log/nginx/access.log lookddak_timing;
location = /healthz {
access_log off;
allow 127.0.0.1;
deny all;
default_type text/plain;
return 200 "ok";
}
# Let's Encrypt 인증 경로
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
# 워드프레스, 숨김 파일, 백업 파일 스캔 차단
location ~* (^/(wp-|xmlrpc\.php)|/\.(?!well-known/)|\.(bak|old|sql|swp)$) {
return 444;
}
location / {
return 301 https://lookddak.com$request_uri;
}
}
# www HTTPS → 기본 도메인 이동
server {
listen 443 ssl;
http2 on;
server_name www.lookddak.com;
access_log /var/log/nginx/access.log lookddak_timing;
ssl_certificate /etc/nginx/ssl/live/lookddak.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/lookddak.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
return 301 https://lookddak.com$request_uri;
}
# 실제 서비스
server {
listen 443 ssl;
http2 on;
server_name lookddak.com;
access_log /var/log/nginx/access.log lookddak_timing;
ssl_certificate /etc/nginx/ssl/live/lookddak.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/lookddak.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# 공통 프록시 설정
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
proxy_connect_timeout 3s;
# 스캐너 차단
location ~* (^/(wp-|xmlrpc\.php)|/\.(?!well-known/)|\.(bak|old|sql|swp)$) {
return 444;
}
# Next.js 정적 파일
location ^~ /_next/static/ {
# IP당 30r/s, 순간 초과분 100건
limit_req zone=static_ip burst=100 nodelay;
# IP당 동시 요청 최대 100개
limit_conn web_conn_ip 100;
# Nginx 디스크 캐시
proxy_cache next_static;
proxy_cache_methods GET HEAD;
proxy_cache_valid 200 7d;
# 같은 파일의 동시 MISS는 Next.js에 한 번만 요청
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
# HIT, MISS, BYPASS 등 캐시 상태 표시
add_header X-Cache-Status $upstream_cache_status always;
proxy_pass http://nextjs_frontend;
}
# Spring API
location /api/ {
client_max_body_size 10m;
# IP당 20r/s, 순간 초과분 40건
limit_req zone=api_ip burst=40 nodelay;
# IP 하나의 API 동시 요청 최대 50개
limit_conn api_conn_ip 50;
# 모든 IP를 합친 API 동시 요청 최대 100개
limit_conn api_total 100;
proxy_read_timeout 60s;
proxy_pass http://spring_backend;
}
# Next.js 페이지 및 기타 URL
location / {
# IP당 10r/s, 순간 초과분 30건
limit_req zone=web_ip burst=50 nodelay;
# IP당 동시 요청 최대 100개
limit_conn web_conn_ip 100;
proxy_pass http://nextjs_frontend;
}
}
| 적용 영역 | 요청 경로 | IP당 지속 속도 | 순간 허용량 | 순간 요청 처리 | IP당 동시 요청 | 전체 동시 요청 |
|---|---|---|---|---|---|---|
| 전체 공통 | 모든 경로 | 없음 | 없음 | 영역별 설정 사용 | 영역별 설정 사용 | API에만 존재 |
| Spring API | /api/ |
20건/초 | 40건 | 즉시 전달 | 50개 | 100개 |
| Next.js 정적 파일 | /_next/static/ |
30건/초 | 100건 | 즉시 전달 | 100개 | 없음 |
| Next.js 일반 요청 | 나머지 / |
30건/초 | 50건 | 즉시 전달 | 100개 | 없음 |
| 스캐너 차단 | /.env, /.git, /wp-*, *.sql 등 |
해당 없음 | 해당 없음 | 즉시 연결 종료 | 해당 없음 | 해당 없음 |
nginx.conf
user nginx;
worker_processes auto;
# 워커 하나가 열 수 있는 최대 FD 수 (docker-compose ulimits.nofile 과 같은 값으로)
worker_rlimit_nofile 65535;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events {
# 기존 1024 → 4096 (동접 1,000명 기준)
worker_connections 4096;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# nginx 버전 숨기기 (선택)
server_tokens off;
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;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
default.conf - 기존
server {
listen 80;
server_name lookddak.com www.lookddak.com;
location = /healthz {
access_log off;
allow 127.0.0.1;
deny all;
return 200 "ok";
}
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
location / {
return 301 https://lookddak.com$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name lookddak.com;
ssl_certificate /etc/nginx/ssl/live/lookddak.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/lookddak.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
resolver 127.0.0.11 valid=10s;
location /api/ {
client_max_body_size 10m;
set $spring_upstream spring:8080;
proxy_pass http://$spring_upstream;
proxy_http_version 1.1;
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;
}
location / {
set $nextjs_upstream nextjs:3000;
proxy_pass http://$nextjs_upstream;
proxy_http_version 1.1;
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;
}
}
default.conf - 변경
# ── server 443 블록 ──
location /_next/static/ {
# [추가] 정상 사용자는 burst=100 안에서 모두 통과, 공격은 초당 30건으로 제한.
limit_req zone=static_ip burst=100 nodelay;
limit_conn conn_ip 100;
# [추가] 한 번 받은 파일은 nginx가 저장해 두고 직접 응답 → Node까지 가지 않음.
proxy_cache next_static;
proxy_cache_valid 200 7d; # 정상 응답(200)만 7일 캐시. 404는 캐시하지 않음.
# [추가] 캐시에 없는 같은 파일 요청이 동시에 몰려도 Next.js에는 1번만 요청.
proxy_cache_lock on;
# [추가] 응답 헤더에 HIT/MISS를 표시 → 캐시가 작동하는지 브라우저 개발자 도구에서 확인용.
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://nextjs_frontend;
}
전체 한도는 "평소 최대 트래픽 < 한도 < 백엔드가 버티는 양" 사이에 있어야 합니다.
| 값 (어제 로그 기준) | |
|---|---|
| 평소 API 최대 | 초당 12건 |
| 평소 프론트 페이지·RSC 최대 | 초당 약 20건 |
| Spring이 버틴 양 | 11:00 |
# ── http 영역 (기존 zone들 아래에 추가) ──
# [추가] 전체 제한용 zone. 키가 $server_name이라 모든 요청이 카운터 하나를 공유함.
# 카운터가 1개뿐이라 공간은 1MB면 충분.
limit_req_zone $server_name zone=api_all:1m rate=150r/s; # Spring 다운 지점(약 300)의 절반
limit_req_zone $server_name zone=page_all:1m rate=100r/s; # Next.js 서버 렌더링 요청
limit_req_zone $server_name zone=img_all:1m rate=20r/s; # 이미지 리사이즈는 CPU를 많이 써서 낮게
# [추가] Spring에 동시에 처리를 맡기는 요청 수의 전체 상한.
limit_conn_zone $server_name zone=conn_all:1m;
-
req, res time 로그에 남도록 설정
$request_time $upstream_response_time $limit_req_status $limit_conn_status -
DDos(분산된 환경에서의 공격) 대비 전체 IP 허용 제한 거는 방식 추가 — 전체 Spring 동시 요청 제한
location /api/ { limit_conn conn_ip 50; limit_conn api_total 100; } -
그 외의 요청(무작위 url)에 대한 요청 제한 추가
Rate limit 설정 이유
1. Rate limit 알고리즘 종류
1. Fixed window
-
고정된 시간 구간마다 요청 수를 셈
10:00:00 ~ 10:00:59 → 최대 100회 10:01:00 ~ 10:01:59 → 다시 최대 100회 -
시간 경계에 허점 존재
10:00:59에 100회 10:01:00에 100회이 알고리즘 상 다른 1분에 각각 100번의 요청이지만, 실제로는 수십 초동안 요청을 몰아보냄
2. sliding window
현재 시각이 10:01:20이면
10:00:20 ~ 10:01:20 사이 요청 수를 계산
현재 시점 기준 바로 이전 일정 시간 계산
3. Token bucket
통의 최대 크기: 토큰 10개
초당 토큰 2개 충전
요청 1회: 토큰 1개 사용
토큰이 있으면 요청 처리, 없으면 거절 — 지속적인 대량 요청 차단
4. Leaky bucket
-
Nginx
limit_req가 공식적으로 사용하는 방식요청이 양동이에 들어옴 ↓ 정해진 속도로만 빠져나감 ↓ 양동이가 꽉 차면 추가 요청 거절 -
ex. 초당 2개 가정시
정상 요청 1~2개 → 통과 순간 요청 5개 → burst 범위라면 일부 허용 계속 초당 68개 → 허용 범위를 초과한 요청 거절https://nginx.org/en/docs/http/ngx_http_limit_req_module.html
2. Rate limit 설정
찰리가 보낸 공격은,
- 단일 IP에
- 특정 API 집중
- 짧은 시간 반복
- HTTP/2 병렬 요청 — 동시성 제한 필요
따라서, nginx 기본제공 leaky bucket(limit_req)를 활용 — IP별 초당 요청 횟수 제한 및 나머지 429 거절
이를 통해 Spring이 죽기전에 차단
2-1. burst 활용
Leaky Bucket을 너무 딱딱하게 적용하면 정상적인 페이지 로딩도 막을 수 있음
ex. 페이지를 열 때 브라우저가 동시에 여러 API를 호출
- burst는 ‘순간 폭주 최대치 설정’
3. 결론
1. 누구를 기준으로 셀지: IP
2. 지속 허용량: rate
3. 순간 허용량: burst
4. 동시 처리량: limit_conn
알고리즘
Nginx Leaky Bucket
지속 속도 rate limit
짧은 순간 요청
→ burst
동시 요청 설정
거절 응답
→ 429
최근 7일간의 API 요청을 확인하려고 했으나, 현재 nginx 로그가 다음 정보만 기록
IP
요청 시각
HTTP 메서드·경로
상태 코드
응답 크기
Referer
User-Agent
필요한 것:
$request_time
$upstream_response_time
구조
사용자
↓
Nginx
├─ /api/* → Spring Boot
├─ /_next/static/* → Next.js + Nginx 캐시
└─ 나머지 경로 → Next.js
-
spring boot
- IP당 지속적으로 초당 20건
- 순간 초과 요청 최대 40건
- IP당 동시 요청 최대 50건
- 전체 API 동시 요청 최대 100건
- 업로드 가능한 요청 본문 최대 10MB
- Spring 응답을 최대 60초까지 기다림
API는 DB 조회·저장과 비즈니스 로직을 수행하므로 Next.js 정적 파일보다 엄격하게 제한한 구조
운영 트래픽과 Spring의 Thread Pool, DB Connection Pool 크기를 측정한 후 조정 필요
-
Next.js 정적 파일
- IP당 초당 30건
- 순간 초과 요청 최대 100건
- IP당 동시 요청 최대 100개
- 응답을 Nginx 디스크에 7일간 캐싱
웹페이지 하나를 열 때 브라우저가 여러 JavaScript와 CSS 파일을 동시에 요청하기 때문에 API보다 순간 허용량을 크게 잡음
첫 번째 요청 → Next.js에서 파일을 가져와 Nginx에 저장 두 번째 요청 → Next.js까지 가지 않고 Nginx가 바로 응답*stampede 방지—
proxy_cache_lock on
캐시에 없는 동일 파일을 사용자 100명이 동시에 요청해도 Next.js에는 대표 요청 하나만 보내도록 막아줌 -
Next.js 일반 페이지
- IP당 초당 30건
- 순간 초과 요청 최대 50건
- IP당 동시 요청 최대 100개
-
upstream 연결 재사용
Nginx가 요청마다 Spring과 TCP 연결을 새로 만들지 않고 기존 연결을 재사용
TCP 연결을 매번 만드는 비용을 줄이는 설정
- Spring: 최대 32개의 유휴 연결 유지
- Next.js: 최대 16개의 유휴 연결 유지
- 사용하지 않는 연결은 30초 후 정리
-
스캐너 요청 차단
/wp-admin,/wp-login등 워드프레스 탐색/.env,/.git등 숨김 파일 탐색.sql,.bak,.old등 백업 파일 탐색