
Правильный деплой Django-приложения — это фундамент надежного продакшена. В этом руководстве разберем три проверенных подхода с реальными конфигурациями, которые используются в production.
Подходит для стартапов и MVP с трафиком до 50-100k пользователей в месяц. Простой, понятный и надежный подход.
Internet → Nginx (80/443) → Gunicorn (8000) → Django App
↓
PostgreSQL (5432)
Redis (6379)
Компоненты:
# gunicorn_config.py
import multiprocessing
import os
# Биндинг на localhost (за Nginx)
bind = "127.0.0.1:8000"
# Расчет воркеров: (2 × CPU) + 1
# Для 2 CPU = 5 воркеров, для 4 CPU = 9 воркеров
workers = multiprocessing.cpu_count() * 2 + 1
# Тип воркера
# sync - для CPU-bound задач (стандартный Django)
# gevent/eventlet - для I/O-bound (много внешних API)
worker_class = "sync"
# Для async views в Django 4.1+ используйте:
# worker_class = "uvicorn.workers.UvicornWorker"
# Максимум одновременных подключений на воркер
worker_connections = 1000
# Автоматический перезапуск воркера после N запросов
# Предотвращает утечки памяти
max_requests = 1000
max_requests_jitter = 50 # Рандомизация для равномерной нагрузки
# Таймауты
timeout = 30 # Убить воркер если запрос выполняется > 30 сек
graceful_timeout = 30 # Время на завершение текущих запросов при reload
keepalive = 2 # Keep-alive соединения
# Логирование
errorlog = "/var/log/gunicorn/error.log"
accesslog = "/var/log/gunicorn/access.log"
loglevel = "info" # debug, info, warning, error, critical
# Безопасность
limit_request_line = 4094 # Максимальная длина HTTP request line
limit_request_fields = 100 # Максимум HTTP заголовков
limit_request_field_size = 8190 # Максимальный размер заголовка
# PID file для управления процессом
pidfile = "/var/run/gunicorn/gunicorn.pid"
# Preload app для экономии памяти (но сложнее hot reload)
preload_app = True
# Hooks для логирования
def on_starting(server):
"""Вызывается при старте master процесса"""
server.log.info("Gunicorn master процесс запущен")
def when_ready(server):
"""Вызывается когда сервер готов принимать запросы"""
server.log.info("Сервер готов принимать запросы")
def on_reload(server):
"""Вызывается при reload конфигурации"""
server.log.info("Конфигурация перезагружена")
Создание директорий:
sudo mkdir -p /var/log/gunicorn
sudo mkdir -p /var/run/gunicorn
sudo chown -R www-data:www-data /var/log/gunicorn
sudo chown -R www-data:www-data /var/run/gunicorn
Запуск и управление:
# Запуск
gunicorn myproject.wsgi:application -c gunicorn_config.py
# Graceful restart (без downtime)
kill -HUP $(cat /var/run/gunicorn/gunicorn.pid)
# Graceful shutdown
kill -TERM $(cat /var/run/gunicorn/gunicorn.pid)
# Проверка количества воркеров
ps aux | grep gunicorn
# /etc/systemd/system/django.service
[Unit]
Description=Django Application (Gunicorn WSGI Server)
Documentation=https://docs.djangoproject.com/
After=network.target postgresql.service redis.service
Requires=postgresql.service
Wants=redis.service
[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/var/www/myproject
# Environment
Environment="PATH=/var/www/myproject/venv/bin"
Environment="DJANGO_SETTINGS_MODULE=myproject.settings.production"
EnvironmentFile=/var/www/myproject/.env
# Основная команда запуска
ExecStart=/var/www/myproject/venv/bin/gunicorn \
--config /var/www/myproject/gunicorn_config.py \
myproject.wsgi:application
# Graceful reload при изменении кода
ExecReload=/bin/kill -s HUP $MAINPID
# Политика перезапуска
Restart=always
RestartSec=10
StartLimitInterval=0
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/myproject/media /var/log/gunicorn /var/run/gunicorn
# Resource limits
LimitNOFILE=65535
MemoryLimit=2G
CPUQuota=90%
# Логирование
StandardOutput=journal
StandardError=journal
SyslogIdentifier=django-app
[Install]
WantedBy=multi-user.target
Управление сервисом:
# Включить автозапуск
sudo systemctl enable django
# Запустить
sudo systemctl start django
# Проверить статус
sudo systemctl status django
# Просмотр логов
sudo journalctl -u django -f
# Перезапустить gracefully
sudo systemctl reload django
# Полный перезапуск
sudo systemctl restart django
# Остановить
sudo systemctl stop django
# /etc/nginx/sites-available/django
# Upstream для балансировки нагрузки
upstream django_backend {
# Для одного Gunicorn процесса
server 127.0.0.1:8000 fail_timeout=30s max_fails=3;
# Для нескольких Gunicorn процессов (масштабирование):
# server 127.0.0.1:8000 weight=1 fail_timeout=30s;
# server 127.0.0.1:8001 weight=1 fail_timeout=30s;
keepalive 32; # Keep-alive соединения к backend
}
# Редирект HTTP → HTTPS
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# ACME challenge для Let's Encrypt
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Редирект всех остальных запросов
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS сервер
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
# SSL сертификаты
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# SSL оптимизация (Mozilla Intermediate)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always;
# Размеры загрузок
client_max_body_size 20M;
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 16k;
# Таймауты
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 65;
send_timeout 10;
# Gzip компрессия
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/rss+xml font/truetype font/opentype
application/vnd.ms-fontobject image/svg+xml;
# Логирование
access_log /var/log/nginx/django_access.log combined buffer=32k flush=5s;
error_log /var/log/nginx/django_error.log warn;
# Статические файлы с агрессивным кешированием
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
# Компрессия статики
gzip_static on;
# Безопасность
location ~* \.(py|pyc|pyo|pyd|log|ini|cfg|md|txt)$ {
deny all;
}
}
# Медиафайлы с умеренным кешированием
location /media/ {
alias /var/www/myproject/media/;
expires 30d;
add_header Cache-Control "public";
# Защита от выполнения скриптов
location ~ \.php$ {
deny all;
}
}
# Защита служебных файлов
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Health check endpoint
location /health/ {
proxy_pass http://django_backend;
access_log off;
}
# Django приложение
location / {
proxy_pass http://django_backend;
proxy_redirect off;
# Заголовки для Django
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 X-Forwarded-Host $server_name;
# Таймауты
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Буферизация
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
# WebSocket support (если используется Channels)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Rate limiting для API endpoints
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://django_backend;
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;
}
}
# Rate limiting зоны (добавить в http {} блок в nginx.conf)
# limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
Активация конфигурации:
# Проверка синтаксиса
sudo nginx -t
# Создание симлинка
sudo ln -s /etc/nginx/sites-available/django /etc/nginx/sites-enabled/
# Перезагрузка Nginx
sudo systemctl reload nginx
# Проверка статуса
sudo systemctl status nginx
# Установка Certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx
# Получение сертификата (автоматическая настройка Nginx)
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Или вручную без изменения конфигов
sudo certbot certonly --nginx -d yourdomain.com -d www.yourdomain.com
# Тестовый запуск для проверки
sudo certbot --nginx --dry-run -d yourdomain.com
# Автоматическое обновление (добавляется автоматически в cron)
sudo certbot renew --dry-run
# Проверка таймера systemd для обновления
sudo systemctl status certbot.timer
# Принудительное обновление
sudo certbot renew --force-renewal
Результат: Надежная инфраструктура за 2-4 часа с автоматическим управлением процессами.
Идеален для команд 2-10 разработчиков. Полная изоляция окружения, легкое масштабирование.
# Dockerfile
# Stage 1: Builder - сборка зависимостей
FROM python:3.11-slim as builder
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
# Установка системных зависимостей для сборки
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Установка Python зависимостей в виртуальное окружение
COPY requirements.txt .
RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip setuptools wheel && \
/opt/venv/bin/pip install -r requirements.txt
# Stage 2: Runtime - минимальный финальный образ
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/opt/venv/bin:$PATH" \
DJANGO_SETTINGS_MODULE=myproject.settings.production
WORKDIR /app
# Установка только runtime зависимостей
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
postgresql-client \
curl \
&& rm -rf /var/lib/apt/lists/*
# Копирование виртуального окружения из builder
COPY --from=builder /opt/venv /opt/venv
# Копирование кода приложения
COPY . .
# Сборка статики
RUN python manage.py collectstatic --noinput --clear
# Создание non-root пользователя для безопасности
RUN groupadd -r django && \
useradd -r -g django -u 1000 django && \
chown -R django:django /app && \
mkdir -p /app/media /app/logs && \
chown -R django:django /app/media /app/logs
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
USER django
EXPOSE 8000
# Entrypoint для миграций и запуска
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--worker-class", "sync", \
"--max-requests", "1000", \
"--max-requests-jitter", "50", \
"--access-logfile", "-", \
"--error-logfile", "-"]
Entrypoint скрипт:
#!/bin/bash
# docker-entrypoint.sh
set -e
echo "Waiting for database..."
while ! nc -z db 5432; do
sleep 0.1
done
echo "Database is ready!"
echo "Running migrations..."
python manage.py migrate --noinput
echo "Creating cache tables..."
python manage.py createcachetable || true
echo "Starting application..."
exec "$@"
# docker-compose.yml
version: '3.9'
services:
db:
image: postgres:15-alpine
container_name: django_postgres
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backups:/backups
environment:
POSTGRES_DB: ${DB_NAME:-django_db}
POSTGRES_USER: ${DB_USER:-django_user}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=ru_RU.UTF-8"
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-django_user}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
container_name: django_redis
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- backend
web:
build:
context: .
dockerfile: Dockerfile
args:
DJANGO_ENV: production
container_name: django_web
command: gunicorn myproject.wsgi:application
--bind 0.0.0.0:8000
--workers 4
--threads 2
--worker-class gthread
--worker-tmp-dir /dev/shm
--max-requests 1000
--max-requests-jitter 50
--access-logfile -
--error-logfile -
volumes:
- ./staticfiles:/app/staticfiles:ro
- ./media:/app/media
- ./logs:/app/logs
ports:
- "8000:8000"
env_file:
- .env
environment:
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- backend
- frontend
celery_worker:
build:
context: .
dockerfile: Dockerfile
container_name: django_celery_worker
command: celery -A myproject worker
--loglevel=info
--concurrency=4
--max-tasks-per-child=1000
volumes:
- ./media:/app/media
- ./logs:/app/logs
env_file:
- .env
environment:
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- backend
celery_beat:
build:
context: .
dockerfile: Dockerfile
container_name: django_celery_beat
command: celery -A myproject beat
--loglevel=info
--scheduler django_celery_beat.schedulers:DatabaseScheduler
volumes:
- ./logs:/app/logs
env_file:
- .env
environment:
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- backend
nginx:
image: nginx:alpine
container_name: django_nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./staticfiles:/app/staticfiles:ro
- ./media:/app/media:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
- web
restart: unless-stopped
networks:
- frontend
certbot:
image: certbot/certbot
container_name: django_certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- frontend
networks:
backend:
driver: bridge
frontend:
driver: bridge
volumes:
postgres_data:
redis_data:
# .env
# ВАЖНО: Не коммитить в git! Добавить в .gitignore
# Django
DJANGO_SECRET_KEY=ваш-очень-длинный-случайный-секретный-ключ-минимум-50-символов
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DJANGO_SETTINGS_MODULE=myproject.settings.production
# Database
DB_NAME=django_production
DB_USER=django_user
DB_PASSWORD=сложный-пароль-БД-минимум-20-символов
DB_HOST=db
DB_PORT=5432
# Redis
REDIS_PASSWORD=сложный-пароль-redis-минимум-20-символов
REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
# Celery
CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
CELERY_RESULT_BACKEND=redis://:${REDIS_PASSWORD}@redis:6379/0
# Email (пример для Gmail)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=app-specific-password
# AWS S3 (для медиафайлов)
AWS_ACCESS_KEY_ID=ваш-access-key
AWS_SECRET_ACCESS_KEY=ваш-secret-key
AWS_STORAGE_BUCKET_NAME=your-bucket-name
AWS_S3_REGION_NAME=eu-central-1
# Sentry (мониторинг ошибок)
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
SENTRY_ENVIRONMENT=production
# Security
SECURE_SSL_REDIRECT=True
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
# Первоначальный деплой
docker-compose up -d --build
# Проверка статуса
docker-compose ps
# Просмотр логов
docker-compose logs -f web
docker-compose logs -f celery_worker
# Применение миграций
docker-compose exec web python manage.py migrate
# Создание суперпользователя
docker-compose exec web python manage.py createsuperuser
# Сбор статики
docker-compose exec web python manage.py collectstatic --noinput
# Обновление кода (zero-downtime)
git pull origin main
docker-compose build web
docker-compose up -d --no-deps web
# Применение миграций после обновления
docker-compose exec web python manage.py migrate
# Перезапуск всех сервисов
docker-compose restart
# Очистка неиспользуемых образов
docker system prune -a
# Backup базы данных
docker-compose exec db pg_dump -U django_user django_production > backup_$(date +%Y%m%d_%H%M%S).sql
# Restore базы данных
docker-compose exec -T db psql -U django_user django_production < backup.sql
# Масштабирование Celery воркеров
docker-compose up -d --scale celery_worker=5
# Остановка всех сервисов
docker-compose down
# Полная очистка (включая volumes - ОСТОРОЖНО!)
docker-compose down -v
Результат: Изолированная среда с простым переносом между серверами, легкое масштабирование.
Для проектов с требованиями high availability, автоматическим масштабированием и сложной инфраструктурой.
Internet → LoadBalancer → Ingress (Nginx) → Service → Pods (Django)
↓
StatefulSet (PostgreSQL)
StatefulSet (Redis)
HPA (Auto-scaling)
# k8s/00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
name: production
environment: production
# Создание secrets из файла
kubectl create secret generic django-secrets \
--from-env-file=.env.production \
--namespace=production
# Или из литералов
kubectl create secret generic django-secrets \
--from-literal=secret-key='ваш-django-secret-key' \
--from-literal=db-password='пароль-бд' \
--from-literal=redis-password='пароль-redis' \
--namespace=production
# Просмотр secrets (base64 encoded)
kubectl get secret django-secrets -n production -o yaml
# k8s/01-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: django-config
namespace: production
data:
DJANGO_SETTINGS_MODULE: "myproject.settings.production"
ALLOWED_HOSTS: "yourdomain.com,www.yourdomain.com"
DB_HOST: "postgres-service"
DB_PORT: "5432"
DB_NAME: "django_production"
REDIS_HOST: "redis-service"
REDIS_PORT: "6379"
CELERY_BROKER_URL: "redis://redis-service:6379/0"
GUNICORN_WORKERS: "4"
GUNICORN_THREADS: "2"
# k8s/02-postgres.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres-service
namespace: production
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
clusterIP: None # Headless service для StatefulSet
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
serviceName: postgres-service
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: django-config
key: DB_NAME
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: django-secrets
key: db-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "fast-ssd" # Используйте ваш storage class
resources:
requests:
storage: 20Gi
# k8s/03-django-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: django-app
namespace: production
labels:
app: django
version: v1
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Максимум 1 дополнительный под при обновлении
maxUnavailable: 0 # Всегда минимум 3 пода доступны
selector:
matchLabels:
app: django
template:
metadata:
labels:
app: django
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
# Anti-affinity: разные ноды для каждого пода
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- django
topologyKey: kubernetes.io/hostname
# Init container для миграций
initContainers:
- name: run-migrations
image: registry.yourdomain.com/django-app:latest
command: ['python', 'manage.py', 'migrate', '--noinput']
envFrom:
- configMapRef:
name: django-config
- secretRef:
name: django-secrets
containers:
- name: django
image: registry.yourdomain.com/django-app:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8000
protocol: TCP
# Environment переменные
envFrom:
- configMapRef:
name: django-config
- secretRef:
name: django-secrets
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
# Resource limits
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# Liveness probe - перезапуск если не отвечает
livenessProbe:
httpGet:
path: /health/
port: 8000
httpHeaders:
- name: Host
value: yourdomain.com
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Readiness probe - не отправлять трафик если не готов
readinessProbe:
httpGet:
path: /health/
port: 8000
httpHeaders:
- name: Host
value: yourdomain.com
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# Startup probe - для медленного старта
startupProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
# Volume mounts
volumeMounts:
- name: media
mountPath: /app/media
- name: static
mountPath: /app/staticfiles
readOnly: true
- name: logs
mountPath: /app/logs
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# Volumes
volumes:
- name: media
persistentVolumeClaim:
claimName: django-media-pvc
- name: static
emptyDir: {}
- name: logs
emptyDir: {}
# Graceful shutdown
terminationGracePeriodSeconds: 60
# Image pull secrets
imagePullSecrets:
- name: registry-credentials
# k8s/04-service-ingress.yaml
apiVersion: v1
kind: Service
metadata:
name: django-service
namespace: production
labels:
app: django
spec:
selector:
app: django
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP
sessionAffinity: ClientIP # Sticky sessions
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: django-ingress
namespace: production
annotations:
# Cert-manager для автоматического SSL
cert-manager.io/cluster-issuer: letsencrypt-prod
# Nginx Ingress настройки
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "20m"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "100"
nginx.ingress.kubernetes.io/limit-connections: "50"
# Security headers
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Frame-Options: SAMEORIGIN";
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "X-XSS-Protection: 1; mode=block";
more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
# CORS (если нужно)
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://yourdomain.com"
spec:
ingressClassName: nginx
tls:
- hosts:
- yourdomain.com
- www.yourdomain.com
secretName: django-tls-cert
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: django-service
port:
number: 80
- host: www.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: django-service
port:
number: 80
# k8s/05-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: django-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: django-app
minReplicas: 3
maxReplicas: 20
metrics:
# CPU-based scaling
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Memory-based scaling
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# Custom metrics (требует Prometheus adapter)
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "1000"
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # Ждать 5 минут перед scale down
policies:
- type: Percent
value: 50 # Уменьшать макс на 50% за раз
periodSeconds: 60
- type: Pods
value: 2 # Уменьшать макс на 2 пода за раз
periodSeconds: 60
selectPolicy: Min # Выбрать более консервативную политику
scaleUp:
stabilizationWindowSeconds: 0 # Немедленный scale up
policies:
- type: Percent
value: 100 # Удвоить количество подов
periodSeconds: 15
- type: Pods
value: 4 # Добавить макс 4 пода
periodSeconds: 15
selectPolicy: Max # Выбрать более агрессивную политику
# k8s/06-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: django-media-pvc
namespace: production
spec:
accessModes:
- ReadWriteMany # Множественный доступ для всех подов
storageClassName: "nfs-storage" # Или ваш storage class
resources:
requests:
storage: 50Gi
# k8s/07-celery-worker.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: celery-worker
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: celery-worker
template:
metadata:
labels:
app: celery-worker
spec:
containers:
- name: celery-worker
image: registry.yourdomain.com/django-app:latest
command:
- celery
- -A
- myproject
- worker
- --loglevel=info
- --concurrency=4
- --max-tasks-per-child=1000
envFrom:
- configMapRef:
name: django-config
- secretRef:
name: django-secrets
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
volumeMounts:
- name: media
mountPath: /app/media
volumes:
- name: media
persistentVolumeClaim:
claimName: django-media-pvc
# Создание namespace
kubectl create namespace production
# Применение всех манифестов
kubectl apply -f k8s/ --namespace=production
# Или по порядку
kubectl apply -f k8s/00-namespace.yaml
kubectl apply -f k8s/01-configmap.yaml
kubectl apply -f k8s/02-postgres.yaml
kubectl apply -f k8s/03-django-deployment.yaml
kubectl apply -f k8s/04-service-ingress.yaml
kubectl apply -f k8s/05-hpa.yaml
kubectl apply -f k8s/06-pvc.yaml
kubectl apply -f k8s/07-celery-worker.yaml
# Проверка статуса
kubectl get all -n production
kubectl get pods -n production -o wide
kubectl get hpa -n production
# Просмотр логов
kubectl logs -f deployment/django-app -n production
kubectl logs -f deployment/celery-worker -n production
# Проверка конкретного пода
kubectl describe pod django-app-xxxxxxxxx-xxxxx -n production
# Выполнение команд в поде
kubectl exec -it deployment/django-app -n production -- python manage.py shell
# Создание суперпользователя
kubectl exec -it deployment/django-app -n production -- python manage.py createsuperuser
# Обновление образа (rolling update)
kubectl set image deployment/django-app \
django=registry.yourdomain.com/django-app:v1.2.3 \
--namespace=production
# Проверка статуса обновления
kubectl rollout status deployment/django-app -n production
# История обновлений
kubectl rollout history deployment/django-app -n production
# Откат к предыдущей версии
kubectl rollout undo deployment/django-app -n production
# Откат к конкретной ревизии
kubectl rollout undo deployment/django-app --to-revision=2 -n production
# Масштабирование вручную
kubectl scale deployment/django-app --replicas=5 -n production
# Port forwarding для локального доступа
kubectl port-forward service/django-service 8000:80 -n production
# Получение событий
kubectl get events -n production --sort-by='.lastTimestamp'
# Мониторинг ресурсов
kubectl top pods -n production
kubectl top nodes
Результат: Enterprise-grade инфраструктура с автоматическим масштабированием, self-healing, zero-downtime deployments.
| Критерий | VPS | Docker | Kubernetes |
|---|---|---|---|
| Сложность настройки | Низкая | Средняя | Высокая |
| Сложность поддержки | Низкая | Средняя | Высокая (но автоматизировано) |
| Стоимость (малый проект) | $5-20/мес | $20-50/мес | $100-300/мес |
| Стоимость (средний) | $50-100/мес | $100-300/мес | $300-1000/мес |
| Стоимость (крупный) | Неприменимо | $500-2000/мес | $1000-10000+/мес |
| Масштабируемость | Ручная (вертикальная) | Средняя (горизонтальная с ограничениями) | Полностью автоматическая |
| Время первоначальной настройки | 2-4 часа | 4-8 часов | 2-5 дней |
| High Availability | Нет (single point of failure) | Возможно (с дополнительной настройкой) | Встроено |
| Auto-scaling | Нет | Нет (требует внешних инструментов) | Встроено (HPA, VPA) |
| Zero-downtime deploys | Возможно (требует настройки) | Возможно | Встроено |
| Self-healing | Нет (требует systemd) | Ограничено (Docker restart policies) | Встроено |
| Load Balancing | Ручная настройка (Nginx) | Ручная настройка | Автоматическое |
| Rollback | Ручной (Git + systemd restart) | Ручной (docker-compose) | Автоматический (kubectl rollout) |
| Monitoring готовность | Требует настройки | Требует настройки | Встроенные механизмы |
| Secrets Management | Файлы .env | Docker secrets / .env | Kubernetes Secrets / Vault |
| Подходит для команды | 1-3 человека | 2-10 человек | 5-100+ человек |
| Трафик (RPS) | До 50-100 | До 500-1000 | Неограничено |
| Learning Curve | Легко | Средне | Сложно |
VPS (Gunicorn):
# Graceful reload воркеров
kill -HUP $(cat /var/run/gunicorn/gunicorn.pid)
# Или через systemd
sudo systemctl reload django
# Blue-Green deployment на VPS (2 Gunicorn процесса)
# 1. Запустить новую версию на порту 8001
gunicorn myproject.wsgi:application --bind 127.0.0.1:8001 -c gunicorn_config.py --daemon
# 2. Переключить Nginx upstream
# 3. Остановить старый процесс на 8000
Docker:
# Rolling update
docker-compose up -d --no-deps --scale web=2 web
docker-compose up -d --no-deps --scale web=3 web
docker-compose up -d --no-deps --remove-orphans web
# Blue-Green с Docker
docker-compose -f docker-compose.blue.yml up -d
# Переключить nginx на blue
docker-compose -f docker-compose.green.yml down
Kubernetes:
# Автоматический Rolling Update через strategy в Deployment
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
# health/views.py
from django.http import JsonResponse
from django.db import connection
from django.core.cache import cache
from django.conf import settings
import redis
import time
def health_check(request):
"""
Комплексная проверка здоровья приложения
GET /health/ - быстрая проверка (для load balancer)
GET /health/detailed/ - детальная проверка (для мониторинга)
"""
start_time = time.time()
checks = {}
overall_status = "healthy"
# 1. Проверка базы данных
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
checks['database'] = {
'status': 'ok',
'latency_ms': round((time.time() - start_time) * 1000, 2)
}
except Exception as e:
checks['database'] = {
'status': 'error',
'error': str(e)
}
overall_status = "unhealthy"
# 2. Проверка Redis/Cache
cache_start = time.time()
try:
test_key = '__health_check__'
cache.set(test_key, 'ok', timeout=10)
result = cache.get(test_key)
if result == 'ok':
checks['cache'] = {
'status': 'ok',
'latency_ms': round((time.time() - cache_start) * 1000, 2)
}
else:
raise Exception("Cache read/write failed")
except Exception as e:
checks['cache'] = {
'status': 'error',
'error': str(e)
}
overall_status = "degraded" # Cache не критичен
# 3. Проверка Celery (опционально)
if hasattr(settings, 'CELERY_BROKER_URL'):
try:
from myproject.celery import app as celery_app
inspect = celery_app.control.inspect()
stats = inspect.stats()
if stats:
checks['celery'] = {
'status': 'ok',
'workers': len(stats)
}
else:
raise Exception("No workers available")
except Exception as e:
checks['celery'] = {
'status': 'error',
'error': str(e)
}
overall_status = "degraded"
# 4. Проверка дискового пространства
import shutil
try:
usage = shutil.disk_usage("/")
percent_used = (usage.used / usage.total) * 100
checks['disk'] = {
'status': 'warning' if percent_used > 80 else 'ok',
'percent_used': round(percent_used, 2),
'free_gb': round(usage.free / (1024**3), 2)
}
if percent_used > 90:
overall_status = "degraded"
except Exception as e:
checks['disk'] = {'status': 'unknown', 'error': str(e)}
# Response
response_data = {
'status': overall_status,
'timestamp': time.time(),
'checks': checks,
'response_time_ms': round((time.time() - start_time) * 1000, 2)
}
status_code = 200 if overall_status == "healthy" else 503
return JsonResponse(response_data, status=status_code)
def liveness_check(request):
"""Простая проверка что приложение живо (для K8s liveness probe)"""
return JsonResponse({'status': 'alive'})
def readiness_check(request):
"""Проверка готовности принимать трафик (для K8s readiness probe)"""
try:
# Минимальная проверка БД
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
return JsonResponse({'status': 'ready'})
except:
return JsonResponse({'status': 'not_ready'}, status=503)
# urls.py
from health import views as health_views
urlpatterns = [
path('health/', health_views.health_check, name='health'),
path('health/detailed/', health_views.health_check, name='health_detailed'),
path('liveness/', health_views.liveness_check, name='liveness'),
path('readiness/', health_views.readiness_check, name='readiness'),
]
#!/bin/bash
# scripts/deploy.sh - Безопасный деплой с миграциями
set -e # Остановка при ошибке
echo "=== Starting deployment ==="
# 1. Проверка миграций БЕЗ применения
echo "Checking for pending migrations..."
python manage.py makemigrations --check --dry-run
# 2. Показать план миграций
echo "Migration plan:"
python manage.py showmigrations --plan
# 3. Backup базы данных ПЕРЕД миграциями
echo "Creating database backup..."
timestamp=$(date +%Y%m%d_%H%M%S)
pg_dump -h localhost -U django_user django_db > backups/db_backup_${timestamp}.sql
echo "Backup created: db_backup_${timestamp}.sql"
# 4. Применение миграций
echo "Applying migrations..."
python manage.py migrate --noinput
# 5. Проверка что всё OK
if [ $? -eq 0 ]; then
echo "Migrations applied successfully"
else
echo "Migration failed! Rolling back..."
psql -h localhost -U django_user django_db < backups/db_backup_${timestamp}.sql
exit 1
fi
# 6. Сбор статики
echo "Collecting static files..."
python manage.py collectstatic --noinput --clear
# 7. Проверка deployment checklist
echo "Running Django deployment checks..."
python manage.py check --deploy --fail-level WARNING
# 8. Restart приложения
echo "Restarting application..."
sudo systemctl reload django
# 9. Health check
echo "Waiting for application to start..."
sleep 5
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health/)
if [ "$response" = "200" ]; then
echo "=== Deployment successful! ==="
else
echo "=== Health check failed (HTTP $response). Please investigate. ==="
exit 1
fi
Откат миграций при проблемах:
# Просмотр истории миграций
python manage.py showmigrations app_name
# Откат к конкретной миграции
python manage.py migrate app_name 0042_previous_migration
# Откат всех миграций приложения
python manage.py migrate app_name zero
# Fake миграция (если схема уже применена вручную)
python manage.py migrate --fake app_name 0043_new_migration
# settings/production.py
import os
from pathlib import Path
# Structured logging с JSON
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'json': {
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
'format': '%(asctime)s %(name)s %(levelname)s %(message)s',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
# Файловый handler с ротацией
'file': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/app.log',
'maxBytes': 10485760, # 10MB
'backupCount': 10,
'formatter': 'verbose',
},
# JSON логи для парсинга (ELK, Loki)
'file_json': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/app.json',
'maxBytes': 10485760,
'backupCount': 10,
'formatter': 'json',
},
# Ошибки отдельно
'file_error': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/var/log/django/error.log',
'maxBytes': 10485760,
'backupCount': 20,
'formatter': 'verbose',
},
# Консоль для Docker/K8s
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
# Sentry для критичных ошибок
'sentry': {
'level': 'ERROR',
'class': 'sentry_sdk.integrations.logging.EventHandler',
},
# Email админам при критичных ошибках
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
'filters': ['require_debug_false'],
},
},
'loggers': {
# Django core
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
# Django request/response
'django.request': {
'handlers': ['file_error', 'mail_admins', 'sentry'],
'level': 'ERROR',
'propagate': False,
},
# Database queries (только для отладки!)
'django.db.backends': {
'handlers': ['file'],
'level': 'WARNING', # DEBUG чтобы видеть SQL
'propagate': False,
},
# Security events
'django.security': {
'handlers': ['file_error', 'mail_admins'],
'level': 'WARNING',
'propagate': False,
},
# Ваше приложение
'myproject': {
'handlers': ['console', 'file', 'file_json', 'sentry'],
'level': 'INFO',
'propagate': False,
},
# Celery tasks
'celery': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
},
'root': {
'handlers': ['console', 'file', 'file_error'],
'level': 'WARNING',
},
}
# Создание директорий для логов
log_dir = Path('/var/log/django')
log_dir.mkdir(parents=True, exist_ok=True)
Интеграция с Sentry:
# settings/production.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.redis import RedisIntegration
sentry_sdk.init(
dsn=os.getenv('SENTRY_DSN'),
integrations=[
DjangoIntegration(),
CeleryIntegration(),
RedisIntegration(),
],
environment=os.getenv('SENTRY_ENVIRONMENT', 'production'),
traces_sample_rate=0.1, # 10% транзакций для performance monitoring
profiles_sample_rate=0.1, # 10% профилирование
send_default_pii=False, # Не отправлять личные данные
before_send=lambda event, hint: event if event.get('level') != 'info' else None,
release=os.getenv('GIT_COMMIT_SHA'), # Версия из CI/CD
)
Для VPS/Docker - использование django-environ:
# settings/production.py
import environ
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, []),
DATABASE_URL=(str, ''),
)
# Чтение .env файла
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
DEBUG = env('DEBUG')
SECRET_KEY = env('SECRET_KEY')
ALLOWED_HOSTS = env('ALLOWED_HOSTS')
DATABASES = {
'default': env.db() # Парсит DATABASE_URL автоматически
}
# Redis
CACHES = {
'default': env.cache('REDIS_URL')
}
Для Kubernetes - использование External Secrets Operator:
# k8s/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: django-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager # или vault, gcp, azure
kind: SecretStore
target:
name: django-secrets
creationPolicy: Owner
data:
- secretKey: secret-key
remoteRef:
key: django/production/secret-key
- secretKey: db-password
remoteRef:
key: django/production/db-password
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-django pytest-cov
- name: Run tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: |
pytest --cov=. --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
build-and-push:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
deploy-kubernetes:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Configure kubectl
run: |
echo "${{ secrets.KUBECONFIG }}" > kubeconfig.yaml
export KUBECONFIG=kubeconfig.yaml
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/django-app \
django=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-${{ github.sha }} \
--namespace=production
kubectl rollout status deployment/django-app -n production
- name: Verify deployment
run: |
kubectl get pods -n production
kubectl get deployment django-app -n production
notify:
needs: [deploy-kubernetes]
runs-on: ubuntu-latest
if: always()
steps:
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Deployment to production: ${{ job.status }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
DEBUG = False в productionSECRET_KEY (минимум 50 символов)ALLOWED_HOSTS настроен правильноSECURE_SSL_REDIRECT = TrueSESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = TrueSECURE_HSTS_SECONDS = 31536000 (HSTS)X_FRAME_OPTIONS = 'DENY' или 'SAMEORIGIN'SECURE_CONTENT_TYPE_NOSNIFF = TrueSECURE_BROWSER_XSS_FILTER = Truepython manage.py check --deployCONN_MAX_AGE настроен (persistent connections)python manage.py collectstaticРекомендации по выбору:
Начинающий проект (MVP): VPS с Gunicorn + Nginx + systemd
Растущий стартап (Product-Market Fit): Docker + Docker Compose
Enterprise / High-load: Kubernetes
Путь миграции: VPS → Docker → Kubernetes по мере роста проекта и команды.
Главное — не переусложнять на старте. Надежный VPS лучше, чем плохо настроенный Kubernetes.
Берём проекты на поддержку с чётким SLA. Стабилизируем за 2 недели, даём план развития на 90 дней. 15+ лет опыта с Django.