Dockerfile, docker-compose, Gunicorn, Nginx, CI/CD, production settings
Контейнеризация решает старую боль «у меня на машине работает»: образ упаковывает приложение вместе с интерпретатором, зависимостями и системными библиотеками. Один и тот же образ проходит через CI, staging и production без расхождений.
💡 Правило: контейнер должен быть stateless и immutable. Всё изменяемое состояние (БД, загруженные файлы, кэш) — снаружи: в БД, S3, Redis, volume. Тогда контейнер можно убить и поднять заново в любой момент — основа горизонтального масштабирования.
Internet → Nginx (TLS, статика) → Gunicorn (N воркеров) → Django
│
┌───────────────┼───────────────┐
▼ ▼ ▼
PostgreSQL Redis Celery worker
| Слой | Зачем |
|---|---|
| Nginx | Терминация TLS, отдача статики/медиа, буферизация, rate limiting |
| Gunicorn | WSGI-сервер: запускает несколько воркеров с вашим Django |
| PostgreSQL | Основное хранилище (вынесено наружу контейнера приложения) |
| Redis | Кэш, брокер Celery, сессии |
⚠️
python manage.py runserverникогда не используется в production: один поток, нет защиты, отдаёт статику неэффективно. Только Gunicorn/Uvicorn за Nginx.
Наивный Dockerfile тащит в финальный образ компиляторы и dev-зависимости. Multi-stage сборка оставляет только нужное для запуска — образ меньше и безопаснее.
# ---- build stage ----
FROM python:3.11-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
# Системные зависимости для сборки (psycopg2 и т.п.)
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ---- runtime stage ----
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
# Только runtime-библиотека Postgres, без компиляторов
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Ставим заранее собранные wheels — быстро и без build-инструментов
COPY /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache /wheels/*
# Непривилегированный пользователь — не запускаем процесс от root
RUN useradd --create-home appuser
COPY . .
RUN chown -R appuser:appuser /app
USER appuser
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "60"]💡
.dockerignoreобязателен. Без него в образ попадут.git,__pycache__,.env, локальнаяdb.sqlite3,node_modules— это раздувает образ и может утечь как секрет.
# .dockerignore
.git
__pycache__/
*.pyc
.env
.venv/
db.sqlite3
media/
Частая ошибка — класть migrate в Dockerfile. Сборка образа не должна трогать БД (её ещё нет, и образ должен быть универсальным для всех окружений).
| Шаг | Где правильно |
|---|---|
collectstatic | В Dockerfile (build-time) или в entrypoint |
migrate | В entrypoint/деплой-хуке (run-time), один раз перед стартом |
#!/bin/sh
# entrypoint.sh — выполняется при старте контейнера
set -e
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec "$@" # передаём управление CMD (gunicorn)⚠️ При нескольких репликах
migrateдолжна выполняться только в одном месте (отдельная job/release-команда), иначе несколько контейнеров начнут мигрировать одновременно и подерутся за блокировки.
services:
web:
build: .
env_file: .env
depends_on:
db: { condition: service_healthy }
redis: { condition: service_started }
expose: ["8000"]
db:
image: postgres:15
volumes: [pgdata:/var/lib/postgresql/data]
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
retries: 5
redis:
image: redis:7-alpine
celery:
build: .
command: celery -A myproject worker -l info
env_file: .env
depends_on: [redis, db]
nginx:
image: nginx:alpine
ports: ["80:80", "443:443"]
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- static_volume:/app/static:ro
depends_on: [web]
volumes:
pgdata:
static_volume:💡
depends_onсcondition: service_healthyдожидается, пока Postgres реально готов принимать соединения, а не просто «контейнер запущен». Без healthcheckwebстартует раньше БД и падает.
# settings.py
import os
DEBUG = False # НИКОГДА не True в production
SECRET_KEY = os.environ['SECRET_KEY'] # падаем, если не задан — это хорошо
ALLOWED_HOSTS = os.environ['ALLOWED_HOSTS'].split(',')
# HTTPS-хардненинг
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# За Nginx-прокси — доверяем заголовку протокола
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')| Правило | Почему |
|---|---|
DEBUG = False | DEBUG=True отдаёт трейсбеки с секретами и кодом всему миру |
| Секреты — из env/secret manager | Секрет в git = скомпрометирован навсегда |
ALLOWED_HOSTS задан явно | Защита от Host-header атак |
Узкий ALLOWED_HOSTS, не ['*'] | * отключает проверку |
⚠️ Секреты не передаются через
ARGв Dockerfile — они остаются в слоях образа и истории сборки. Используйте переменные окружения в рантайме (env_file, secret manager: Vault, AWS Secrets Manager, Docker secrets).
# .github/workflows/deploy.yml (упрощённо)
jobs:
test:
steps:
- run: docker compose run --rm web python manage.py test
build-and-push:
needs: test
steps:
- run: docker build -t registry/app:${{ github.sha }} .
- run: docker push registry/app:${{ github.sha }}
deploy:
needs: build-and-push
steps:
- run: ssh prod "docker compose pull && docker compose up -d --no-deps web"💡 Тегируйте образы по git-SHA, а не только
latest. Это даёт воспроизводимость и мгновенный откат:docker compose upсо старым тегом.
.dockerignore и не тащите секреты/мусор в образmigrate в рантайме (один раз), а не при сборке образаdepends_on в composeDEBUG = False, секреты из env, ALLOWED_HOSTS задан явноSECURE_PROXY_SSL_HEADER за проксиВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Мониторинг и Логирование