블로그 로드맵의 클라우드 파트 대미를 장식할 [Cloud 13] Nginx 리버스 프록시 설정 편입니다.
이제 서버의 문지기 역할을 하는 Nginx를 도입하여, 외부의 요청을 안전하고 효율적으로 Django(Gunicorn)에게 전달하는 구조를 완성할 차례입니다. 실무에서 왜 Nginx를 반드시 쓰는지, 그리고 도커 환경에서 어떻게 설정하는지 상세히 정리해 드립니다.
[Cloud 13] Nginx 리버스 프록시 설정으로 서버 트래픽 안정화하기
지금까지 우리는 Django 앱을 Gunicorn으로 띄워 8000번 포트로 열어두었습니다. 하지만 실제 서비스에서는 사용자가 직접 8000번 포트로 접속하게 하지 않습니다. 보안과 성능을 위해 앞단에 Nginx라는 강력한 웹 서버를 세워야 합니다.
1. 왜 Nginx(리버스 프록시)를 써야 할까?
Nginx는 사용자와 Django 사이에서 '대리인(Proxy)' 역할을 수행합니다.
- 보안 강화: Django 서버의 직접적인 노출을 막고, 공격자가 서버 내부 구조를 알기 어렵게 합니다.
- 버퍼링 및 로드밸런싱: 느린 네트워크를 가진 사용자의 요청을 Nginx가 대신 받아 정리해서 Django에게 넘겨줌으로써 서버 부하를 줄입니다.
- 정적 파일 처리: css, js, 이미지 등 변하지 않는 파일은 Django를 거치지 않고 Nginx가 직접 빠르게 응답합니다.
- HTTPS(SSL) 적용: 보안 연결 설정을 Django 코드가 아닌 Nginx 단에서 깔끔하게 처리할 수 있습니다.
2. Nginx 설정 파일 작성 (nginx.conf)
프로젝트 내에 nginx 폴더를 만들고 default.conf 파일을 작성합니다.
upstream django_app {
# 도커 컴포즈에서 설정한 서비스 이름(web)과 포트를 사용합니다.
server web:8000;
}
server {
listen 80;
server_name dev-portfolio.com www.dev-portfolio.com; # 내 도메인 주소
location / {
proxy_pass http://django_app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
# 정적 파일 경로 설정 (나중에 S3를 안 쓸 경우 대비)
location /static/ {
alias /app/static/;
}
location /media/ {
alias /app/media/;
}
}
3. Docker Compose에 Nginx 서비스 추가
이제 docker-compose.yml 파일을 수정하여 Nginx 컨테이너를 함께 띄웁니다.
services:
db: ...
redis: ...
web:
build: .
# 이제 외부 포트(8000)를 열 필요가 없습니다. Nginx만 통하면 되니까요.
expose:
- "8000"
...
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./static:/app/static
- ./media:/app/media
depends_on:
- web
4. 리버스 프록시 작동 원리
- 사용자가 도메인(80포트)으로 접속합니다.
- AWS 보안 그룹이 80포트 요청을 허용하여 EC2로 보냅니다.
- Nginx 컨테이너가 이 요청을 낚아챕니다.
- Nginx는 설정된 규칙(proxy_pass)에 따라 web(Django) 컨테이너의 8000번 포트로 요청을 전달합니다.
- Django가 처리한 결과를 Nginx가 받아 다시 사용자에게 돌려줍니다.
5. [선생님의 심화 보충] Gunicorn 설정 최적화
Nginx와 함께 쓸 때 Gunicorn의 성능을 극대화하려면 워커(Worker) 프로세스 수를 조정해야 합니다.
- 공식: Workers = (2 x CPU 코어 수) + 1
- t3.micro 같은 2코어 서버라면 워커를 3~5개로 설정하여 동시에 들어오는 요청을 더 효율적으로 처리할 수 있습니다. 도커의 command 부분에 -w 3 옵션을 추가해 보세요.
✍️ 블로그 작성을 위한 마지막 조언
독자들에게 **"Nginx를 설치하는 순간, 프로젝트가 '연습용'에서 '상용 서비스'로 격상된다"**는 자부심을 심어주세요. 8000번 포트를 주소창에 치지 않아도 도메인만으로 접속되는 깔끔한 사용자 경험을 강조하면 좋습니다.
/home/play/workspace/django_test/mysite
1️⃣ Docker 서비스 확인/관리
# 도커 서비스 상태 확인
sudo service docker status # active(가동중) 확인
# 도커 서비스 시작 (꺼져있으면)
sudo service docker start
# 기존 데몬 서비스 중지 (포트 충돌 방지용)
sudo systemctl stop nginx # nginx 중지
sudo systemctl stop mysite # gunicorn(systemd 단위 mysite) 중지
🔎 관련 개념
- Docker Daemon: 도커 컨테이너를 실행·관리하는 백그라운드 서비스.
- systemctl/service: 리눅스에서 서비스(데몬)를 시작·중지·상태 확인하는 명령.
- 포트 충돌: nginx, gunicorn 등이 이미 80/8000 포트를 쓰면 새 컨테이너 실행이 실패 → 중지 필요.
2️⃣ 베이스 이미지 가져오기
# Python 3.10 + Alpine Linux 이미지 다운로드
docker pull python:3.10.0-alpine
🔎 관련 개념
- Image: 컨테이너 실행을 위한 템플릿(불변 스냅샷).
- python:3.10.0-alpine: Python 3.10이 설치된 초경량 Alpine Linux.
- Alpine Linux: 용량이 작고 보안성이 높은 배포판, 서버 서빙에 자주 사용.
3️⃣ requirements.txt 준비
# 현재 가상환경의 패키지 목록을 동결(freeze)
pip list --format=freeze > requirements.txt
🔎 관련 개념
- requirements.txt: 프로젝트 의존성을 고정시켜주는 파일.
- freeze: 현재 환경에 설치된 패키지를 버전까지 포함해 기록.
- 장점: 재현성 ↑, 단점: 불필요한 패키지까지 포함될 수 있음.
- 실무 팁: 꼭 필요한 상위 패키지만 작성(Django, gunicorn, mysqlclient 등).
4️⃣ Dockerfile 작성
# 베이스 이미지
FROM python:3.10.0-alpine
# 컨테이너 내부 작업 디렉토리
WORKDIR /usr/src/app
# apk 업데이트 및 필수 라이브러리 설치
RUN apk update
RUN apk add --no-cache mariadb-dev python3-dev musl-dev zlib-dev gcc
# 소스코드 복사
COPY . /usr/src/app
# pip 최신화 및 requirements 설치
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
🔎 관련 개념
- WORKDIR: 컨테이너 내부의 작업 디렉토리 설정.
- apk add: Alpine Linux 패키지 설치 명령.
- mariadb-dev: mysqlclient 빌드용 헤더
- python3-dev: 파이썬 C API 헤더
- musl-dev: C 표준 라이브러리 헤더
- zlib-dev: 압축 관련 라이브러리(Pillow 등에서 사용)
- gcc: C 컴파일러
- COPY: 호스트 소스를 컨테이너로 복사.
- pip install: 파이썬 의존성 설치.
5️⃣ requirements.txt (최소 구성)
Django==5.2.5
django-extensions==4.1
gunicorn==23.0.0
mysqlclient==2.2.7
🔎 관련 개념
- Django: 웹 프레임워크.
- django-extensions: 개발 보조 기능(쉘 확장, 그래프 시각화 등).
- gunicorn: 파이썬 WSGI 서버(운영 환경에서 runserver 대신 사용).
- mysqlclient: MySQL/MariaDB와 Django를 연결하는 Python 라이브러리.
6️⃣ 이미지 빌드 & 컨테이너 실행
# Dockerfile 기반으로 이미지 빌드
docker build -t my-django-app:alpine .
# 컨테이너 실행 (개발용 runserver 예시)
docker run -d --name mysite-alpine -p 8000:8000 my-django-app:alpine \
python manage.py runserver 0.0.0.0:8000
# 실행 상태 확인
docker ps
docker logs -f mysite-alpine
🔎 관련 개념
- docker build: Dockerfile로부터 새로운 이미지 생성.
- docker run: 이미지를 기반으로 컨테이너 실행.
- -p 8000:8000: 호스트 8000 ↔ 컨테이너 8000 포트 연결.
- runserver: 개발 서버 실행 (운영에선 gunicorn + nginx 권장).
- docker ps/logs: 컨테이너 실행 상태 및 로그 확인.
7️⃣ 이미지 확인
# 현재 로컬에 저장된 도커 이미지 목록 확인
docker images
🔎 관련 개념
- docker images: 도커 엔진에 다운로드(build)된 이미지 목록을 보여줌.
- 출력 컬럼:
- REPOSITORY: 이미지 이름
- TAG: 버전/태그 (예: 0.1, alpine)
- IMAGE ID: 이미지 고유 식별자
- CREATED: 생성 시각
- SIZE: 이미지 용량
8️⃣ 컨테이너 실행 (베이스 이미지로 테스트)
# python:3.10.0-alpine 이미지를 컨테이너로 실행
docker run -itd --name django_test python:3.10.0-alpine
# 실행 중인 컨테이너 내부 접속 (Alpine은 기본 쉘이 ash)
docker exec -it django_test /bin/ash
# 네트워크 확인 (구글 DNS 서버로 ping 테스트)
ping 8.8.8.8
🔎 관련 개념
- docker run -itd:
- -i : 인터랙티브 모드
- -t : 가상 터미널 할당
- -d : 백그라운드 실행 (detached)
- --name: 컨테이너에 이름 부여 (django_test).
- /bin/ash: Alpine Linux 기본 쉘. (Debian/Ubuntu 계열은 /bin/bash).
- ping: 네트워크 연결 상태 확인. 컨테이너가 외부와 통신 가능한지 테스트.
9️⃣ 컨테이너 실행 (Django 프로젝트 기반 이미지)
# 빌드한 이미지(mysite:0.1)로 Django 컨테이너 실행
docker run -itd --name django -p 8888:8888 mysite:0.1
# 컨테이너 내부 접속
docker exec -it django /bin/ash
🔎 관련 개념
- mysite:0.1: 앞에서 만든 Dockerfile 기반의 커스텀 이미지.
- -p 8888:8888: 호스트 8888 ↔ 컨테이너 8888 포트 연결.
- 즉, 브라우저에서 http://localhost:8888 접근 시 컨테이너 내부 장고 서버에 접속 가능.
- 컨테이너 내부 접속: docker exec으로 직접 들어가 패키지 설치/로그 확인/DB 접속 가능.
1) Django settings 디렉토리화
# 프로젝트 패키지 폴더로 이동 (예: manage.py 아래의 mysite/패키지)
cd /home/play/workspace/django_test/mysite/mysite
# settings 디렉토리 생성
mkdir -p settings
# ⚠️ 오타 주의: setting/ (X) → settings/ (O)
mv settings.py ./settings/
# settings 디렉토리로 이동
cd settings
# 기존 settings.py를 공통 설정 base.py로 이름 변경
mv settings.py base.py
# 패키지 인식을 위해 __init__.py 생성
touch __init__.py
# base.py (발췌) — settings 디렉토리로 이동했으므로 BASE_DIR 계층 1단계 증가
# 16번째 라인 예시
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent # ← settings/ 기준으로 프로젝트 루트까지
🔎 관련 개념
- mv는 이동/이름변경(복사 아님). 대상 디렉터리 존재 여부 중요.
- settings/로 빼면 패키지화가 필요 → __init__.py 필수.
- BASE_DIR은 파일 위치 기준이 바뀌므로 경로 계층을 조정해야 함.
2) 환경별 설정 분리 (local / prod)
# settings/local.py — 개발용
from .base import *
DEBUG = True
ALLOWED_HOSTS = ["*"]
# settings/prod.py — 운영용
from .base import *
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "mydjango",
"USER": "django",
"PASSWORD": "Encore!@#10",
"HOST": "192.168.0.30",
"PORT": 3306,
}
}
DEBUG = False
# ALLOWED_HOSTS = ["your.domain", "server.ip"]
🔎 관련 개념
- from .base import * 후 동일 이름 변수에 재할당하면 해당 모듈에서 덮어쓰기됨.
- 비밀번호/키 등 민감정보는 환경변수로 주입(코드 하드코딩 지양).
3) 개발/운영 실행 커맨드
# (개발) manage.py가 있는 곳에서 로컬 설정으로 실행
cd /home/play/workspace/django_test/mysite
python manage.py runserver 0.0.0.0:8000 --settings=mysite.settings.local
# (운영) gunicorn으로 WSGI 실행 (prod 설정)
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 \
--env DJANGO_SETTINGS_MODULE=mysite.settings.prod
🔎 관련 개념
- runserver는 개발용, gunicorn은 운영용.
- 둘을 **같은 포트(8000)**로 동시에 띄우면 포트 충돌.
4) docker-compose.yml
# docker-compose.yml
version: "3"
services:
web:
build: ./nginx
volumes:
- static_volume:/usr/src/app/static
ports:
- "80:80" # 외부 80 → nginx 80
depends_on:
- was
was:
build: .
command: gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --env DJANGO_SETTINGS_MODULE=mysite.settings.prod
volumes:
- static_volume:/usr/src/app/static
- ./:/usr/src/app/ # 개발 편의용 바인드(운영에선 제거 권장)
ports:
- "8000:8000" # 운영에선 보통 제거(내부통신만)
volumes:
static_volume:
🔎 관련 개념
- web(nginx:80) ↔ 내부 네트워크 ↔ was(gunicorn:8000)
- depends_on은 시작 순서만 보장(ready 대기는 아님).
- 운영에선 was의 포트 노출을 제거해 보안↑(nginx만 공개).
5) Nginx 이미지 및 설정
# nginx/Dockerfile
FROM nginx:latest
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
# nginx/nginx.conf
upstream mysite {
server was:8000; # docker 네트워크의 서비스명:포트
}
server {
listen 80;
location / {
proxy_pass http://mysite;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host; # Host 전달(도메인/CSRF 판단)
proxy_redirect off;
}
location /static/ {
alias /usr/src/app/static/; # STATIC_ROOT와 일치
}
}
🔎 관련 개념
- was는 compose 서비스명으로 DNS가 자동 등록되어 was:8000 접근 가능.
- Host, X-Forwarded-Proto 전달은 CSRF/보안 판단에 중요.
- Django STATIC_ROOT는 /usr/src/app/static 으로 맞추면 collectstatic 결과를 nginx가 바로 서빙.
6) Compose 올리기 & 재빌드
# 컨테이너/네트워크 생성 및 기동
docker compose up -d
# 변경사항까지 강제 빌드 후 기동
docker compose up -d --build
# 로그 확인
docker compose logs -f
# (최초/재배포) 정적파일 수집 + 마이그레이션
docker compose run --rm was python manage.py collectstatic --noinput --settings=mysite.settings.prod
docker compose run --rm was python manage.py migrate --settings=mysite.settings.prod
🔎 관련 개념
- up -d는 백그라운드 실행. --build로 이미지 재빌드.
- collectstatic/migrate는 배포 체크리스트에 포함.
- 준비 순서 통제를 원하면 WAS healthcheck + depends_on.condition: service_healthy 사용.
1) VM(가상머신)에서 ML/DL 할 때 생기는 제약
- GPU 접근: 게스트 OS가 호스트 GPU를 직접 못 보거나, 제한적으로만 봄
- 해결엔 PCIe 패스스루( VT-d / IOMMU ), vGPU, Hyper-V GPU-P 같은 하이퍼바이저 기능이 필요
- 성능/지연: 가상화 계층 때문에 I/O 지연(디스크/네트워크), 일부 CPU 명령어(AVX/AVX2/AVX-512) 노출 제한으로 라이브러리 성능 저하 가능
- 분산/네트워킹: RDMA/InfiniBand, NCCL 같은 고성능 통신 스택 구성 난이도↑ 또는 미지원
- 장치 접근: USB/카메라/센서 등 특수 장치 패스스루 필요
2) WSL이 Hyper-V로 완화하는 것(WSL2 기준)
- 실제 리눅스 커널 + 경량 Hyper-V VM → 리눅스 네이티브에 가까운 호환성
- GPU 가속 지원: Windows용 드라이버 + WSL 유저스페이스로 CUDA/DirectML 사용 가능
- Docker 연동: Docker Desktop의 WSL2 백엔드로 컨테이너 기반 개발 원활
- 주의 포인트
- 파일 I/O: /mnt/c(윈도우 디스크)에서 작업하면 느림 → WSL 내부 경로(~/project, /home/...)에 코드/데이터 두기
- 완전한 해소는 아님: 일부 특수 기능(예: 특정 RDMA/IB, 디바이스 패스스루)은 여전히 제한될 수 있음
- 리소스 제어: .wslconfig로 메모리/스왑 제한 조정 가능
3) mysqlclient vs PyMySQL (Django에서 MySQL 연결)
- mysqlclient
- C 확장 모듈(MySQL/MariaDB C 커넥터 바인딩) → 빠르고 안정적
- 빌드/설치 시 시스템 헤더/라이브러리 필요 → 도커/알파인에선 사전 패키지 설치가 필수
- Django ENGINE: django.db.backends.mysql 그대로 사용
- PyMySQL
- Pure-Python 구현 → 설치 간단, 빌드 도구 불필요(속도는 다소 느림)
- Django에서 pymysql.install_as_MySQLdb()로 mysqlclient 대체 가능
- 선택 가이드: 운영/성능 중시 → mysqlclient, 빠른 프로토타입/빌드 편의 → PyMySQL
4) Alpine(알파인)에서 apk로 설치하는 이유/구성
- 왜 필요?
- mysqlclient는 C로 된 확장 모듈이라 컴파일이 필요할 수 있음(사전 컴파일된 wheel이 없거나, 환경이 달라 재컴파일 필요)
- 대표 패키지 의미
- gcc: C 컴파일러 (확장 모듈 빌드)
- musl-dev: 알파인 기본 C 표준 라이브러리 musl의 개발 헤더
- python3-dev: 파이썬 C API 헤더
- mariadb-dev: libmariadb 개발 헤더/라이브러리(= MySQL 클라이언트 C 라이브러리 대체)
- zlib-dev: 압축 라이브러리(일부 패키지 빌드/런타임 의존)
- 정리: 위 패키지들이 있어야 pip install mysqlclient가 성공적으로 컴파일
개념 정리 — “RESTful API가 뭐야?”
- REST(Representational State Transfer)
웹 아키텍처 스타일. 자원(Resource) 을 식별 가능한 URI로 표현하고, HTTP 메서드(GET/POST/PUT/PATCH/DELETE)로 자원을 표준 방식으로 조작하는 철학이에요. - 핵심 원칙(요약)
- 리소스 지향: /users/, /users/42/처럼 명사형 URI
- HTTP 메서드 의미 통일:
- GET(조회), POST(생성), PUT/PATCH(전체/부분 수정), DELETE(삭제)
- 무상태(Stateless): 서버는 요청 간 세션 상태를 보관하지 않음(토큰 등으로 인증)
- 표현(Representation): JSON 등으로 리소스 상태를 표현
- 표준 상태코드: 200 OK, 201 Created, 400/404/409/500 등 일관된 응답
- 왜 쓰나?
- 클라이언트(웹/모바일/서버) 간 호환성과 확장성↑
- 문서화/테스트/배포가 표준화되어 협업이 쉬워짐
모델 서빙 wsl에서
ping 확인ping 192.168.56.10
감정분석 모델 만들기
# 감성분석 파이프라인 준비
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import pipeline
# 한국어 감성분석 모델 로드(첫 호출 시 모델 파일 다운로드)
pipe = pipeline("text-classification", model="nlp04/korean_sentiment_analysis_kcelectra")
# 단건 테스트 추론
pipe("토스해달라고")
# MySQL 연결 및 최신 질문 조회
import pymysql
# 데이터베이스 연결(호스트/계정/비밀번호/DB/포트)
connection = pymysql.connect(host='192.168.0.30', user='django', password='Encore!@#10', database='mydjango', port=3306)
# 커서 생성
cur = connection.cursor()
# 질문 테이블에서 생성일 내림차순으로 전체 행 조회
cur.execute("select * from board_question order by create_date desc")
# 모든 행 가져오기 (리스트[튜플, ...])
result = cur.fetchall()
# 첫 행의 세 번째 컬럼(인덱스 2; 예: content)을 확인
result[0][2]
# 해당 텍스트에 대해 감성분석 실행
pipe(result[0][2])
# 1) 가상환경 활성화 (conda)
conda activate torch
# 2) Django/DRF 설치
pip install django
pip install djangorestframework
# 3) 새 프로젝트 생성: senti/
django-admin startproject senti
# 4) 앱 생성: serving/
cd senti
python manage.py startapp serving
# (선택) 개발 서버 실행
# python manage.py runserver 0.0.0.0:8000
# settings.py에 추가
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'serving.apps.ServingConfig',
]
# urls.py
from django.contrib import admin
from django.urls import path
from ..serving import views
urlpatterns = [
path('admin/', admin.site.urls),
path('predict/', views.predict),
]
# serving views.py
from django.shortcuts import render
from rest_framework.decorators import api_view
from rest_framework.response import Response
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from transformers import pipeline
pipe = pipeline("text-classification", model="nlp04/korean_sentiment_analysis_kcelectra")
# Create your views here.
@api_view(['POST'])
def predict(request):
text = request.data.get('content')
result = pipe(text)[0]
return Response(result)
서버실행
python manage.py runserver 0.0.0.0:8000
Talend API(= Talend Cloud API Services): API를 **설계 → 문서화 → 모킹/테스트 → 구현 → 배포(마이크로서비스)**까지 이어서 지원하는 Talend의 제품군입니다. 클라우드에서 설계하고, 컨테이너化해 배포·CI/CD와 연계할 수 있어요.


Keep (계속 잘한 것)
- 정리 포맷 확립: 코드셀=코드+주석만, 개념은 텍스트로 분리 → 학습 기록 가독성↑
- 컨테이너 기본기: docker run/exec, -itd 의미, 베이스 이미지 vs 프로젝트 이미지 구분 명확
- 포트 매핑 이해: 호스트:컨테이너 규칙과 내부 리슨 포트 일치 원칙 숙지
- Django 설정 분리: base/local/prod, BASE_DIR 조정, --settings로 환경 지정
- 운영 구조 인지: Nginx(리버스 프록시/TLS/정적) ↔ Gunicorn(WSGI) ↔ Django 구조 파악
- Compose 흐름: web(nginx)와 was(gunicorn) 서비스, static_volume 공유 이해
- ML/데이터 연동: Transformers 파이프라인 + PyMySQL로 DB 텍스트 추출 후 감성분석 연결
- DRF 기본: RESTful 개념, @api_view 데코레이터 의미와 동작 이해
Problem (불편/개선 필요)
- 포트 충돌/불일치: -p 8888:8888인데 내부는 :8000으로 리슨 → 접속 실패 빈발
- HTTPS 혼선: dev 서버(HTTP)에 HTTPS로 접속 → “Bad request version” 오류
- CSRF/URL 정책: /predict(무슬래시)로 POST + APPEND_SLASH=True → 500 유발
- 비밀정보 하드코딩: DB 비번/설정이 코드에 노출
- 운영 노출 범위: compose에서 was의 8000:8000 외부 노출(보안/관리 측면 비권장)
- 헬스체크 부재: depends_on만 있고 서비스 준비(ready) 대기를 보장하지 않음
- Alpine 빌드 의존성: mysqlclient 컴파일러/헤더 누락 시 설치 실패 위험
- WSL/VM 제약 혼동: 파일 I/O 경로, GPU 가속 가용성, 네트워크 규칙 등 성능/제약 포인트 산발적
Try (실행 가능한 개선안)
- 포트 운영 규칙 고정
- 개발: -p 8888:8000 + 앱은 항상 8000 리슨(Dockerfile CMD도 8000로 일원화)
- 운영: Nginx만 80/443 공개, was는 내부 통신만(ports 제거)
- TLS 정석화
- Nginx에서 443/TLS 종료 → Gunicorn/Django는 HTTP(8000)
- Django에 SECURE_PROXY_SSL_HEADER, CSRF_TRUSTED_ORIGINS(https://…), ALLOWED_HOSTS 정확히 설정
- URL/슬래시 정책 일관화
- 라우트는 /predict/로 통일 또는 DRF Router의 trailing_slash=False 전역 사용
- 프록시에서 /predict → /predict/ 308 리다이렉트(본문 보존) 규칙 추가 고려
- CSRF 안정화
- 템플릿 {% csrf_token %}, AJAX X-CSRFToken+credentials:'include' 준수
- 배포 직후 캐시/쿠키 이슈 대비해 Ctrl+F5 안내와 캐시 무효화 전략(버전 쿼리) 도입
- 비밀값/환경 분리
- .env/environment:로 DB 비번, SECRET_KEY 주입(코드 하드코딩 제거)
- 헬스체크 도입
- WAS에 /health 추가 + compose healthcheck → web.depends_on.condition: service_healthy
- 빌드 최적화
- Alpine에 apk add mariadb-dev python3-dev musl-dev zlib-dev gcc 명시
- 가능하면 멀티-스테이지로 빌드 툴 제거, 또는 PyMySQL로 간편 대체(성능 요구 낮을 때)
- WSL 성능 수칙
- 대용량 I/O는 WSL 내부 경로에서 처리(/mnt/c 지양), GPU 사용 시 드라이버/툴체인 정합성 점검
Troubleshooting 요약
- “address already in use: 8000”
- 원인: 호스트 8000을 기존 프로세스/컨테이너(gunicorn 등)가 점유
- 해결: ss/lsof로 점유 주체 확인 → systemctl stop 또는 docker stop / 포트 변경(-p 8888:8000)
- HTTPS → dev 서버(HTTP)로 접속 시 400 “Bad request version”
- 원인: TLS 바이트를 HTTP 서버가 받음
- 해결: Nginx(443)에서 TLS 종료, 백엔드는 HTTP. 혹은 일단 **http://**로 접속
- CSRF 검증 실패
- 원인: 토큰/쿠키/출처 불일치, Host/X-Forwarded-Proto 미전달, 신뢰 오리진 누락
- 해결: CSRF 토큰 삽입/헤더 전송, 프록시 헤더 전달, CSRF_TRUSTED_ORIGINS/ALLOWED_HOSTS 보완
- POST /predict + APPEND_SLASH=True → 500
- 원인: /predict → /predict/ 리다이렉트 중 POST 본문 보존 불가
- 해결: /predict/ 호출로 통일, 또는 라우트에서 무슬래시 허용/APPEND_SLASH=False, 프록시에서 308 리다이렉트
- 컨테이너 접속 불가(포트 불일치)
- 원인: -p 8888:8888 매핑인데 앱은 :8000에서 리슨
- 해결: -p 8888:8000로 수정하거나 앱 리슨 포트를 8888로 맞춤
- mysqlclient 빌드 실패(Alpine)
- 원인: 컴파일러/헤더 부재
- 해결: apk add mariadb-dev python3-dev musl-dev zlib-dev gcc 후 pip install mysqlclient
- 대안: PyMySQL 사용(설치 용이, 성능은 다소 낮음)
'웹 > 배포' 카테고리의 다른 글
| [Final 17] 서비스 모니터링 기법과 17일간의 로드맵 회고,"로그 관리 (0) | 2025.09.12 |
|---|---|
| [Final 16] 코드만 push하세요, 배포는 GitHub Actions가 알아서 합니다 (0) | 2025.09.11 |
| [Final 15] 아직도 HTTP? Certbot으로 5분 만에 HTTPS 적용하기 (0) | 2025.09.10 |
| [Final 14] 서버 용량 걱정 끝! S3로 Static & Media 파일 관리하기 (0) | 2025.09.10 |
| [Cloud 12] 내 서비스에 이름 붙이기: Route53 도메인 및 탄력적 IP 연결 (0) | 2025.09.05 |