KR_Docker - somaz94/python-study GitHub Wiki
ํ์ด์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ์ํ ๋์ปค ์ค์ ์ด๋ค.
# ๊ธฐ๋ณธ Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
โ
ํน์ง:
- ๊ฒฝ๋ ๋ฒ ์ด์ค ์ด๋ฏธ์ง
- ์์กด์ฑ ๊ด๋ฆฌ
- ์์ ๋๋ ํ ๋ฆฌ ์ค์
- ๊ณ์ธต์ ๋น๋ ๊ตฌ์กฐ
- ์ด๋ฏธ์ง ์บ์ฑ ํ์ฉ
# ์ด๋ฏธ์ง ๋น๋
docker build -t myapp:latest .
# ์ปจํ
์ด๋ ์คํ
docker run -p 5000:5000 myapp:latest
# ์ปจํ
์ด๋ ๋ชฉ๋ก ํ์ธ
docker ps
# ๋ก๊ทธ ํ์ธ
docker logs <container_id>
# ์ปจํ
์ด๋ ์ค์ง
docker stop <container_id>
# ์ด๋ฏธ์ง ๋ชฉ๋ก ํ์ธ
docker images
๋์ปค๋ฅผ ์ฌ์ฉํ ํ์ด์ฌ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ ๋ฐฉ๋ฒ์ด๋ค.
# ๊ฐ๋ฐ์ฉ Dockerfile
FROM python:3.9-slim
WORKDIR /app
# ๊ฐ๋ฐ ๋๊ตฌ ์ค์น
RUN apt-get update && apt-get install -y \
git \
curl \
vim \
gcc \
&& rm -rf /var/lib/apt/lists/*
# ์์กด์ฑ ์ค์น
COPY requirements.txt requirements-dev.txt ./
RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
# ์์ค ์ฝ๋ ๋ณต์ฌ
COPY . .
# ๊ฐ๋ฐ ์๋ฒ ์คํ
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
โ
ํน์ง:
- ๊ฐ๋ฐ ๋๊ตฌ ํฌํจ
- ํซ ๋ฆฌ๋ก๋ฉ
- ๋๋ฒ๊น ์ง์
- ๋ณผ๋ฅจ ๋ง์ดํธ๋ก ์ฝ๋ ๋ณ๊ฒฝ ์ค์๊ฐ ๋ฐ์
- ๊ฐ๋ฐ์ฉ ํ๊ฒฝ๋ณ์ ์ค์
// .devcontainer/devcontainer.json
{
"name": "Python Dev Environment",
"dockerFile": "../Dockerfile.dev",
"context": "..",
"extensions": [
"ms-python.python",
"ms-azuretools.vscode-docker",
"ms-python.vscode-pylance"
],
"forwardPorts": [8000],
"postCreateCommand": "pip install --user -e .",
"remoteUser": "vscode",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"python.pythonPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true
}
}
์ฌ๋ฌ ์๋น์ค๋ก ๊ตฌ์ฑ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ๊ด๋ฆฌํ๋ Docker Compose ์ค์ ์ด๋ค.
# docker-compose.yml
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
container_name: python_web
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- .:/app
- static_data:/app/static
environment:
- FLASK_ENV=development
- DATABASE_URL=postgresql://user:password@db:5432/dbname
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
networks:
- app_network
db:
image: postgres:13
container_name: python_db
restart: unless-stopped
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=dbname
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- app_network
redis:
image: redis:6-alpine
container_name: python_redis
restart: unless-stopped
volumes:
- redis_data:/data
networks:
- app_network
volumes:
postgres_data:
redis_data:
static_data:
networks:
app_network:
driver: bridge
โ
ํน์ง:
- ์๋น์ค ์ ์
- ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
- ๋ณผ๋ฅจ ๋ง์ดํธ
- ๋คํธ์ํฌ ์ค์
- ์๋น์ค ๊ฐ ์์กด์ฑ ์ ์
# ์๋น์ค ์์
docker-compose up -d
# ์๋น์ค ์ํ ํ์ธ
docker-compose ps
# ์๋น์ค ๋ก๊ทธ ํ์ธ
docker-compose logs -f web
# ํน์ ์๋น์ค ์ฌ์์
docker-compose restart web
# ์๋น์ค ์ค์ง
docker-compose stop
# ์๋น์ค ์ ๊ฑฐ (๋ณผ๋ฅจ ํฌํจ)
docker-compose down -v
๋์ปค ์ด๋ฏธ์ง ์ต์ ํ๋ฅผ ์ํ ๋ฉํฐ์คํ
์ด์ง ๋น๋ ๊ตฌ์ฑ์ด๋ค.
# ๋ฉํฐ์คํ
์ด์ง Dockerfile
# ๋น๋ ์คํ
์ด์ง
FROM python:3.9-slim as builder
WORKDIR /app
# ๋น๋ ์์กด์ฑ ์ค์น
RUN apt-get update && apt-get install -y \
gcc \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# ์ต์ข
์คํ
์ด์ง
FROM python:3.9-slim
WORKDIR /app
# ํ์ํ ํจํค์ง๋ง ์ค์น
RUN apt-get update && apt-get install -y \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# ๋น๋ ์คํ
์ด์ง์์ ์์ฑ๋ ํ ํ์ผ ๋ณต์ฌ
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \
&& rm -rf /wheels
# ์ ํ๋ฆฌ์ผ์ด์
์ฝ๋ ๋ณต์ฌ
COPY . .
# ๋น๊ถํ ์ฌ์ฉ์ ์ค์
RUN adduser --disabled-password --gecos '' appuser
USER appuser
# ์คํ ๋ช
๋ น
EXPOSE 8000
CMD ["gunicorn", "app:app", "-w", "4", "-b", "0.0.0.0:8000"]
โ
ํน์ง:
- ์ด๋ฏธ์ง ํฌ๊ธฐ ์ต์ ํ
- ๋น๋ ์บ์ ํ์ฉ
- ๋ณด์์ฑ ํฅ์
- ์์กด์ฑ ๊ณ์ธต ๋ถ๋ฆฌ
- ์คํ ํ๊ฒฝ ์ต์ํ
# ์ผ๋ฐ ๋น๋
$ docker build -t myapp:standard -f Dockerfile.standard .
$ docker images myapp:standard
REPOSITORY TAG SIZE
myapp standard 1.2GB
# ๋ฉํฐ์คํ
์ด์ง ๋น๋
$ docker build -t myapp:multistage -f Dockerfile.multistage .
$ docker images myapp:multistage
REPOSITORY TAG SIZE
myapp multistage 450MB
ํ์ด์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ๋์ปค ์ด๋ฏธ์ง๋ฅผ ์ต์ ํํ๋ ๋ฐฉ๋ฒ์ด๋ค.
# ์ต์ ํ๋ Dockerfile
FROM python:3.9-slim
# ํ๊ฒฝ ๋ณ์ ์ค์
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_VERSION=1.1.14 \
POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_CREATE=false
# ์์
๋๋ ํ ๋ฆฌ ์ค์
WORKDIR /app
# ์์คํ
์์กด์ฑ ์ค์น
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gcc \
postgresql-client \
curl \
&& rm -rf /var/lib/apt/lists/*
# Poetry ์ค์น ๋ฐ ์์กด์ฑ ๊ด๋ฆฌ
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="/root/.local/bin:$PATH"
# Poetry ์์กด์ฑ ํ์ผ ๋ณต์ฌ ๋ฐ ์ค์น
COPY pyproject.toml poetry.lock* ./
RUN poetry install --no-dev --no-root
# ์์ค ์ฝ๋ ๋ณต์ฌ
COPY . .
# ์ฌ์ ์ปดํ์ผ
RUN python -m compileall /app
# ํฌํธ ์ค์
EXPOSE 8000
# ํฌ์ค์ฒดํฌ
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# ์คํ ๋ช
๋ น
CMD ["gunicorn", "app:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]
โ
ํน์ง:
- ์ฑ๋ฅ ์ต์ ํ
- ์บ์ ๋ ์ด์ด ๊ด๋ฆฌ
- ๋ณด์ ์ค์
- ์ต์ ๊ถํ ์์น ์ ์ฉ
- ํฌ์ค์ฒดํฌ ๊ตฌํ
# Trivy๋ฅผ ์ฌ์ฉํ ๋ณด์ ์ทจ์ฝ์ ์ค์บ
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:latest
# ์ด๋ฏธ์ง ๋ ์ด์ด ๋ถ์
docker history myapp:latest
๋์ปค๋ฅผ ์ฌ์ฉํ ํ๋ก๋์
ํ๊ฒฝ ๊ตฌ์ฑ๊ณผ ๋ฐฐํฌ ์ ๋ต์ด๋ค.
# docker-compose.prod.yml
version: '3.8'
services:
web:
image: myapp:${VERSION:-latest}
restart: always
expose:
- "8000"
environment:
- DATABASE_URL=postgresql://user:${DB_PASSWORD}@db:5432/myapp
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY=${SECRET_KEY}
- ENVIRONMENT=production
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
depends_on:
- db
- redis
networks:
- app_network
deploy:
replicas: 3
update_config:
parallelism: 1
delay: 10s
order: start-first
db:
image: postgres:13
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backups:/backups
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=myapp
networks:
- app_network
deploy:
resources:
limits:
cpus: '1'
memory: 1G
placement:
constraints: [node.role == manager]
redis:
image: redis:6-alpine
restart: always
volumes:
- redis_data:/data
command: redis-server --appendonly yes
networks:
- app_network
nginx:
image: nginx:latest
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
- static_volume:/var/www/static
depends_on:
- web
networks:
- app_network
certbot:
image: certbot/certbot
volumes:
- ./nginx/ssl:/etc/letsencrypt
- ./nginx/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
volumes:
postgres_data:
redis_data:
static_volume:
networks:
app_network:
#!/bin/bash
# deploy.sh
# ํ๊ฒฝ ๋ณ์ ๋ก๋
set -a
source .env.prod
set +a
# ์ด๋ฏธ์ง ๋น๋ ๋๋ ๊ฐ์ ธ์ค๊ธฐ
if [ "$1" == "build" ]; then
echo "Building new image..."
docker build -t myapp:$VERSION .
docker tag myapp:$VERSION myregistry.com/myapp:$VERSION
docker push myregistry.com/myapp:$VERSION
else
echo "Pulling image..."
docker pull myregistry.com/myapp:$VERSION
fi
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฑ์
echo "Backing up database..."
docker-compose exec db pg_dump -U user myapp > ./backups/myapp_$(date +%Y%m%d_%H%M%S).sql
# ๋ฐฐํฌ
echo "Deploying version $VERSION..."
docker-compose -f docker-compose.prod.yml down
docker-compose -f docker-compose.prod.yml up -d
# ๋ฐฐํฌ ํ์ธ
echo "Checking deployment..."
sleep 10
docker-compose -f docker-compose.prod.yml ps
curl -s http://localhost/health | grep -q "ok" && echo "Deployment successful!" || echo "Deployment failed!"
โ
ํน์ง:
- ํ๊ฒฝ ๋ถ๋ฆฌ
- ๋กค๋ง ์ ๋ฐ์ดํธ ์ ๋ต
- ๋ฆฌ์์ค ์ ํ ์ค์
- ๋ก๋ ๋ฐธ๋ฐ์ฑ
- SSL ์ธ์ฆ์ ์๋ํ
๋์ปคํ๋ ํ์ด์ฌ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น
๊ตฌ์ฑ์ด๋ค.
# docker-compose.monitor.yml
version: '3.8'
services:
web:
# ๊ธฐ์กด web ์๋น์ค์ ์ถ๊ฐ
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
prometheus:
image: prom/prometheus
volumes:
- ./prometheus:/etc/prometheus
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- app_network
grafana:
image: grafana/grafana
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
ports:
- "3000:3000"
networks:
- app_network
loki:
image: grafana/loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./loki:/etc/loki
networks:
- app_network
promtail:
image: grafana/promtail
volumes:
- /var/log:/var/log
- ./promtail:/etc/promtail
command: -config.file=/etc/promtail/config.yml
networks:
- app_network
volumes:
prometheus_data:
grafana_data:
# app.py (Prometheus ํตํฉ ์์ )
from flask import Flask
from prometheus_flask_exporter import PrometheusMetrics
import logging
from logging.handlers import RotatingFileHandler
import os
app = Flask(__name__)
metrics = PrometheusMetrics(app)
# ์ปค์คํ
๋ฉํธ๋ฆญ ์ ์
request_count = metrics.counter(
'request_count', 'App Request Count',
labels={'endpoint': lambda: request.endpoint}
)
# ๋ก๊น
์ค์
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240, backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')
@app.route('/')
@request_count
def index():
app.logger.info('Index page requested')
return 'Hello, World!'
@app.route('/health')
def health():
return {"status": "ok"}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)
โ
ํน์ง:
- ์ค์ ์ง์ค์ ๋ก๊น
- ๋ฉํธ๋ฆญ ์์ง ๋ฐ ์๊ฐํ
- ์๋ฆผ ์ค์
- ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
- ์ปจํ ์ด๋ ์ํ ์ถ์
โ
๋ชจ๋ฒ ์ฌ๋ก:
- ๋ ์ด์ด ์ต์ํ: ๊ด๋ จ RUN ๋ช ๋ น์ ๊ฒฐํฉํ์ฌ ์ด๋ฏธ์ง ํฌ๊ธฐ ์ต์ ํ
- ์บ์ ํ์ฉ: ๋ณ๊ฒฝ์ด ์ ์ ๋ ์ด์ด๋ฅผ ๋จผ์ ๋ฐฐ์นํ์ฌ ๋น๋ ์๊ฐ ๋จ์ถ
- ๋ณด์ ๊ณ ๋ ค์ฌํญ ์ค์: ๋ฃจํธ ๊ถํ ์ฌ์ฉ ์ง์, ์ทจ์ฝ์ ์ค์บ๋ ์ค์
- ํ๊ฒฝ ๋ณ์ ํ์ฉ: ์ค์ ๊ฐ์ ํ๊ฒฝ ๋ณ์๋ก ๋ถ๋ฆฌํ์ฌ ์ด์์ฑ ํฅ์
- ๋ณผ๋ฅจ ๋ง์ดํธ ์ ๋ต: ๋ฐ์ดํฐ ์ง์์ฑ๊ณผ ์ฑ๋ฅ์ ๊ณ ๋ คํ ๋ณผ๋ฅจ ์ค๊ณ
- ๋คํธ์ํฌ ์ค์ : ์๋น์ค ๊ฐ ํต์ ๋ณด์๊ณผ ๊ฒฉ๋ฆฌ๋ฅผ ์ํ ๋คํธ์ํฌ ์ค๊ณ
- ๋ก๊น ์ค์ : ๊ตฌ์กฐํ๋ ๋ก๊ทธ ํ์๊ณผ ์ค์ ์ง์ค์ ๋ก๊ทธ ์์ง ๊ตฌ์ฑ
- ๋ชจ๋ํฐ๋ง ๊ตฌ์ฑ: ์ปจํ ์ด๋ ์ํ์ ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
- CI/CD ํตํฉ: ์๋ํ๋ ๋น๋, ํ ์คํธ ๋ฐ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ
- ์ปจํ ์ด๋ ์ค์ผ์คํธ๋ ์ด์ : ํ์ฅ์ฑ๊ณผ ๊ฐ์ฉ์ฑ์ ์ํ ์ค์ผ์คํธ๋ ์ด์ ๋๊ตฌ ํ์ฉ
- ํจ์จ์ ์ธ ์ข ์์ฑ ๊ด๋ฆฌ: Poetry ๋๋ Pipenv๋ฅผ ์ฌ์ฉํ ์์กด์ฑ ๊ด๋ฆฌ
- ์ ์ ํ ๊ธฐ๋ณธ ์ด๋ฏธ์ง ์ ํ: ์ ํ๋ฆฌ์ผ์ด์ ์๊ตฌ์ฌํญ์ ๋ง๋ ์ ์ ํ ๋ฒ ์ด์ค ์ด๋ฏธ์ง ์ฌ์ฉ
- ํฌ์ค์ฒดํฌ ๊ตฌํ: ์ปจํ ์ด๋ ์ํ ๋ชจ๋ํฐ๋ง์ ์ํ ํฌ์ค์ฒดํฌ ์๋ํฌ์ธํธ ์ ๊ณต
- ์ด๋ฏธ์ง ํ๊น ์ ๋ต: ์๋ฉํฑ ๋ฒ์ ๋์ ํ์ฉํ ์ด๋ฏธ์ง ํ๊น ์ ๋ต ์๋ฆฝ
- ๋ฆฌ์์ค ์ ํ ์ค์ : ๋ฉ๋ชจ๋ฆฌ, CPU ์ฌ์ฉ๋ ์ ํ์ผ๋ก ์์ ์ฑ ํ๋ณด