본문으로 건너뛰기
김신건의 로그

[FastAPI] Deployment (Uvicorn, Gunicorn, Docker)

· 수정 · 📖 약 3분 · 1,018자/단어 #python #fastapi #deployment #uvicorn #gunicorn #docker
FastAPI Deployment, FastAPI 배포, Uvicorn, Gunicorn Uvicorn, FastAPI Docker, ASGI 배포, FastAPI 프로덕션

정의

FastAPI 배포는 ASGI 서버 (Uvicorn) + 프로세스 관리자 (Gunicorn 또는 Uvicorn 자체) + 컨테이너 (Docker) + 오케스트레이터 (Kubernetes/ECS) 계층으로 구성됩니다. 각 계층의 역할과 트레이드오프를 정확히 이해해야 프로덕션 안정성이 확보됩니다.

구성 계층

[Client]
   ↓ HTTPS
[Load Balancer / CDN (ALB, CloudFront, Nginx)]
   ↓ HTTP
[프로세스 관리자 (Gunicorn or Uvicorn --workers)]
   ↓ multiple processes
[Uvicorn worker (ASGI server, per process)]

[FastAPI app]

Uvicorn 단독 (개발/간단 프로덕션)

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
  • --workers: 프로세스 수 (기본 1)
  • --reload: 개발용 자동 재시작 (프로덕션 X)
  • --log-level: debug/info/warning/error/critical
  • --access-log / --no-access-log
  • --proxy-headers --forwarded-allow-ips="*": 리버스 프록시 뒤 배포 시

Uvicorn —workers vs Gunicorn

  • Uvicorn —workers: multiprocessing 프로세스 매니저. 단순, 별도 의존성 없음.
  • Gunicorn + Uvicorn workers: Gunicorn 이 프로세스 라이프사이클 (graceful reload, timeout, pre-fork) 관리, Uvicorn 이 각 프로세스 안에서 ASGI 서버 역할.

Gunicorn 의 장점: signal 처리 (SIGHUP graceful reload), timeout, memory leak worker 재활용 등 성숙한 기능.

Gunicorn + Uvicorn worker

pip install gunicorn uvicorn[standard]

gunicorn myapp.main:app \
  -w 4 \
  -k uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --timeout 120 \
  --graceful-timeout 30 \
  --keep-alive 5 \
  --access-logfile - \
  --error-logfile -
  • -w: worker 프로세스 수. 일반 공식 2 * CPU + 1, async 워크로드는 CPU 개수 근방.
  • -k uvicorn.workers.UvicornWorker: Uvicorn 을 worker 로
  • --timeout: worker 가 응답 못 하면 kill (기본 30초)
  • --graceful-timeout: shutdown 시 진행 중 요청 완료 대기
  • --keep-alive: HTTP keep-alive 연결 유지 초

UvicornWorker vs UvicornH11Worker

  • UvicornWorker (기본): httptools + uvloop 로 성능 최적
  • UvicornH11Worker: pure Python h11. 특수 환경 (uvloop 미지원) 용

Worker 수 결정

CPU-bound async 앱:

  • 이벤트 루프가 CPU 를 대부분 소비 -> worker 수 = CPU 개수

IO-bound async 앱:

  • 이벤트 루프가 대부분 대기 -> worker 수 = CPU 개수 (많이 늘려도 이득 적음)

Sync 앱 (blocking IO):

  • Worker 하나에 스레드풀 40 워커. worker 수 = CPU 개수
  • 총 동시 요청 처리 = workers × threadpool

메모리 제약:

  • 각 worker 는 별도 프로세스 -> 앱 메모리 × worker 수
  • ML 모델 로드 앱은 worker 수를 신중히 (또는 shared memory)

신호 처리 & graceful shutdown

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # startup
    app.state.db = await create_engine()
    app.state.http = httpx.AsyncClient()
    yield
    # shutdown
    await app.state.http.aclose()
    await app.state.db.dispose()

app = FastAPI(lifespan=lifespan)

Gunicorn 이 SIGTERM 을 받으면 새 요청 accept 중지, 진행 중 요청 완료 대기, lifespan shutdown 실행.

Kubernetes 는 SIGTERM 을 pod 종료 전에 보냄. terminationGracePeriodSeconds 를 Gunicorn --graceful-timeout 보다 크게.

Docker

최소 Dockerfile

FROM python:3.12-slim

WORKDIR /app

# 의존성 캐시
COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir uv && \
    uv sync --frozen --no-dev

# 소스
COPY src /app/src

# non-root user
RUN useradd -m app && chown -R app:app /app
USER app

EXPOSE 8000

CMD ["uv", "run", "uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Multi-stage build (이미지 크기 최소화)

# --- builder ---
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

# --- runtime ---
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src /app/src
ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]

이미지 스택 선택

  • python:3.12-slim: 표준, ~130 MB base. glibc 사용.
  • python:3.12-alpine: 가벼움 (~55 MB base). musl libc, 일부 wheel 미지원.
  • gcr.io/distroless/python3-debian12: 최소, non-root, shell 없음. 디버그 어려움.

추천: python:3.12-slim + multi-stage 로 100-200 MB 최종 이미지.

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}]
          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}
            initialDelaySeconds: 5
            periodSeconds: 5
          startupProbe:
            httpGet: {path: /health/startup, port: 8000}
            failureThreshold: 30
            periodSeconds: 5
          lifecycle:
            preStop:
              exec:
                command: ["sleep", "5"]   # LB 에서 빠지는 시간

Probe 엔드포인트

@app.get("/health/live", include_in_schema=False)
def liveness():
    return {"status": "ok"}

@app.get("/health/ready", include_in_schema=False)
async def readiness(
    db: Annotated[AsyncSession, Depends(get_async_db)]
):
    await db.execute(text("SELECT 1"))
    return {"status": "ok"}

@app.get("/health/startup", include_in_schema=False)
def startup():
    return {"status": "ready"}
  • Liveness: 앱이 살아있는가 (fail 시 재시작)
  • Readiness: 트래픽을 받을 수 있는가 (fail 시 LB 에서 제거)
  • Startup: 초기화 중 (다른 probe 지연)

Reverse Proxy 설정

Nginx

upstream fastapi {
    server 127.0.0.1:8000;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

    location / {
        proxy_pass http://fastapi;
        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;
        proxy_set_header Upgrade $http_upgrade;         # WebSocket
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600;
        proxy_buffering off;                             # SSE
    }
}

FastAPI 는 --proxy-headers --forwarded-allow-ips="*" 로 X-Forwarded-* 신뢰.

AWS ALB

  • Health check path: /health/ready
  • Idle timeout: 60초 (WebSocket 은 확장)
  • Stickiness: WebSocket 이 프로세스 로컬 상태 유지 시

로깅 & 관측

구조화 로그

import logging
import json
from pythonjsonlogger import jsonlogger

handler = logging.StreamHandler()
handler.setFormatter(jsonlogger.JsonFormatter(
    "%(asctime)s %(name)s %(levelname)s %(message)s"
))
logging.basicConfig(handlers=[handler], level=logging.INFO)

CloudWatch, Datadog, Loki 등이 JSON 로그를 파싱 가능.

OpenTelemetry

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
opentelemetry-instrument uvicorn myapp.main:app

FastAPI, SQLAlchemy, HTTP client 등 자동 계측.

Sentry

import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration

sentry_sdk.init(
    dsn="https://xxx@sentry.io/yyy",
    traces_sample_rate=0.1,
    integrations=[FastApiIntegration()],
)

환경 변수 & Secrets

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    secret_key: str
    debug: bool = False

    class Config:
        env_file = ".env"

Kubernetes: Secret 리소스로 마운트. AWS: Secrets Manager / SSM Parameter Store.

성능 팁

  1. --http httptools, --loop uvloop: Uvicorn 기본이지만 uvicorn[standard] 필요
  2. --limit-concurrency N: 동시 요청 상한 (backpressure)
  3. --limit-max-requests N: worker 재시작 트리거 (메모리 누수 완화)
  4. Prefork with LazyLoad: ML 모델은 fork 후 로드 (COW 활용) 또는 각 worker 개별 로드
  5. HTTP/2: Uvicorn 은 미지원. Hypercorn 사용 또는 프록시 (Nginx/Envoy) 에서 종단

함정

WARNING

--reload 는 개발만. 프로덕션에서 코드 변경 감시로 부하 + 리소스 누수.

CAUTION

worker_class 잘못 지정. Sync worker (sync) 는 FastAPI 부적합. 반드시 uvicorn.workers.UvicornWorker.

WARNING

Signal 처리 misconfig. Gunicorn --graceful-timeout < Kubernetes terminationGracePeriodSeconds 여야 in-flight 요청 완료.

IMPORTANT

모델 로딩은 lifespan startup. Endpoint 별 매 요청 로드는 재앙. 앱 시작 시 한 번, app.state 에 저장.

CAUTION

컨테이너 안에서 USER root 유지 위험. non-root user 사용, non-privileged port (8000+) 사용.

관련 위키

이 글의 용어 (9개)
[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…
[Container] OCI Image: spec, manifest, layer 표준cloud
정의 OCI (Open Container Initiative) = 컨테이너 표준 (Linux Foundation, 2015). image format + runtime + dis…
[FastAPI] Async / Sync Endpointsfastapi
정의 FastAPI 는 ASGI 프레임워크 이므로 endpoint 를 로 정의해 이벤트 루프에서 실행하거나, 로 정의해 스레드풀에서 실행할 수 있습니다. 어느 쪽을 선택하는지가 …
[FastAPI] Middlewarefastapi
정의 FastAPI Middleware 는 모든 요청과 응답을 가로채 로깅, 인증, 압축, CORS 등 공통 처리를 하는 계층입니다. Starlette 의 ASGI middlew…
[FastAPI] WebSocketsfastapi
정의 FastAPI WebSockets 는 Starlette 위에서 양방향 실시간 통신을 제공합니다. HTTP 대신 / 스킴을 쓰며, 한 연결 위에서 서버와 클라이언트가 자유롭게…
[Python] FastAPIfastapi
정의 FastAPI 는 Python 3.8+ 을 위한 ASGI 기반 현대 웹/API 프레임워크 입니다. Sebastián Ramírez (tiangolo) 가 2018년 발표했고…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기