[이제와서 시작하는 Docker 마스터하기 - 고급편 #3] Docker로 Python 애플리케이션 배포
[이제와서 시작하는 Docker 마스터하기 - 고급편 #3] Docker로 Python 애플리케이션 배포
“이제와서 시작하는 Docker 마스터하기” Python은 Docker와 함께 사용하기 좋은 언어입니다. 이번 편에서는 Flask와 Django 애플리케이션을 Docker로 배포하는 방법을 실습과 함께 알아보겠습니다.
Python Docker 배포 전략 비교
| 프레임워크 | 특징 | 사용 시나리오 | 컨테이너 크기 | 성능 |
|---|---|---|---|---|
| Flask | 경량, 유연함 | 마이크로서비스, API | 50-100MB | 빠름 |
| Django | 풀스택, 기능 풍부 | 대규모 웹앱 | 150-300MB | 중간 |
| FastAPI | 비동기, 타입 힌트 | 고성능 API | 60-120MB | 매우 빠름 |
| Streamlit | 데이터 앱 | 대시보드, ML 데모 | 200-400MB | 중간 |
| Celery | 비동기 작업 | 백그라운드 처리 | 100-200MB | 해당 없음 |
Flask 애플리케이션 컨테이너화
1. Flask 애플리케이션 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# app.py
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
import prometheus_client
from prometheus_client import Counter, Histogram, Gauge
import time
import os
import logging
# 앱 초기화
app = Flask(__name__)
# 설정
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
'DATABASE_URL',
'postgresql://user:password@postgres:5432/myapp'
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['REDIS_URL'] = os.getenv('REDIS_URL', 'redis://redis:6379/0')
# 확장 초기화
db = SQLAlchemy(app)
redis_client = FlaskRedis(app)
# 로깅 설정
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Prometheus 메트릭
REQUEST_COUNT = Counter(
'app_request_count',
'Application Request Count',
['method', 'endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'app_request_latency_seconds',
'Application Request Latency'
)
ACTIVE_USERS = Gauge(
'app_active_users',
'Active Users'
)
# 모델 정의
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=db.func.current_timestamp())
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'created_at': self.created_at.isoformat()
}
# 미들웨어
@app.before_request
def before_request():
request.start_time = time.time()
@app.after_request
def after_request(response):
if hasattr(request, 'start_time'):
latency = time.time() - request.start_time
REQUEST_LATENCY.observe(latency)
REQUEST_COUNT.labels(
method=request.method,
endpoint=request.endpoint or 'unknown',
status=response.status_code
).inc()
return response
# 라우트
@app.route('/health')
def health_check():
"""헬스체크 엔드포인트"""
try:
# DB 연결 확인
db.session.execute('SELECT 1')
# Redis 연결 확인
redis_client.ping()
return jsonify({'status': 'healthy'}), 200
except Exception as e:
logger.error(f'Health check failed: {str(e)}')
return jsonify({'status': 'unhealthy', 'error': str(e)}), 503
@app.route('/api/items', methods=['GET'])
def get_items():
"""아이템 목록 조회"""
# Redis 캐시 확인
cached = redis_client.get('items_list')
if cached:
logger.info('Cache hit for items list')
return cached
# DB에서 조회
items = Item.query.order_by(Item.created_at.desc()).all()
result = jsonify([item.to_dict() for item in items])
# 캐시 저장 (60초)
redis_client.setex('items_list', 60, result.data)
return result
@app.route('/api/items', methods=['POST'])
def create_item():
"""아이템 생성"""
data = request.get_json()
item = Item(
name=data['name'],
description=data.get('description', '')
)
db.session.add(item)
db.session.commit()
# 캐시 무효화
redis_client.delete('items_list')
logger.info(f'Created item: {item.id}')
return jsonify(item.to_dict()), 201
@app.route('/metrics')
def metrics():
"""Prometheus 메트릭 엔드포인트"""
return prometheus_client.generate_latest()
# 데이터베이스 초기화
def init_db():
with app.app_context():
db.create_all()
logger.info('Database initialized')
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=5000)
2. requirements.txt
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-Redis==0.4.0
psycopg2-binary==2.9.7
prometheus-client==0.17.1
gunicorn==21.2.0
3. Flask Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
FROM python:3.11-slim as builder
# 빌드 의존성 설치
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 의존성 설치
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# 런타임 스테이지
FROM python:3.11-slim
# 런타임 의존성
RUN apt-get update && apt-get install -y \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# 사용자 생성
RUN useradd -m -u 1000 appuser
WORKDIR /app
# 빌더에서 패키지 복사
COPY --from=builder /root/.local /home/appuser/.local
# 애플리케이션 코드 복사
COPY --chown=appuser:appuser . .
# PATH 업데이트
ENV PATH=/home/appuser/.local/bin:$PATH
USER appuser
EXPOSE 5000
# Gunicorn으로 실행
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "120", "app:app"]
Django 애플리케이션 컨테이너화
1. Django 프로젝트 구조
1
2
3
4
5
6
7
8
9
10
11
12
13
14
django-app/
├── myproject/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── myapp/
│ ├── models.py
│ ├── views.py
│ └── urls.py
├── requirements.txt
├── manage.py
├── Dockerfile
└── docker-compose.yml
2. Django settings.py (Docker 설정)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost').split(',')
# 데이터베이스
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME', 'djangodb'),
'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', 'password'),
'HOST': os.getenv('DB_HOST', 'postgres'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
# Redis 캐시
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': os.getenv('REDIS_URL', 'redis://redis:6379/1'),
}
}
# Celery 설정
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://redis:6379/0')
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://redis:6379/0')
# 정적 파일
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
# 로깅
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console'],
'level': 'INFO',
},
}
3. Django Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
FROM python:3.11-slim as builder
# 빌드 의존성
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# 런타임 스테이지
FROM python:3.11-slim
# 런타임 의존성
RUN apt-get update && apt-get install -y \
libpq5 \
netcat-traditional \
&& rm -rf /var/lib/apt/lists/*
# 사용자 생성
RUN useradd -m -u 1000 django
WORKDIR /app
# 휠 파일과 requirements 복사
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
# 패키지 설치
RUN pip install --no-cache /wheels/*
# 애플리케이션 코드 복사
COPY --chown=django:django . .
# 정적 파일 디렉토리 생성
RUN mkdir -p staticfiles mediafiles
RUN chown -R django:django staticfiles mediafiles
USER django
# 정적 파일 수집
RUN python manage.py collectstatic --noinput
EXPOSE 8000
# entrypoint 스크립트
COPY --chown=django:django docker-entrypoint.sh .
RUN chmod +x docker-entrypoint.sh
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "myproject.wsgi:application"]
4. docker-entrypoint.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
set -e
# PostgreSQL이 준비될 때까지 대기
echo "Waiting for postgres..."
while ! nc -z $DB_HOST $DB_PORT; do
sleep 0.1
done
echo "PostgreSQL started"
# 마이그레이션 실행
python manage.py migrate --noinput
# 슈퍼유저 생성 (개발 환경)
if [ "$DJANGO_SUPERUSER_USERNAME" ]; then
python manage.py createsuperuser \
--noinput \
--username $DJANGO_SUPERUSER_USERNAME \
--email $DJANGO_SUPERUSER_EMAIL || true
fi
exec "$@"
Docker Compose 설정
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DEBUG=False
- SECRET_KEY=${SECRET_KEY:-your-secret-key}
- ALLOWED_HOSTS=localhost,127.0.0.1
- DB_HOST=postgres
- DB_NAME=djangodb
- DB_USER=postgres
- DB_PASSWORD=password
- REDIS_URL=redis://redis:6379/1
- CELERY_BROKER_URL=redis://redis:6379/0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- static_volume:/app/staticfiles
- media_volume:/app/mediafiles
networks:
- django-network
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=djangodb
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- django-network
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- django-network
celery:
build: .
command: celery -A myproject worker -l info
environment:
- DEBUG=False
- SECRET_KEY=${SECRET_KEY:-your-secret-key}
- DB_HOST=postgres
- DB_NAME=djangodb
- DB_USER=postgres
- DB_PASSWORD=password
- CELERY_BROKER_URL=redis://redis:6379/0
depends_on:
- redis
- postgres
networks:
- django-network
celery-beat:
build: .
command: celery -A myproject beat -l info
environment:
- DEBUG=False
- SECRET_KEY=${SECRET_KEY:-your-secret-key}
- DB_HOST=postgres
- DB_NAME=djangodb
- DB_USER=postgres
- DB_PASSWORD=password
- CELERY_BROKER_URL=redis://redis:6379/0
depends_on:
- redis
- postgres
networks:
- django-network
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- static_volume:/app/staticfiles:ro
- media_volume:/app/mediafiles:ro
depends_on:
- web
networks:
- django-network
volumes:
postgres_data:
redis_data:
static_volume:
media_volume:
networks:
django-network:
nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream django {
server web:8000;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://django;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /app/staticfiles/;
}
location /media/ {
alias /app/mediafiles/;
}
}
}
Python 특화 최적화
Python 이미지 최적화 비교
| 베이스 이미지 | 크기 | 장점 | 단점 | 용도 |
|---|---|---|---|---|
| python:3.11 | ~950MB | 모든 도구 포함 | 크기가 큼 | 개발 환경 |
| python:3.11-slim | ~150MB | 균형잡힌 크기 | 일부 도구 누락 | 일반 프로덕션 |
| python:3.11-alpine | ~50MB | 매우 작음 | 빌드 도구 필요 | 최소 프로덕션 |
| distroless/python3 | ~52MB | 보안 강화 | 쉘 없음 | 고보안 환경 |
| chainguard/python | ~45MB | 취약점 최소화 | 제한적 | 엔터프라이즈 |
1. 멀티스테이지 빌드 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 의존성 캐싱을 위한 별도 스테이지
FROM python:3.11-slim as python-deps
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --upgrade pip setuptools wheel
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt
# 애플리케이션 스테이지
FROM python:3.11-slim
# 휠 파일 복사 및 설치
COPY --from=python-deps /wheels /wheels
RUN pip install --no-cache /wheels/* && rm -rf /wheels
# 나머지 설정...
2. Poetry 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM python:3.11-slim as builder
RUN pip install poetry==1.6.1
WORKDIR /app
COPY pyproject.toml poetry.lock ./
# 의존성 설치
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi --no-root --only main
# 런타임 스테이지
FROM python:3.11-slim
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
WORKDIR /app
COPY . .
CMD ["gunicorn", "app:app"]
3. 비동기 애플리케이션 (FastAPI)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# main.py
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
import uvicorn
app = FastAPI()
# 비동기 DB 설정
engine = create_async_engine(
"postgresql+asyncpg://user:password@postgres/db"
)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/items")
async def get_items(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Item))
items = result.scalars().all()
return items
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
1
2
3
4
5
6
7
8
9
10
11
# FastAPI Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
개발 환경 설정
docker-compose.dev.yml
1
2
3
4
5
6
7
8
9
10
11
12
version: '3.8'
services:
web:
build: .
command: python manage.py runserver 0.0.0.0:8000
volumes:
- .:/app
environment:
- DEBUG=True
ports:
- "8000:8000"
성능 모니터링
APM 통합 (New Relic)
1
2
3
# settings.py
if os.getenv('NEW_RELIC_LICENSE_KEY'):
NEW_RELIC_CONFIG_FILE = 'newrelic.ini'
1
2
3
4
5
# Dockerfile에 추가
RUN pip install newrelic
# CMD 수정
CMD ["newrelic-admin", "run-program", "gunicorn", "myproject.wsgi:application"]
트러블슈팅
일반적인 문제 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 로그 확인
docker compose logs -f web
# 쉘 접속
docker compose exec web bash
# Python 쉘
docker compose exec web python manage.py shell
# 마이그레이션 문제
docker compose exec web python manage.py makemigrations
docker compose exec web python manage.py migrate
# 정적 파일 문제
docker compose exec web python manage.py collectstatic --noinput
보안 고려사항
1. 시크릿 관리
1
2
3
4
5
6
7
8
9
# settings.py
import os
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.getenv('SECRET_KEY')
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable is not set")
2. 프로덕션 체크리스트
1
2
3
4
5
6
# Django 보안 체크
docker compose exec web python manage.py check --deploy
# 취약점 스캔
docker run --rm -v $(pwd):/src \
pyupio/safety safety check --file /src/requirements.txt
2025년 Python Docker 배포 트렌드
컨테이너화 도구 비교
| 도구 | 특징 | 장점 | 단점 | 2025년 트렌드 |
|---|---|---|---|---|
| Docker | 표준 컨테이너 | 범용성 | 단일 호스트 | 여전히 주류 |
| Podman | Rootless | 보안 강화 | 생태계 작음 | 성장 중 |
| Buildah | OCI 빌더 | 유연함 | 복잡함 | 특수 용도 |
| Kaniko | 데몬리스 빌드 | CI/CD 최적화 | Docker 비호환 | K8s 환경 |
| Pack | Cloud Native Buildpacks | 자동 최적화 | 제한적 커스터마이징 | 급성장 |
Python 앱 배포 아키텍처
graph TB
subgraph "Development"
A[로컬 개발] --> B[테스트]
B --> C[이미지 빌드]
end
subgraph "CI/CD"
C --> D[자동 테스트]
D --> E[보안 스캔]
E --> F[이미지 푸시]
end
subgraph "Registry"
F --> G[Docker Hub]
F --> H[Private Registry]
F --> I[Cloud Registry]
end
subgraph "Deployment"
G --> J[개발 환경]
H --> K[스테이징]
I --> L[프로덕션]
J --> M[모니터링]
K --> M
L --> M
end
M --> N[로그 수집]
M --> O[메트릭 수집]
M --> P[알림]
마무리
Python 애플리케이션을 Docker로 배포하면 의존성 관리가 쉬워지고 일관된 환경을 유지할 수 있습니다. Flask의 단순함부터 Django의 풍부한 기능까지, Docker는 모든 Python 프로젝트에 적용할 수 있습니다. 다음 편에서는 Docker로 데이터베이스를 운영하는 방법을 알아보겠습니다.
다음 편 예고
- 프로덕션 데이터베이스 운영
- 백업과 복원 전략
- 고가용성 구성
- 성능 튜닝
데이터베이스도 Docker로 안전하게 운영해봅시다! 🗄️
📚 Docker 마스터하기 시리즈
🐳 기초편 (입문자용 - 5편)
💼 실전편 (중급자용 - 6편)
- Docker 네트워크 기초
- Docker 볼륨과 데이터 관리
- Docker Compose 입문
- 멀티 컨테이너 애플리케이션
- Docker Hub 활용하기
- Docker 보안 베스트 프랙티스
🚀 고급편 (전문가용 - 9편)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.