[Flask] Deployment (WSGI, Gunicorn, uWSGI, Docker)
정의
Flask 는 WSGI 앱. 프로덕션 배포는 WSGI 서버 (Gunicorn, uWSGI, gevent) + 리버스 프록시 (Nginx, ALB) + 컨테이너 (Docker) + 오케스트레이터 (Kubernetes/ECS) 계층으로 구성됩니다. 내장 개발 서버 (app.run) 은 절대 프로덕션 노출 금지.
구성 계층
[Client]
↓ HTTPS
[Load Balancer / CDN (ALB, CloudFront, Nginx)]
↓ HTTP
[WSGI 서버 (Gunicorn / uWSGI)]
↓ 프로세스 워커
[Flask WSGI 앱]
왜 개발 서버는 안 되는가
app.run(debug=True) 는 Werkzeug 개발 서버.
- Single-threaded (기본): 동시 요청 처리 불가
- debug=True 는 원격 코드 실행 취약점
- 성능 최적화 안 됨
- Graceful shutdown 미지원
FLASK_DEBUG=1+PIN없이 노출 = 서버 완전 장악 가능
Gunicorn (권장)
pre-fork 워커 모델. Python WSGI 서버의 사실상 표준.
pip install gunicorn
gunicorn myapp:create_app\(\) \
--workers 4 \
--worker-class sync \
--bind 0.0.0.0:8000 \
--timeout 60 \
--graceful-timeout 30 \
--keep-alive 5 \
--max-requests 10000 \
--max-requests-jitter 100 \
--access-logfile - \
--error-logfile - \
--log-level info
Worker 수
일반 공식: workers = 2 * cores + 1 (I/O bound). 조정 후 실제 부하로 조정.
메모리도 고려: 각 worker 는 프로세스 -> 앱 메모리 × workers.
Worker class
sync(기본): 요청당 하나. 표준.gthread: 각 worker 안에 여러 스레드. I/O bound 유리.gevent: 그린 스레드 (monkey patching). 매우 많은 동시 연결.eventlet: gevent 유사.
gunicorn myapp:create_app\(\) --worker-class gthread --workers 4 --threads 4
# 총 동시 요청 = 4 * 4 = 16
Timeout
--timeout 60: worker 가 60초 응답 못 하면 kill. 정상값이지만 큰 파일 다운로드/업로드는 늘리기.--graceful-timeout 30: SIGTERM 후 in-flight 요청 완료 대기.
max-requests
메모리 leak 완화. worker 가 N 요청 처리 후 재시작.
--max-requests 10000 --max-requests-jitter 100
Jitter 는 재시작 시점 랜덤화로 동시 재시작 방지.
uWSGI (대안)
기능 방대, 설정 복잡. 대규모 배포에서 여전히 쓰임.
# uwsgi.ini
[uwsgi]
module = myapp:create_app()
processes = 4
threads = 2
socket = 0.0.0.0:8000
harakiri = 60 # timeout
max-requests = 5000
vacuum = true
die-on-term = true
uwsgi --ini uwsgi.ini
Pros: 매우 많은 튜닝 옵션, 여러 언어 지원 Cons: 배우기 어려움, 최근 커뮤니티 활성도 감소 (Gunicorn 우세)
리버스 프록시 (Nginx)
upstream flask_app {
server 127.0.0.1:8000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
client_max_body_size 10M;
keepalive_timeout 65;
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://flask_app;
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_set_header X-Forwarded-Host $host;
proxy_read_timeout 60s;
}
}
ProxyFix (X-Forwarded-* 신뢰)
리버스 프록시 뒤에서 실제 client IP, scheme (http/https), host 를 제대로 인식하려면:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1, # X-Forwarded-For 신뢰 개수
x_proto=1, # X-Forwarded-Proto
x_host=1, # X-Forwarded-Host
x_prefix=1, # X-Forwarded-Prefix (subpath)
)
x_for=1: 프록시 하나만 신뢰. 여러 프록시 (ALB + CloudFront) 는 개수 증가.- 이 미들웨어 없이는
request.remote_addr이 프록시 IP 만 반환. - 잘못 신뢰하면 IP 스푸핑 가능. 실제 신뢰할 프록시 개수 정확히.
Docker
기본 Dockerfile
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev gcc && \
rm -rf /var/lib/apt/lists/*
COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir uv && \
uv sync --frozen --no-dev
COPY src /app/src
RUN useradd -m -u 1000 app && chown -R app:app /app
USER app
EXPOSE 8000
CMD ["uv", "run", "gunicorn", \
"myapp:create_app()", \
"--workers", "4", \
"--bind", "0.0.0.0:8000", \
"--access-logfile", "-", \
"--error-logfile", "-"]
Multi-stage build
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/.venv /app/.venv
COPY src /app/src
ENV PATH="/app/.venv/bin:$PATH"
RUN useradd -m -u 1000 app && chown -R app:app /app
USER app
CMD ["gunicorn", "myapp:create_app()", "--workers", "4", "--bind", "0.0.0.0:8000"]
Kubernetes 배포
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels: {app: myapp}
template:
metadata:
labels: {app: myapp}
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
image: myrepo/myapp:1.0
ports: [{containerPort: 8000}]
env:
- name: SECRET_KEY
valueFrom:
secretKeyRef: {name: myapp-secrets, key: secret-key}
- name: DATABASE_URL
valueFrom:
secretKeyRef: {name: myapp-secrets, key: db-url}
resources:
requests: {cpu: 500m, memory: 512Mi}
limits: {cpu: 2, memory: 2Gi}
livenessProbe:
httpGet: {path: /health/live, port: 8000}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet: {path: /health/ready, port: 8000}
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
Health check endpoint:
@app.get("/health/live")
def live():
return "ok"
@app.get("/health/ready")
def ready():
try:
db.session.execute(text("SELECT 1"))
return "ok"
except Exception:
return "not ready", 503
Signal handling
Gunicorn signal:
- SIGTERM: graceful shutdown (기본 K8s 종료)
- SIGINT (Ctrl+C): graceful shutdown
- SIGQUIT: 즉시 종료
- SIGHUP: reload workers
terminationGracePeriodSeconds > gunicorn --graceful-timeout 이어야 in-flight 요청 완료.
로깅
구조화 로그 (JSON)
import logging
from pythonjsonlogger import jsonlogger
def setup_logging(app):
handler = logging.StreamHandler()
handler.setFormatter(jsonlogger.JsonFormatter(
"%(asctime)s %(name)s %(levelname)s %(message)s"
))
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
Gunicorn access log 도 커스터마이제이션:
# gunicorn.conf.py
import json
def json_access_log_format(record):
return json.dumps({
"method": record.request_line.split()[0],
"path": record.request_line.split()[1],
"status": record.status_code,
"response_time": record.response_time,
})
# gunicorn --access-logformat '{}' --logger-class 'gunicorn.glogging.Logger'
Sentry, OpenTelemetry
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
sentry_sdk.init(
dsn="https://xxx@sentry.io/yyy",
integrations=[FlaskIntegration(), SqlalchemyIntegration()],
traces_sample_rate=0.1,
profiles_sample_rate=0.1,
)
정적 파일
프로덕션에서 Flask 가 정적 파일 서빙하지 마세요. Nginx 나 CDN (CloudFront) 에서 서빙.
Whitenoise (덜 권장, 소규모):
from whitenoise import WhiteNoise
app.wsgi_app = WhiteNoise(app.wsgi_app, root="static/")
환경 변수 & Secrets
class Config:
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]
Kubernetes Secret, AWS Secrets Manager, Vault 등에서 주입. Docker 이미지에 secret 굽지 마세요.
성능 튜닝 체크리스트
- Worker 수 = 2×CPU + 1 부터 시작, 부하 관찰 후 조정
--max-requests로 memory leak 완화- DB connection pool: SQLALCHEMY_ENGINE_OPTIONS 에
pool_size,max_overflow,pool_recycle - 정적 파일은 CDN/Nginx
- 캐시 (Flask-Caching) Redis
- N+1 쿼리 해결: SQLAlchemy
joinedload,selectinload - Gzip: Nginx 에서 활성화 (Flask 에서 X)
- HTTP/2: Nginx 에서 종단
함정
WARNING
app.run(debug=True) 프로덕션 노출 금지. RCE 취약점.
CAUTION
ProxyFix 개수 신중하게. 잘못 신뢰하면 IP 스푸핑. 실제 프록시 개수 정확히.
WARNING
Sync worker 로 sync DB + sync HTTP client 사용 시 요청당 한 워커 점유. 동시 처리량이 (workers × threads) 한계. gthread 나 gevent 로 확장.
IMPORTANT
Graceful shutdown 순서: LB 에서 제거 -> in-flight 요청 완료 -> DB 세션 정리 -> 프로세스 종료. K8s preStop sleep 5 은 LB 등록 해제 대기.
CAUTION
정적 파일을 Flask 로 서빙하면 앱이 병목. Gunicorn worker 하나가 CSS/JS 다운로드 처리하는 낭비. Nginx / CloudFront.
관련 위키
- Flask - 상위 개요
- Flask App Factory - Gunicorn 진입점
- Flask Blueprints
- Flask Request/Response - ProxyFix
- Flask Extensions - 캐시, CORS
- Flask Testing
- ECS Fargate - 컨테이너 배포
- EKS
- ALB/NLB
- Container Image Best Practices
- FastAPI Deployment - 대비
이 글의 용어 (11개)
- [AWS] ALB vs NLB: L7 vs L4 로드 밸런서cloud
- 정의 | | ALB | NLB | (Classic ELB) | |---|---|---|---| | Layer | L7 (HTTP) | L4 (TCP/UDP) | L4 + L7 (…
- [AWS] ECS + Fargate: 컨테이너 오케스트레이션cloud
- 정의 ECS (Elastic Container Service) = AWS 의 컨테이너 오케스트레이션. K8s 보다 단순하고 AWS 네이티브. Fargate = ECS (또는 EK…
- [AWS] EKS: managed Kubernetescloud
- 정의 EKS (Elastic Kubernetes Service) = AWS 의 managed K8s control plane. worker node 는 사용자 (또는 Fargat…
- [Container] Image Best Practices: 작게, 안전하게virtualization
- 정의 컨테이너 image best practices = 작고 (small), 안전하고 (secure), 재현 가능하고 (reproducible), 서명된 (signed) imag…
- [FastAPI] Deployment (Uvicorn, Gunicorn, Docker)fastapi
- 정의 FastAPI 배포는 ASGI 서버 (Uvicorn) + 프로세스 관리자 (Gunicorn 또는 Uvicorn 자체) + 컨테이너 (Docker) + 오케스트레이터 (Kub…
- [Flask] Application Factory Patternflask
- 정의 Application Factory 는 Flask 앱 인스턴스를 함수 안에서 생성 하는 패턴입니다. 모듈 스코프에 를 두는 대신 함수를 정의해 반환합니다. 이 패턴은 사실상…
- [Flask] Blueprintsflask
- 정의 Blueprint 는 Flask 앱을 여러 모듈로 나누는 표준 도구입니다. 라우트, 정적파일, 템플릿, before/after hook, error handler 를 그룹화…
- [Flask] Extensions (SQLAlchemy, Login, Migrate, WTF, ...)flask
- 정의 Flask Extension 은 코어에 없는 기능 (DB, 인증, 마이그레이션, 캐시 등) 을 표준 인터페이스로 통합하는 서드파티 패키지입니다. 패턴이 관용이며, appli…
- [Flask] Request & Responseflask
- 정의 Flask Request/Response 는 Werkzeug 의 / 래퍼를 확장한 것입니다. Thread-local 프록시 로 뷰 함수 어디서든 현재 요청에 접근하고, re…
- [Flask] Testingflask
- 정의 Flask Testing 은 앱 인스턴스를 직접 감싸 HTTP 요청을 시뮬레이션하는 workflow 입니다. (Werkzeug) 로 라우팅/뷰/미들웨어까지 실제 실행하고 응…
- [Python] Flaskflask
- 정의 Flask 는 Armin Ronacher 가 2010년 발표한 WSGI 기반 Python 마이크로프레임워크 입니다. Pallets Projects 가 유지관리하고, 2026…
💬 댓글