Docker로 Nginx 배포하기 완벽 가이드: 설정 파일 마운트, HTTPS 설정과 리버스 프록시 실전

일곱 번째로 docker restart nginx를 실행하고 브라우저를 새로고침했지만 여전히 502 오류 페이지가 나타납니다. nginx.conf를 분명히 수정했는데 컨테이너 안에서는 새 설정이 적용되지 않습니다.
Docker로 Nginx를 배포할 때 생기는 설정 문제는 컨테이너화를 해 본 사람이라면 거의 누구나 겪습니다. 설정 파일 마운트, HTTPS 인증서, 컨테이너 간 통신은 간단해 보이지만 실제로는 세세한 함정이 숨어 있습니다. 이 글에서는 설정 파일을 올바르게 마운트하는 방법부터 HTTPS 인증서 자동 갱신, 리버스 프록시 네트워크 설정까지 완전하고 바로 사용할 수 있는 Docker Nginx 배포 방안을 설명합니다.
이 글에서 다음 내용을 배울 수 있습니다.
- 설정 파일을 수정해도 적용되지 않는 이유와 올바른 5가지 마운트 방법
- 자체 서명 인증서부터 Let’s Encrypt까지 HTTPS 인증서를 설정하는 전체 과정
- 다른 Docker 컨테이너를 리버스 프록시할 때의 네트워크 설정 요령
- 프로덕션 환경의 보안 강화와 성능 튜닝 실전
Docker Nginx 기본 설정과 파일 마운트
설정 파일을 반드시 마운트해야 하는 이유
Nginx 설정을 이미지 안에 바로 넣으면 안 되냐고 생각할 수 있습니다. 물론 가능하지만, 그 과정은 꽤 번거롭습니다.
설정을 조금만 수정해도 이미지를 다시 빌드하고 레지스트리에 푸시한 뒤, 다시 받아 컨테이너를 재시작해야 합니다. 이 과정을 모두 거치면 십여 분이 금방 지나갑니다. 설정 버전 관리와 여러 환경 전환까지 고려하면 더 복잡해집니다. 새벽 2시에 프로덕션에서 문제가 생겨 설정을 빠르게 롤백해야 하는데 이미지 빌드가 끝날 때까지 기다려야 한다면 난감할 수밖에 없습니다.
설정 파일 마운트는 바로 이 문제를 해결합니다. 호스트의 파일을 수정하면 컨테이너 안에도 즉시 반영되므로 테스트, 롤백, 팀 협업이 훨씬 편해집니다.
Nginx 컨테이너의 주요 디렉터리 구조
먼저 Nginx 관련 파일이 컨테이너 안에서 어디에 있는지 살펴보겠습니다.
/etc/nginx/
├── nginx.conf # 기본 설정 파일
├── conf.d/ # 각 사이트의 하위 설정 파일 디렉터리
│ └── default.conf # 기본 사이트 설정
/usr/share/nginx/html # 정적 파일 루트 디렉터리
/var/log/nginx/ # 로그 디렉터리
├── access.log
└── error.log
여기서 핵심은 nginx.conf에 include /etc/nginx/conf.d/*.conf;라는 줄이 있다는 점입니다. 기본 설정은 conf.d 디렉터리 아래의 모든 .conf 파일을 자동으로 불러옵니다. nginx.conf만 마운트하고 conf.d를 빠뜨렸다면 첫 번째 함정에 빠진 것입니다.
올바른 마운트 방법: step by step
컨테이너를 바로 시작하지 말고 먼저 준비 작업을 해 두세요. 이후의 많은 문제를 줄일 수 있습니다.
1단계: 컨테이너의 기본 설정을 템플릿으로 복사
왜 이렇게 해야 할까요? 공식 기본 설정은 이미 검증되어 있으므로 처음부터 직접 작성하는 것보다 복사해서 수정하는 편이 안정적입니다.
# 임시 컨테이너 시작
docker run --name nginx-temp -d nginx
# 기본 설정 복사
docker cp nginx-temp:/etc/nginx/nginx.conf ./nginx/nginx.conf
docker cp nginx-temp:/etc/nginx/conf.d ./nginx/conf.d
# 사용을 마친 임시 컨테이너 삭제
docker stop nginx-temp && docker rm nginx-temp
2단계: 호스트에 표준 디렉터리 구조 생성
저는 보통 Docker 관련 파일을 모두 /opt 아래에 둡니다. 필요에 따라 원하는 경로로 바꿔도 됩니다.
mkdir -p /opt/nginx/{conf,conf.d,html,logs,ssl}
이 디렉터리 구조에는 설정, 정적 파일, 로그, SSL 인증서에 필요한 경로가 모두 포함됩니다. 한 번에 준비할 수 있습니다.
3단계: 모든 디렉터리를 마운트하여 컨테이너 시작
이제 핵심 단계입니다. 각 -v 옵션 뒤의 경로 매핑을 잘 살펴보세요.
docker run -d --name my-nginx \
-p 80:80 -p 443:443 \
-v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro \
-v /opt/nginx/conf.d:/etc/nginx/conf.d \
-v /opt/nginx/html:/usr/share/nginx/html \
-v /opt/nginx/logs:/var/log/nginx \
-v /opt/nginx/ssl:/etc/nginx/ssl \
nginx
몇 가지 핵심 사항은 다음과 같습니다.
-p 80:80 -p 443:443: HTTP와 HTTPS 포트를 모두 공개합니다. 뒤에서 HTTPS를 설정할 때 필요합니다.nginx.conf뒤에는:ro를 붙여 읽기 전용으로 마운트했습니다. 컨테이너 안의 프로세스가 실수로 설정을 수정하는 일을 막아 줍니다.conf.d는 단일 파일이 아니라 디렉터리 전체를 마운트했습니다. 이는 매우 중요하며 뒤에서 이유를 설명합니다.
흔히 빠지는 네 가지 함정
함정 1: vim으로 설정을 편집한 뒤 컨테이너에 동기화되지 않음
제가 처음 겪었던 문제입니다. 호스트의 nginx.conf를 수정하고 컨테이너를 재시작했는데도 아무 변화가 없었습니다.
원인은 vim이 파일을 편집할 때 inode 값을 바꿀 수 있기 때문입니다. Docker 마운트는 inode와 연결되어 있으므로 inode가 바뀌면 컨테이너 안에서는 여전히 이전 파일을 보게 됩니다.
해결 방법은 두 가지입니다.
- 방법 A: inode를 바꾸지 않는 nano 편집기 사용
- 방법 B: 단일 파일 대신 디렉터리 전체 마운트
저는 지금은 항상 방법 B를 사용합니다. 디렉터리를 마운트하면 파일의 inode가 바뀌어도 영향을 받지 않고, 설정 파일을 자유롭게 추가하거나 삭제할 수 있습니다.
함정 2: include 경로 설정 오류
컨테이너에 들어가 nginx.conf를 확인합니다.
docker exec -it my-nginx bash
cat /etc/nginx/nginx.conf | grep include
다음 줄이 있는지 확인하세요.
include /etc/nginx/conf.d/*.conf;
이 경로를 호스트 경로인 /opt/nginx/conf.d/*.conf로 바꿨다면 잘못된 설정입니다. 컨테이너 안에서는 컨테이너 내부 경로만 사용할 수 있습니다.
함정 3: conf.d 디렉터리 마운트를 빠뜨림
nginx.conf만 마운트해서는 충분하지 않습니다. 호스트의 conf.d 아래에 새 사이트 설정을 추가해도 컨테이너에서는 전혀 보이지 않습니다.
다음 명령으로 확인할 수 있습니다.
docker exec my-nginx ls /etc/nginx/conf.d
호스트의 /opt/nginx/conf.d 아래에 있는 모든 파일이 표시되어야 합니다.
함정 4: 파일 권한 때문에 Nginx가 설정을 읽지 못함
Linux에서 자주 생기는 문제입니다. 설정 파일 권한이 너무 엄격하면 Nginx 프로세스가 보통 nginx 사용자로 실행되기 때문에 파일을 읽지 못할 수 있습니다.
다음처럼 해결합니다.
chmod 644 /opt/nginx/conf/nginx.conf
chmod 644 /opt/nginx/conf.d/*.conf
설정을 수정한 뒤에는 구문을 검사하는 습관을 들이세요.
docker exec my-nginx nginx -t
syntax is ok가 표시되면 안심해도 됩니다.
단일 파일 마운트와 디렉터리 마운트 중 무엇을 선택해야 할까?
자주 받는 질문입니다. 제 권장 사항은 다음과 같습니다.
기본 설정 nginx.conf: 자주 바뀌지 않으므로 단일 파일로 마운트하고 읽기 전용 :ro를 추가해도 됩니다.
conf.d 디렉터리: 새 사이트 설정을 자유롭게 추가할 수 있도록 반드시 디렉터리 단위로 마운트합니다.
html, logs: 이 둘도 디렉터리 단위로 마운트합니다.
원칙은 간단합니다. 여러 파일을 유연하게 관리해야 하면 디렉터리를 마운트하고, 버전 관리가 필요한 핵심 설정 파일은 단일 파일로 마운트하되 읽기 전용으로 보호합니다.
HTTPS 설정과 Let’s Encrypt 자동 갱신
자체 서명 인증서: 개발 환경에서 빠르게 검증하기
프로덕션 환경에서는 정식 인증서를 사용해야 하지만 개발과 테스트 단계에서는 자체 서명 인증서로 충분합니다. 몇 줄의 명령으로 만들 수 있습니다.
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /opt/nginx/ssl/nginx.key \
-out /opt/nginx/ssl/nginx.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Dev/CN=localhost"
이 명령은 두 파일을 생성합니다.
nginx.key: 절대 유출하면 안 되는 개인 키nginx.crt: 인증서 파일
그런 다음 /opt/nginx/conf.d/ 아래에 ssl.conf를 만듭니다.
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
핵심 사항은 다음과 같습니다.
ssl_certificate경로는 호스트의/opt/nginx/ssl이 아니라 컨테이너 내부 경로인/etc/nginx/ssl입니다.ssl_protocols는 TLS 1.2와 1.3만 활성화합니다. 이전 프로토콜에는 보안 취약점이 있습니다.- 컨테이너를 시작할 때
-p 443:443를 빠뜨리지 마세요. 그렇지 않으면 HTTPS 포트가 공개되지 않습니다.
설정을 핫 리로드합니다.
docker exec my-nginx nginx -s reload
https://localhost에 접속하면 브라우저에서 인증서를 신뢰할 수 없다는 경고가 표시됩니다. 자체 서명 인증서에서는 정상적인 현상이므로 당황하지 말고 ‘계속 접속’을 선택하면 됩니다.
Let’s Encrypt: 프로덕션 환경용 무료 인증서
Let’s Encrypt는 무료이고 자동화할 수 있으며 전 세계에서 신뢰받습니다. 유일한 ‘단점’은 인증서가 90일 뒤 만료된다는 점이지만 자동 갱신을 설정하면 문제가 되지 않습니다.
여기서는 수동으로 처리하는 것보다 안정적인 docker-compose와 certbot 컨테이너 조합을 권장합니다.
먼저 전체 docker-compose.yml을 살펴보겠습니다.
version: '3'
services:
nginx:
image: nginx:latest
container_name: my-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/html:/usr/share/nginx/html
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
networks:
- web
certbot:
image: certbot/certbot
container_name: certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- web
networks:
web:
driver: bridge
몇 가지 핵심 사항을 살펴보겠습니다.
1. 인증서와 검증 파일의 공유 마운트
./certbot/conf를 두 컨테이너에 모두 마운트합니다. certbot은 인증서를 생성하고 nginx는 인증서를 읽습니다../certbot/www는 Let’s Encrypt의 HTTP 검증에 사용되는 webroot입니다.
2. Nginx를 6시간마다 자동 reload
이 명령은 복잡해 보이지만 실제로는 백그라운드 루프를 시작해 6시간마다 설정을 reload합니다. 따라서 인증서가 갱신되면 즉시 적용됩니다.
3. Certbot이 12시간마다 갱신 확인
Let’s Encrypt는 하루에 한 번 확인할 것을 권장하며, 여기서는 더 안전하게 12시간으로 설정했습니다.
최초 인증서 발급 단계
HTTP 검증을 위해 /opt/nginx/conf.d/ 아래에 임시 설정 파일 temp.conf를 먼저 만듭니다.
server {
listen 80;
server_name your-domain.com; # 실제 도메인으로 변경
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
서비스를 시작합니다.
docker-compose up -d
인증서를 신청합니다. your-domain.com과 [email protected]을 실제 값으로 바꾸세요.
docker-compose run --rm certbot certonly --webroot \
-w /var/www/certbot \
-d your-domain.com \
--email [email protected] \
--agree-tos \
--no-eff-email
정상적으로 완료되면 “Congratulations!”라는 문구가 표시됩니다. 인증서 파일은 ./certbot/conf/live/your-domain.com/ 디렉터리에 있습니다.
이제 정식 HTTPS 설정을 만들 수 있습니다. temp.conf를 다음과 같이 수정합니다.
server {
listen 80;
server_name your-domain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL 최적화 설정
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
# HSTS(HTTPS 강제)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
설정을 reload합니다.
docker-compose exec nginx nginx -s reload
자동 갱신 검증
Let’s Encrypt 인증서는 90일 동안 유효하지만 certbot은 만료까지 30일이 남았을 때 자동으로 갱신합니다. 다음 명령으로 직접 테스트할 수 있습니다.
docker-compose run --rm certbot renew --dry-run
“The dry run was successful”이 표시되면 정상입니다.
cron 로그를 확인해 갱신 작업이 제대로 실행되는지도 확인할 수 있습니다.
docker-compose logs certbot
HTTPS 설정의 추가 보안 옵션
SSL 설정을 더 안전하게 만들고 싶다면 다음 항목을 추가할 수 있습니다.
# OCSP Stapling(온라인 인증서 상태 확인)
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/your-domain.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# iframe 삽입 금지(클릭재킹 방지)
add_header X-Frame-Options DENY;
# XSS 방지
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
설정을 마친 뒤 SSL Labs에서 SSL 설정 점수를 확인할 수 있습니다. A+ 등급도 어렵지 않게 받을 수 있습니다.
리버스 프록시와 Docker 컨테이너 간 통신
컨테이너 네트워크 이해하기
Nginx로 다른 Docker 컨테이너를 프록시할 때 가장 까다로운 부분은 네트워크 설정입니다. 컨테이너 IP를 직접 사용해 봤다면 재시작할 때마다 IP가 바뀌어 설정을 다시 수정해야 하는 문제를 겪었을 수 있습니다.
Docker에는 여러 네트워크 모드가 있으며 다음 두 가지가 자주 사용됩니다.
- bridge(브리지): 기본 모드로, 컨테이너가 가상 브리지를 통해 통신합니다.
- host: 컨테이너가 호스트의 네트워크 스택을 직접 사용합니다. 성능은 좋지만 격리 수준이 낮습니다.
Nginx 리버스 프록시에는 사용자 정의 bridge 네트워크를 적극 권장합니다. 이유는 다음과 같습니다.
- 컨테이너끼리 서비스 이름으로 직접 통신할 수 있어 IP 변경을 신경 쓸 필요가 없습니다.
- 프로젝트별 컨테이너가 독립된 네트워크 환경을 구성하므로 네트워크가 격리됩니다.
- Docker에 내장된 서비스 검색 기능으로 DNS가 자동 해석됩니다.
사용자 정의 네트워크 생성과 컨테이너 실행
먼저 사용자 정의 네트워크를 만듭니다.
docker network create my-app-network
Node.js API라고 가정한 백엔드 서비스를 시작합니다.
docker run -d \
--name backend-api \
--network my-app-network \
-e NODE_ENV=production \
my-backend:latest
Nginx도 동일한 네트워크에 연결해 시작합니다.
docker run -d \
--name my-nginx \
--network my-app-network \
-p 80:80 -p 443:443 \
-v /opt/nginx/conf.d:/etc/nginx/conf.d \
nginx
핵심은 두 컨테이너가 동일한 네트워크에 있다는 점입니다. 이제 Nginx 설정에서 backend-api라는 이름으로 백엔드 서비스에 직접 접근할 수 있습니다.
Nginx 리버스 프록시 설정 실전
/opt/nginx/conf.d/ 아래에 api-proxy.conf를 만듭니다.
upstream backend {
server backend-api:3000; # 컨테이너 이름:포트
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
# API 인터페이스 프록시
location /api/ {
proxy_pass http://backend/;
# 실제 클라이언트 정보 전달
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;
# WebSocket 지원(필요한 경우)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 시간 초과 설정
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 정적 파일은 Nginx가 직접 처리
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
놓치기 쉬운 세부 사항은 다음과 같습니다.
1. upstream과 proxy_pass의 경로 처리
proxy_pass http://backend/; 끝에 슬래시가 있다는 점에 주목하세요. 이 경우 /api/users 요청은 /api 접두사가 제거된 http://backend/users로 전달됩니다.
반대로 슬래시 없이 proxy_pass http://backend;로 작성하면 전체 경로를 유지한 http://backend/api/users로 전달됩니다.
2. X-Forwarded-For 헤더의 중요성
백엔드 서비스가 실제 클라이언트 IP를 확인해야 한다면 이 헤더가 필요합니다. 그렇지 않으면 백엔드에는 모두 Nginx 컨테이너의 IP로 표시됩니다.
3. keepalive 연결 풀
upstream의 keepalive 32는 TCP 연결을 재사용해 핸드셰이크 오버헤드를 줄입니다. 동시 요청이 많은 환경에서 특히 유용합니다.
docker-compose로 여러 컨테이너 간단히 관리하기
네트워크를 수동으로 만들고 컨테이너를 하나씩 시작하는 일은 번거롭습니다. docker-compose를 사용하면 한 번에 처리할 수 있습니다.
version: '3.8'
services:
backend:
image: my-backend:latest
container_name: backend-api
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://db:5432/mydb
networks:
- app-network
depends_on:
- db
nginx:
image: nginx:latest
container_name: my-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/html:/usr/share/nginx/html
- ./nginx/logs:/var/log/nginx
networks:
- app-network
depends_on:
- backend
db:
image: postgres:14
container_name: postgres-db
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=mydb
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
db-data:
명령 한 줄로 전체 서비스 스택을 시작합니다.
docker-compose up -d
Docker는 자동으로 다음 작업을 수행합니다.
app-network라는 네트워크 생성depends_on순서에 따라 컨테이너 시작- 컨테이너 간 DNS 해석 설정
이제 Nginx 설정에서 backend와 db를 호스트 이름으로 사용해 해당 서비스에 바로 접근할 수 있습니다. 매우 편리합니다.
흔한 프록시 문제의 진단 방법
문제 1: 502 Bad Gateway
가장 흔한 오류이며 보통 원인은 다음과 같습니다.
- 백엔드 서비스가 시작되지 않았거나 중단됨
- Nginx와 백엔드가 같은 네트워크에 있지 않음
- 백엔드가 수신하는 포트가 잘못됨
다음 순서로 확인합니다.
# 백엔드 서비스가 실행 중인지 확인
docker ps | grep backend
# 네트워크 연결 확인
docker network inspect my-app-network
# Nginx 컨테이너에 들어가 연결 테스트
docker exec -it my-nginx sh
ping backend-api
curl http://backend-api:3000/health
ping이 되지 않으면 네트워크 설정 문제입니다. curl이 시간 초과되면 백엔드 서비스의 수신 설정을 확인해야 합니다.
문제 2: 요청 시간 초과
기본 proxy 시간 초과 값은 60초입니다. 대용량 파일 업로드나 복잡한 계산처럼 API 처리 시간이 길다면 시간 초과 값을 늘려야 합니다.
location /api/long-running/ {
proxy_pass http://backend/;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
문제 3: POST 요청 본문 누락
POST 요청이 백엔드에서 GET으로 바뀌거나 요청 본문이 비어 있는 경우가 있습니다. 다음 설정을 확인하세요.
location /api/ {
proxy_pass http://backend/;
proxy_request_buffering off; # 요청 버퍼링 비활성화
client_max_body_size 100M; # 대용량 파일 업로드 허용
}
여러 백엔드 서비스의 로드 밸런싱
백엔드 서비스 인스턴스가 여러 개라면 Nginx로 로드 밸런싱할 수 있습니다.
upstream backend_cluster {
least_conn; # 최소 연결 알고리즘
server backend-1:3000 weight=3; # 가중치 3
server backend-2:3000 weight=1; # 가중치 1
server backend-3:3000 backup; # 백업 서버
keepalive 32;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend_cluster/;
# ... 기타 proxy 설정
}
}
로드 밸런싱 알고리즘은 다음과 같습니다.
round-robin(기본값): 순차 분배least_conn: 연결 수가 가장 적은 서버 우선ip_hash: 동일한 클라이언트 IP를 항상 동일한 서버에 할당
백엔드 하나가 중단되면 Nginx는 자동으로 트래픽을 다른 정상 노드로 전환합니다.
프로덕션 환경 모범 사례와 성능 최적화
설정 파일 관리: 아무 준비 없이 프로덕션에 배포하지 않기
설정 파일을 서버에 그대로 던져 두는 방식은 개발 환경에나 적합합니다. 프로덕션 환경에서는 체계적으로 관리해야 합니다.
Git으로 설정 관리
저는 모든 Nginx 설정만 보관하는 별도 저장소를 만들고 환경별로 디렉터리를 나눠 관리합니다. 장점은 분명합니다.
- 버전 기록을 한눈에 확인할 수 있고
git checkout으로 바로 롤백할 수 있습니다. - 팀 협업이 편리하며 PR로 설정 변경을 리뷰할 수 있습니다.
- CI/CD와 연동해 자동 배포할 수 있습니다.
설정 템플릿화
환경별 설정은 대부분 비슷합니다. 템플릿과 변수 치환을 사용하면 작업량을 크게 줄일 수 있습니다. 배포할 때 envsubst로 변수를 치환하면 됩니다.
로그 관리: 디스크가 가득 찬 뒤에 생각하지 않기
Nginx 로그는 빠르게 늘어납니다. 트래픽이 많은 사이트에서는 하루에 몇 GB가 쌓이는 일도 드물지 않습니다. 제대로 관리하지 않으면 어느 날 디스크가 가득 차 컨테이너가 바로 중단될 수 있습니다.
로그 로테이션 설정
호스트에서 logrotate를 설정해 매일 로그를 순환하고 14일간 보관하며, 이전 로그는 자동으로 압축하도록 합니다. 중요한 점은 로테이션 뒤에 Nginx가 로그 파일을 다시 열도록 nginx -s reopen을 실행해야 한다는 것입니다. 그렇지 않으면 Nginx가 계속 이전 파일에 로그를 씁니다.
중앙 집중식 로그 구성
서버가 여러 대라면 로그를 중앙 플랫폼으로 보낼 수 있습니다. ELK, Loki, 클라우드 서비스 등을 고려하세요. Docker 로그 드라이버나 Promtail을 사용하면 간단히 구성할 수 있습니다.
우아한 리로드: 사용자가 알아채지 못하게 하기
설정을 수정한 뒤 바로 docker restart를 실행하면 안 됩니다. 재시작하면 모든 연결이 끊기고 처리 중인 요청도 모두 사라집니다.
올바른 방법은 다음과 같습니다.
# 먼저 설정 구문 검사
docker exec my-nginx nginx -t
# 구문에 문제가 없으면 reload
docker exec my-nginx nginx -s reload
reload는 우아한 리로드입니다. 새 worker 프로세스가 새 설정을 불러와 시작되고, 이전 worker는 기존 요청을 모두 처리한 뒤 종료됩니다. 전체 과정이 끊김 없이 전환되어 사용자는 변화를 느끼지 못합니다.
보안 강화 체크리스트
프로덕션에 배포하기 전 다음 보안 항목을 확인하세요.
1. Nginx 버전 숨기기
기본 오류 페이지에는 Nginx 버전이 노출됩니다. 공격자는 이를 이용해 알려진 취약점을 겨냥할 수 있습니다. http 블록에 server_tokens off;를 추가해 버전 번호를 숨기세요.
2. 속도 제한으로 DDoS 방어
간단하지만 효과적인 보호 설정입니다.
http {
# IP별 요청 빈도 제한
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# 동시 연결 수 제한
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
}
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 10;
proxy_pass http://backend/;
}
}
3. 설정 파일을 읽기 전용으로 마운트
기본 설정에 :ro를 추가해 컨테이너 안의 프로세스가 실수로 수정하지 못하게 합니다.
4. 최소 권한 원칙
nginx.conf에 user nginx;가 있는지 확인하세요. root로 바꾸면 보안 위험이 매우 커집니다.
성능 튜닝: Nginx 성능을 최대한 활용하기
worker 프로세스 수 최적화
worker_processes auto; # CPU 코어 수에 자동으로 맞춤
worker_cpu_affinity auto; # CPU affinity 바인딩
auto를 사용하면 편리합니다. Nginx가 CPU 코어 수를 자동으로 감지합니다.
연결 수 최적화
events {
worker_connections 2048; # worker별 최대 연결 수
use epoll; # Linux에서는 epoll의 성능이 가장 좋음
}
이론상 최대 동시 연결 수는 worker_processes * worker_connections입니다. 다만 실제 환경에서는 파일 디스크립터 제한인 ulimit -n도 고려해야 합니다.
gzip 압축
텍스트 콘텐츠를 압축하면 전송량을 60~80% 줄일 수 있습니다.
http {
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # 압축 수준 1~9 중 6은 성능과 압축률의 균형점
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
gzip_min_length 1000; # 1KB보다 작은 파일은 압축하지 않음. 오히려 CPU를 낭비할 수 있음
}
정적 리소스 캐시
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
expires 30d; # 브라우저에서 30일간 캐시
add_header Cache-Control "public, immutable";
}
HTTP/2 지원
HTTP/2는 성능을 크게 향상하며 다음처럼 간단히 활성화할 수 있습니다.
server {
listen 443 ssl http2; # http2만 추가하면 됨
# ... 기타 설정
}
단, HTTPS가 먼저 설정되어 있어야 합니다. HTTP/2는 TLS를 기반으로 동작합니다.
모니터링과 상태 확인
마지막으로 모니터링을 빼놓지 마세요. Prometheus와 Grafana를 사용하고 nginx-prometheus-exporter로 지표를 내보내는 구성을 권장합니다. Nginx 설정에서 stub_status를 활성화하되 Docker 내부 네트워크에서만 접근할 수 있도록 제한한 다음, Prometheus가 metrics를 수집하고 Grafana 대시보드에서 모니터링하도록 구성합니다.
결론
지금까지의 내용을 네 가지로 정리할 수 있습니다.
설정 파일 마운트: 단일 파일보다 디렉터리 마운트가 낫습니다. 기본 설정은 읽기 전용으로 보호하고 conf.d와 ssl 디렉터리도 빠뜨리지 마세요. vim이 inode를 바꾸는 문제도 피해야 합니다.
HTTPS 설정: 개발 환경에서는 자체 서명 인증서로 빠르게 검증하고, 프로덕션에서는 Let’s Encrypt와 Certbot으로 자동화합니다. docker-compose를 사용하면 갱신 과정도 간단해집니다.
리버스 프록시: 사용자 정의 bridge 네트워크가 가장 편리합니다. 컨테이너 이름으로 통신하면 IP를 신경 쓰지 않아도 됩니다. upstream에 keepalive를 설정하면 성능과 안정성을 모두 높일 수 있습니다.
프로덕션 운영: Git으로 설정 버전을 관리하고 로그 로테이션도 빠뜨리지 마세요. restart 대신 우아한 reload를 사용하고 필요한 보안 정책을 모두 적용해야 합니다.
Docker Nginx는 겉보기에는 간단하지만 실제로는 세부 사항이 많습니다. 이 내용을 이해하면 개발부터 프로덕션까지 안정적이고 신뢰할 수 있는 웹 서비스 아키텍처를 구축할 수 있습니다.
이 글이 몇 가지 함정을 피하거나 오랫동안 해결하지 못한 문제를 해결하는 데 도움이 되었다면 충분히 가치가 있습니다. 직접 따라 해 보고 문제가 생기면 댓글로 의견을 나눠 주세요.
마지막으로 한 가지 권합니다. 이 글의 설정 템플릿을 저장해 두었다가 다음에 바로 재사용하세요. 같은 작업을 다시 하는 대신 아낀 시간을 가족과 보내는 편이 훨씬 좋지 않을까요?
Docker로 Nginx를 배포하는 전체 과정
설정 파일 마운트, HTTPS 설정, 리버스 프록시 실전을 통해 설정 미적용과 컨테이너 통신 같은 흔한 문제를 해결합니다.
⏱️ Estimated time: 1 hr
- 1
Step 1: 설정 파일 마운트: 올바른 5가지 방법
올바른 5가지 마운트 방법:
• 설정 파일 마운트: -v ./nginx.conf:/etc/nginx/nginx.conf
• 설정 디렉터리 마운트: -v ./conf.d:/etc/nginx/conf.d
• Volume으로 마운트
• docker-compose로 설정
• ConfigMap 사용(K8s 환경)
흔한 문제:
• nginx.conf를 분명히 수정했는데도 컨테이너 설정이 적용되지 않음
• -v 옵션으로 설정 파일 디렉터리를 올바르게 마운트해야 함
• 설정 파일 형식이 올바른지 확인
모범 사례:
• 단일 파일보다 디렉터리 마운트 권장
• 기본 설정은 읽기 전용으로 보호
• conf.d와 ssl 디렉터리도 빠뜨리지 않기
• vim이 inode를 바꾸는 문제 피하기 - 2
Step 2: HTTPS 설정과 Let's Encrypt 자동 갱신
HTTPS 설정:
• Let's Encrypt 무료 인증서 사용
• 자동 갱신 설정: certbot renew
• 인증서 디렉터리 마운트: -v ./certs:/etc/nginx/certs
• SSL 인증서 경로 설정
• HTTP/2와 TLS 1.3 지원
Let's Encrypt 설정:
• certbot으로 인증서 발급: certbot certonly --standalone
• 자동 갱신 테스트: certbot renew --dry-run
• 컨테이너에 인증서 마운트: -v ./letsencrypt:/etc/letsencrypt
• 인증서를 사용하도록 nginx.conf 설정 - 3
Step 3: 리버스 프록시 설정과 프로덕션 환경 모범 사례
리버스 프록시 설정:
• upstream이 백엔드 컨테이너를 가리키도록 설정
• 컨테이너 이름 또는 서비스 이름 사용: proxy_pass http://backend:8080
• 상태 확인 설정
• CORS 문제 처리
• 로드 밸런싱 설정
프로덕션 환경 모범 사례:
• docker-compose로 여러 컨테이너 관리
• 상태 확인 설정
• 리소스 제한 설정
• 로그 로테이션 설정
• 환경 변수로 설정 관리
• 이미지 정기 업데이트
Docker Nginx는 겉보기에는 간단하지만 실제로는 세부 사항이 많습니다. 이 내용을 이해하면 개발부터 프로덕션까지 안정적이고 신뢰할 수 있는 웹 서비스 아키텍처를 구축할 수 있습니다.
FAQ
Docker Nginx 설정 파일을 수정해도 적용되지 않는 이유는 무엇인가요?
올바른 5가지 마운트 방법:
1) 설정 파일 마운트: -v ./nginx.conf:/etc/nginx/nginx.conf
2) 설정 디렉터리 마운트: -v ./conf.d:/etc/nginx/conf.d
3) Volume으로 마운트
4) docker-compose로 설정
5) ConfigMap 사용(K8s 환경)
모범 사례:
• 단일 파일보다 디렉터리 마운트 권장
• 기본 설정은 읽기 전용으로 보호
• conf.d와 ssl 디렉터리도 빠뜨리지 않기
• vim이 inode를 바꾸는 문제 피하기
Docker Nginx에 HTTPS를 어떻게 설정하나요?
• Let's Encrypt 무료 인증서 사용
• 자동 갱신 설정: certbot renew
• 인증서 디렉터리 마운트: -v ./certs:/etc/nginx/certs
• SSL 인증서 경로 설정
• HTTP/2와 TLS 1.3 지원
Let's Encrypt 설정:
• certbot으로 인증서 발급: certbot certonly --standalone
• 자동 갱신 테스트: certbot renew --dry-run
• 컨테이너에 인증서 마운트: -v ./letsencrypt:/etc/letsencrypt
• 인증서를 사용하도록 nginx.conf 설정
Docker Nginx 리버스 프록시는 어떻게 설정하나요?
• upstream이 백엔드 컨테이너를 가리키도록 설정
• 컨테이너 이름 또는 서비스 이름 사용: proxy_pass http://backend:8080
• 상태 확인 설정
• CORS 문제 처리
• 로드 밸런싱 설정
컨테이너 간 통신:
• docker-compose로 사용자 정의 네트워크 생성
• 컨테이너가 동일한 네트워크에 있는지 확인
• 서비스 이름 또는 컨테이너 이름으로 접근
• upstream이 백엔드 서비스를 가리키도록 설정
프로덕션 환경에서 Docker Nginx를 운영할 때의 모범 사례는 무엇인가요?
• docker-compose로 여러 컨테이너 관리
• 상태 확인 설정
• 리소스 제한 설정
• 로그 로테이션 설정
• 환경 변수로 설정 관리
• 이미지 정기 업데이트
Docker Nginx는 겉보기에는 간단하지만 실제로는 세부 사항이 많습니다. 이 내용을 이해하면 개발부터 프로덕션까지 안정적이고 신뢰할 수 있는 웹 서비스 아키텍처를 구축할 수 있습니다.
권장 사항: 이 글의 설정 템플릿을 저장해 두었다가 다음에 바로 재사용하세요. 같은 작업을 다시 할 필요가 없습니다.
5분 읽기 · 게시일: 2025년 12월 18일 · 수정일: 2026년 9월 4일
Docker 실전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Docker로 MySQL 배포하기 완벽 가이드: 데이터 영속화부터 마스터-슬레이브 복제 실전까지
Docker MySQL 데이터 영속화와 마스터-슬레이브 복제 설정을 다루고, 컨테이너 재시작 시 데이터 손실, 설정 파일 마운트, 연결 실패 등 자주 발생하는 문제를 해결하는 프로덕션급 배포 방안을 제공합니다.
33편 중 23편
다음
Docker 이미지 보안 스캔과 수정: Trivy 실전 튜토리얼 및 CI/CD 통합 가이드
Docker Hub 이미지의 76%에는 보안 취약점이 존재합니다. Trivy 스캔 도구의 실전 사용법과 체계적인 취약점 수정 방법, CI/CD 자동화 통합을 완전한 명령 예제와 함께 설명합니다.
33편 중 25편



댓글
GitHub로 로그인하여 댓글을 남기세요