본문으로 건너뛰기

Nginx 무중단 리로드 — nginx -s reload 원리와 배포 자동화

Nginx의 가장 강력한 기능 중 하나는 서비스 중단 없이 설정을 다시 적용할 수 있다는 점입니다. nginx -s reload 명령 하나로 수만 개의 동시 연결을 유지한 채 설정 변경을 반영할 수 있습니다. 이 챕터에서는 Nginx 리로드의 내부 동작 원리부터 배포 파이프라인 자동화까지 완전히 다룹니다.

nginx -s reload 동작 원리

Master/Worker 프로세스 아키텍처

Nginx는 하나의 Master 프로세스와 여러 Worker 프로세스로 구성됩니다.

┌─────────────────────────────────────────────────────────────────┐
│ Nginx 프로세스 구조 │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Master Process │ │
│ │ - 설정 파일 읽기 및 검증 │ │
│ │ - Worker 프로세스 생성/관리 │ │
│ │ - 시그널 처리 (HUP, QUIT, TERM 등) │ │
│ │ - PID: /var/run/nginx.pid │ │
│ └────────────────┬─────────────────────────┘ │
│ │ fork() │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ Worker 1 Worker 2 Worker N │
│ (요청 처리) (요청 처리) (요청 처리) │
└─────────────────────────────────────────────────────────────────┘

Master 프로세스는 root 권한으로 실행되며 설정 파일을 파싱하고, Worker 프로세스를 관리합니다. 실제 클라이언트 요청은 처리하지 않습니다.

Worker 프로세스는 실제 HTTP 요청을 처리합니다. 각 Worker는 싱글 스레드로 이벤트 루프(epoll/kqueue)를 기반으로 수천 개의 동시 연결을 비동기로 처리합니다.

리로드 시 내부 동작 흐름

nginx -s reload 실행


Master 프로세스에 HUP 시그널 전송


Master: 새로운 설정 파일 읽기 및 검증

┌────┴────┐
│ 검증 실패│ → 에러 로그 기록, 현재 Workers 유지 (서비스 중단 없음)
│ 검증 성공│
└────┬────┘


Master: 새로운 설정으로 새 Worker 프로세스 생성


새 Workers: 새 연결 수락 시작


Master: 기존 Workers에게 Graceful Shutdown 시그널 전송


기존 Workers: 진행 중인 요청 완료 후 종료


완료: 새 Workers만 운영

이 과정에서 클라이언트 입장에서는 단 한 건의 요청도 끊기지 않습니다. 기존 연결은 기존 Worker가 완료하고, 새 연결은 새 Worker가 처리합니다.

시그널을 통한 직접 제어

nginx -s reload는 내부적으로 kill -HUP $(cat /var/run/nginx.pid) 와 동일합니다.

# nginx.pid 파일에서 Master PID 확인
cat /var/run/nginx.pid
# 예: 1234

# 직접 시그널 전송 (nginx -s reload와 동일)
kill -HUP 1234

# 또는 nginx -s 명령어 사용
nginx -s reload # HUP 시그널 (무중단 리로드)
nginx -s quit # QUIT 시그널 (Graceful 종료)
nginx -s stop # TERM 시그널 (즉시 종료)
nginx -s reopen # USR1 시그널 (로그 파일 재오픈)

nginx -t 설정 검증

리로드 전 반드시 nginx -t로 설정 문법을 검증해야 합니다. 잘못된 설정으로 리로드하면 Nginx가 오류 상태에 빠질 수 있습니다.

# 기본 문법 검증
nginx -t

# 예시 성공 출력:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# 예시 실패 출력:
# nginx: [emerg] unexpected ";" in /etc/nginx/conf.d/app.conf:15
# nginx: configuration file /etc/nginx/nginx.conf test failed

# 상세 출력 (모든 검증 과정 표시)
nginx -T

# 특정 설정 파일로 검증
nginx -t -c /path/to/custom/nginx.conf

일반적인 설정 오류 유형

# 1. 세미콜론 누락
server_name example.com # 오류: 세미콜론 없음
server_name example.com; # 정상

# 2. 중괄호 불일치
server {
listen 80;
# 오류: 닫는 중괄호 누락

# 3. 잘못된 지시자
proxiy_pass http://backend; # 오류: 오타 (proxiy)
proxy_pass http://backend; # 정상

# 4. upstream 서버 미정의
proxy_pass http://undefined_upstream; # 오류: upstream 블록 없음

Upstream 동적 변경 패턴

배포 시 upstream 서버 목록을 변경하는 가장 안전한 방법은 심볼릭 링크를 활용하는 것입니다.

심볼릭 링크를 이용한 원자적 교체

# 디렉토리 구조
/etc/nginx/
├── conf.d/
│ ├── upstream_v1.conf # v1 upstream 설정
│ ├── upstream_v2.conf # v2 upstream 설정
│ └── upstream.conf → upstream_v1.conf # 심볼릭 링크 (현재 활성)
└── nginx.conf # include conf.d/upstream.conf;
# upstream_v1.conf
upstream app_backend {
server 192.168.1.20:8080;
server 192.168.1.21:8080;
}

# upstream_v2.conf
upstream app_backend {
server 192.168.1.30:8080;
server 192.168.1.31:8080;
}
# 원자적 심볼릭 링크 교체 (ln -sfn은 원자적 연산)
ln -sfn /etc/nginx/conf.d/upstream_v2.conf /etc/nginx/conf.d/upstream.conf

# 검증 후 리로드
nginx -t && nginx -s reload

ln -sfn은 원자적(atomic) 연산이므로, 교체 도중 요청이 들어와도 항상 완전한 설정 파일을 참조합니다.

배포 자동화 스크립트

deploy-nginx.sh — 완전한 배포 파이프라인

#!/bin/bash
# deploy-nginx.sh
# 사용법: ./deploy-nginx.sh <new_upstream_conf>

set -euo pipefail # 오류 발생 시 즉시 종료

NGINX_CONF_DIR="/etc/nginx/conf.d"
UPSTREAM_SYMLINK="$NGINX_CONF_DIR/upstream.conf"
NEW_CONF=$1
BACKUP_CONF="${UPSTREAM_SYMLINK}.bak"
LOG_FILE="/var/log/nginx-deploy.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

log() {
echo "[$TIMESTAMP] $1" | tee -a "$LOG_FILE"
}

# 사전 검사
if [ -z "$NEW_CONF" ] || [ ! -f "$NEW_CONF" ]; then
echo "Usage: $0 <new_upstream_conf_path>"
exit 1
fi

log "Starting deployment with config: $NEW_CONF"

# 1. 현재 설정 백업 (롤백용)
if [ -L "$UPSTREAM_SYMLINK" ]; then
CURRENT_CONF=$(readlink "$UPSTREAM_SYMLINK")
log "Current config: $CURRENT_CONF"
ln -sfn "$CURRENT_CONF" "$BACKUP_CONF"
fi

# 2. 새 설정으로 심볼릭 링크 교체
log "Updating symlink to $NEW_CONF..."
ln -sfn "$NEW_CONF" "$UPSTREAM_SYMLINK"

# 3. 설정 검증
log "Validating nginx configuration..."
if ! nginx -t 2>&1 | tee -a "$LOG_FILE"; then
log "ERROR: Nginx configuration test failed. Rolling back..."
# 롤백: 이전 설정 복원
if [ -L "$BACKUP_CONF" ]; then
ln -sfn "$(readlink $BACKUP_CONF)" "$UPSTREAM_SYMLINK"
rm -f "$BACKUP_CONF"
fi
exit 1
fi

# 4. Nginx 리로드
log "Reloading Nginx..."
if nginx -s reload; then
log "Nginx reloaded successfully"
else
log "ERROR: Nginx reload failed. Rolling back..."
if [ -L "$BACKUP_CONF" ]; then
ln -sfn "$(readlink $BACKUP_CONF)" "$UPSTREAM_SYMLINK"
nginx -s reload
fi
exit 1
fi

# 5. 헬스체크 (리로드 후 서비스 정상 확인)
sleep 2
log "Running post-reload health check..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 5 http://127.0.0.1/health 2>/dev/null)

if [ "$HTTP_CODE" = "200" ]; then
log "Health check passed (HTTP 200)"
rm -f "$BACKUP_CONF"
log "Deployment completed successfully!"
else
log "WARNING: Health check returned HTTP $HTTP_CODE"
log "Rolling back..."
if [ -L "$BACKUP_CONF" ]; then
ln -sfn "$(readlink $BACKUP_CONF)" "$UPSTREAM_SYMLINK"
nginx -s reload
log "Rolled back to previous configuration"
fi
exit 1
fi

리로드 실패 시 자동 롤백 스크립트

#!/bin/bash
# safe-nginx-reload.sh

NGINX_CONF_DIR="/etc/nginx/conf.d"
MAX_RETRIES=3
ROLLBACK_WAIT=5

safe_reload() {
local ATTEMPT=1

while [ $ATTEMPT -le $MAX_RETRIES ]; do
echo "Reload attempt $ATTEMPT of $MAX_RETRIES..."

# 설정 검증
if ! nginx -t &>/dev/null; then
echo "Config validation failed on attempt $ATTEMPT"
ATTEMPT=$((ATTEMPT + 1))
sleep $ROLLBACK_WAIT
continue
fi

# 리로드 실행
if nginx -s reload; then
sleep 2
# 리로드 후 헬스체크
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 5 http://127.0.0.1/health)
if [ "$HTTP_CODE" = "200" ]; then
echo "Reload successful!"
return 0
else
echo "Post-reload health check failed: HTTP $HTTP_CODE"
fi
fi

ATTEMPT=$((ATTEMPT + 1))
echo "Waiting ${ROLLBACK_WAIT}s before retry..."
sleep $ROLLBACK_WAIT
done

echo "All reload attempts failed. Manual intervention required."
return 1
}

safe_reload

Docker 환경에서의 Nginx 리로드

실행 중인 컨테이너에 리로드 신호 전송

# 방법 1: docker exec 사용
docker exec nginx_container nginx -s reload

# 방법 2: docker kill로 HUP 시그널 전송
docker kill --signal=HUP nginx_container

# 방법 3: docker compose에서
docker compose exec nginx nginx -s reload

# 설정 검증 후 리로드
docker exec nginx_container nginx -t && \
docker exec nginx_container nginx -s reload

Docker 볼륨을 통한 설정 업데이트

# docker-compose.yml
version: '3.8'
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
- "443:443"
# 호스트에서 설정 파일 변경 후 리로드
vim ./nginx/conf.d/upstream.conf

# 컨테이너 내에서 검증 후 리로드
docker exec nginx_container nginx -t && \
docker exec nginx_container nginx -s reload

GitHub Actions 워크플로우에서 Nginx 리로드 연동

# .github/workflows/deploy.yml
name: Deploy and Reload Nginx

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Build application
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} myapp:latest

- name: Deploy to server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# 1. 새 컨테이너 배포
docker pull myapp:${{ github.sha }}
docker stop app-green || true
docker rm app-green || true
docker run -d --name app-green -p 8081:8080 myapp:${{ github.sha }}

# 2. 헬스체크 대기
for i in {1..30}; do
HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/health)
[ "$HTTP" = "200" ] && break
echo "Waiting for app-green... attempt $i"
sleep 3
done

# 3. Nginx upstream 업데이트
cat > /etc/nginx/conf.d/upstream_new.conf << 'EOF'
upstream app_backend {
server 127.0.0.1:8081;
}
EOF

# 4. 설정 검증 및 리로드
nginx -t && nginx -s reload

# 5. 기존 컨테이너 중지
docker stop app-blue || true
docker rm app-blue || true
docker rename app-green app-blue

echo "Deployment completed: ${{ github.sha }}"

- name: Notify on success
if: success()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H "Content-type: application/json" \
-d '{"text": "Deployment successful: ${{ github.sha }}"}'

- name: Notify on failure
if: failure()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H "Content-type: application/json" \
-d '{"text": "Deployment FAILED: ${{ github.sha }}"}'

nginx_pid 기반 시그널 전송 방법

# nginx.pid 위치 확인 (배포판마다 다를 수 있음)
nginx -V 2>&1 | grep pid # --pid-path 옵션 확인

# 일반적인 위치들
/var/run/nginx.pid # Ubuntu/Debian
/run/nginx.pid # 최신 systemd 기반 배포판
/usr/local/nginx/logs/nginx.pid # 소스 컴파일 설치

# PID 기반 직접 시그널
NGINX_PID=$(cat /var/run/nginx.pid)
kill -HUP $NGINX_PID # 무중단 리로드
kill -QUIT $NGINX_PID # Graceful 종료
kill -TERM $NGINX_PID # 즉시 종료
kill -USR1 $NGINX_PID # 로그 파일 재오픈

# Master PID와 Worker PID 모두 확인
ps aux | grep nginx
# 출력 예:
# root 1234 0.0 0.0 ... nginx: master process
# www-data 1235 0.1 0.2 ... nginx: worker process
# www-data 1236 0.1 0.2 ... nginx: worker process

리로드 전후 동작 검증

#!/bin/bash
# verify-reload.sh — 리로드 전후 서비스 연속성 검증

TARGET_URL="http://localhost/health"
DURATION=30 # 테스트 시간 (초)
INTERVAL=0.5 # 요청 간격 (초)
SUCCESS=0
FAILED=0

echo "Starting continuous request test for ${DURATION} seconds..."

# 백그라운드에서 연속 요청
start_time=$(date +%s)
while true; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
[ $elapsed -ge $DURATION ] && break

HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--max-time 2 "$TARGET_URL" 2>/dev/null)

if [ "$HTTP_CODE" = "200" ]; then
SUCCESS=$((SUCCESS + 1))
else
FAILED=$((FAILED + 1))
echo "[$(date)] FAILED: HTTP $HTTP_CODE"
fi

sleep $INTERVAL
done

TOTAL=$((SUCCESS + FAILED))
echo ""
echo "=== Test Results ==="
echo "Total requests: $TOTAL"
echo "Success: $SUCCESS"
echo "Failed: $FAILED"
echo "Success rate: $(echo "scale=2; $SUCCESS * 100 / $TOTAL" | bc)%"

실제 리로드를 수행하면서 이 스크립트를 동시에 실행하면 리로드 중 요청 손실 여부를 정확히 측정할 수 있습니다.

전문가 팁

  • nginx -s reload는 설정 파일이 유효할 때만 새 Workers를 생성합니다. 설정 오류가 있으면 기존 Workers가 계속 동작하므로 서비스는 유지됩니다.
  • 배포 자동화 스크립트에서 항상 nginx -t 검증을 리로드보다 먼저 실행하고, 실패 시 즉시 중단하세요.
  • 고트래픽 환경에서는 worker_shutdown_timeout 지시자로 기존 Worker의 graceful shutdown 타임아웃을 설정하세요.
  • systemd와 함께 사용할 때는 systemctl reload nginx를 사용하는 것이 더 안전합니다 (systemd가 프로세스 상태를 관리).
  • nginx -T(대문자)는 현재 적용된 전체 설정을 출력하므로 디버깅에 매우 유용합니다.