본문으로 건너뛰기

095 Docker 컨테이너화

키워드: Docker, 컨테이너

개요

Docker는 애플리케이션을 컨테이너로 패키징하여 어디서나 동일하게 실행할 수 있게 합니다. ML 모델과 API 서버를 Docker 이미지로 만들어 일관된 배포 환경을 구축합니다.

실습 환경

  • Python 버전: 3.11 권장
  • 필요 도구: Docker Desktop, pycaret[full]>=3.0

프로젝트 구조

ml-api/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 앱
│ └── model/
│ └── diabetes_model.pkl
├── requirements.txt
├── Dockerfile
└── docker-compose.yml

requirements.txt

pycaret>=3.0.0
fastapi>=0.100.0
uvicorn>=0.23.0
pandas>=1.5.0
numpy>=1.23.0
scikit-learn>=1.2.0

기본 Dockerfile

# 095 Dockerfile
FROM python:3.11-slim

# 095 작업 디렉토리 설정
WORKDIR /app

# 095 시스템 의존성 설치
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*

# 095 파이썬 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 095 애플리케이션 코드 복사
COPY app/ ./app/

# 095 포트 노출
EXPOSE 8000

# 095 실행 명령
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

최적화된 Dockerfile

# 095 Dockerfile.optimized
# 095 멀티스테이지 빌드
FROM python:3.11-slim as builder

WORKDIR /app

# 095 빌드 의존성 설치
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*

# 095 가상환경 생성 및 의존성 설치
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 095 런타임 이미지
FROM python:3.11-slim

WORKDIR /app

# 095 가상환경 복사
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# 095 비루트 사용자 생성
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

# 095 애플리케이션 복사
COPY --chown=appuser:appuser app/ ./app/

EXPOSE 8000

# 095 헬스체크
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

app/main.py

# 095 app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import pandas as pd
import os

# 095 모델 로드
from pycaret.classification import load_model, predict_model

app = FastAPI(title="ML API", version="1.0.0")

# 095 모델 경로
MODEL_PATH = os.getenv('MODEL_PATH', 'app/model/diabetes_model')
model = load_model(MODEL_PATH)

class PatientData(BaseModel):
Pregnancies: float = Field(..., ge=0)
Glucose: float = Field(..., ge=0)
BloodPressure: float = Field(..., ge=0)
SkinThickness: float = Field(..., ge=0)
Insulin: float = Field(..., ge=0)
BMI: float = Field(..., ge=0)
DiabetesPedigreeFunction: float = Field(..., ge=0)
Age: float = Field(..., ge=0)

class PredictionResponse(BaseModel):
prediction: int
probability: float

@app.get("/health")
def health():
return {"status": "healthy"}

@app.post("/predict", response_model=PredictionResponse)
def predict(data: PatientData):
try:
input_df = pd.DataFrame([data.dict()])
result = predict_model(model, data=input_df)
return PredictionResponse(
prediction=int(result['prediction_label'].values[0]),
probability=float(result['prediction_score'].values[0])
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

Docker 빌드 및 실행

# 095 이미지 빌드
docker build -t ml-api:latest .

# 095 컨테이너 실행
docker run -d -p 8000:8000 --name ml-api ml-api:latest

# 095 로그 확인
docker logs ml-api

# 095 테스트
curl http://localhost:8000/health

# 095 컨테이너 중지 및 삭제
docker stop ml-api
docker rm ml-api

docker-compose.yml

# 095 docker-compose.yml
version: '3.8'

services:
ml-api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- MODEL_PATH=app/model/diabetes_model
- LOG_LEVEL=INFO
volumes:
- ./logs:/app/logs
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3

# 선택: 모니터링 추가
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml

grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
depends_on:
- prometheus

Docker Compose 명령

# 095 서비스 시작
docker-compose up -d

# 095 서비스 상태 확인
docker-compose ps

# 095 로그 확인
docker-compose logs -f ml-api

# 095 서비스 중지
docker-compose down

# 095 이미지 재빌드
docker-compose build --no-cache
docker-compose up -d

환경 변수 관리

# 095 Dockerfile - 환경 변수 설정
ENV MODEL_PATH=/app/model/diabetes_model
ENV LOG_LEVEL=INFO
ENV WORKERS=4
# 095 docker-compose.yml - 환경 변수 파일 사용
services:
ml-api:
build: .
env_file:
- .env
# 095 .env
MODEL_PATH=app/model/diabetes_model
LOG_LEVEL=INFO
WORKERS=4

볼륨 마운트

# 095 docker-compose.yml
services:
ml-api:
build: .
volumes:
# 모델 파일 마운트 (재배포 없이 모델 업데이트 가능)
- ./models:/app/model:ro
# 로그 영구 저장
- ./logs:/app/logs

멀티 컨테이너 구성

# 095 docker-compose.yml - 프로덕션 구성
version: '3.8'

services:
# 로드 밸런서
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- ml-api-1
- ml-api-2

# API 서버 인스턴스 1
ml-api-1:
build: .
environment:
- INSTANCE_ID=1
expose:
- "8000"

# API 서버 인스턴스 2
ml-api-2:
build: .
environment:
- INSTANCE_ID=2
expose:
- "8000"

# Redis 캐시
redis:
image: redis:alpine
ports:
- "6379:6379"

nginx.conf

# 095 nginx.conf
events {
worker_connections 1024;
}

http {
upstream ml_api {
server ml-api-1:8000;
server ml-api-2:8000;
}

server {
listen 80;

location / {
proxy_pass http://ml_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

location /health {
proxy_pass http://ml_api;
}
}
}

GPU 지원 Docker

# 095 Dockerfile.gpu
FROM nvidia/cuda:11.8-runtime-ubuntu22.04

# 095 Python 설치
RUN apt-get update && apt-get install -y \
python3.11 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# 095 docker-compose.gpu.yml
services:
ml-api-gpu:
build:
dockerfile: Dockerfile.gpu
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
ports:
- "8000:8000"

이미지 크기 최적화

# 095 .dockerignore
__pycache__
*.pyc
*.pyo
.git
.gitignore
*.md
.env
.venv
tests/
docs/
*.log
# 095 이미지 크기 확인
docker images ml-api

# 095 레이어 분석
docker history ml-api:latest

# 095 슬림 이미지 사용
# 095 python:3.11-slim vs python:3.11 (약 800MB 차이)

프로덕션 체크리스트

# 095 docker-compose.prod.yml
version: '3.8'

services:
ml-api:
build:
context: .
dockerfile: Dockerfile.optimized
ports:
- "8000:8000"
environment:
- LOG_LEVEL=WARNING
- WORKERS=4
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '1'
memory: 2G
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

테스트 스크립트

# 095 test_docker.py
import requests
import time

def test_api():
base_url = "http://localhost:8000"

# 헬스 체크
response = requests.get(f"{base_url}/health")
assert response.status_code == 200
print("Health check: OK")

# 예측 테스트
patient = {
"Pregnancies": 6, "Glucose": 148, "BloodPressure": 72,
"SkinThickness": 35, "Insulin": 0, "BMI": 33.6,
"DiabetesPedigreeFunction": 0.627, "Age": 50
}

response = requests.post(f"{base_url}/predict", json=patient)
assert response.status_code == 200
result = response.json()
print(f"Prediction: {result}")

print("All tests passed!")

if __name__ == "__main__":
# 컨테이너 시작 대기
time.sleep(5)
test_api()

정리

  • Docker: 일관된 실행 환경 제공
  • Dockerfile: 이미지 빌드 정의
  • docker-compose: 멀티 컨테이너 관리
  • 최적화: 멀티스테이지 빌드, slim 이미지
  • 프로덕션: 리소스 제한, 로깅, 헬스체크

다음 글 예고

다음 글에서는 모니터링과 재학습을 다룹니다.


PyCaret 머신러닝 마스터 시리즈 #095