Прошлые темы разбирали части по отдельности: Dockerfile, compose с PostgreSQL и Redis, gunicorn и статику и Nginx с HTTPS. Эта тема собирает их в один повторяемый процесс релиза: конфигурация из окружения, настройки безопасности за прокси, WhiteNoise, многоэтапный образ, compose-файл, где миграции выполняются один раз за релиз, health-пробы, безопасные изменения схемы и конвейер CI/CD с откатом.
От dev к prod
| Вопрос | Разработка | Production |
|---|---|---|
DEBUG | True | False |
| Секреты | в settings.py | переменные окружения, никогда не в образе |
| Сервер | runserver | gunicorn за Nginx |
| Статика | раздаёт runserver | collectstatic + WhiteNoise |
| База | SQLite или локальный контейнер | PostgreSQL с именованным volume |
| HTTPS | нет | TLS завершает Nginx (или другой прокси) |
| Миграции | вручную | одна задача за релиз |
Правило за большинством решений: образ хранит код, окружение — конфигурацию, volume — данные. Один и тот же образ должен работать в staging и production, меняется только окружение.
Конфигурация из окружения
Все значения, зависящие от среды, читайте из переменных окружения. Отсутствующий секрет должен сразу останавливать процесс: os.environ["NAME"] бросает KeyError, и в production это именно то, что нужно. Для необязательных значений напишите небольшие помощники, потому что bool("False") равно True, а строка "0" тоже истинна:
import os
def env_bool(name: str, default: bool = False) -> bool:
raw = os.environ.get(name, str(default))
return raw.strip().lower() in {"1", "true", "yes", "on"}
def env_list(name: str, default: str = "") -> list[str]:
raw = os.environ.get(name, default)
return [x.strip() for x in raw.split(",") if x.strip()]
os.environ["DJANGO_DEBUG"] = "0"
os.environ["DJANGO_ALLOWED_HOSTS"] = "example.com, www.example.com"
print("DEBUG =", env_bool("DJANGO_DEBUG"))
print("ALLOWED_HOSTS =", env_list("DJANGO_ALLOWED_HOSTS"))
print("USE_TLS =", env_bool("USE_TLS", True))DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
USE_TLS = TrueРазделите настройки на base.py, dev.py и prod.py (настройки и окружения); production импортирует базу и переопределяет:
# config/settings/prod.py
import os
import dj_database_url
from .base import * # noqa: F401,F403
from .env import env_bool, env_list
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # KeyError = fail fast
DEBUG = env_bool("DJANGO_DEBUG", False)
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS")
CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS")
DATABASES = {
"default": dj_database_url.config(
conn_max_age=600, # reuse connections
conn_health_checks=True, # drop dead ones
)
}Файл .env лежит только на сервере (права 600, не в git и не в образе) и передаётся контейнерам через env_file:
# .env (on the server, chmod 600, never in git or in the image)
DJANGO_SECRET_KEY=replace-with-a-long-random-value
DJANGO_DEBUG=0
DJANGO_ALLOWED_HOSTS=shop.example.com,localhost
DJANGO_CSRF_TRUSTED_ORIGINS=https://shop.example.com
DATABASE_URL=postgres://shop:change-me@db:5432/shop
POSTGRES_USER=shop
POSTGRES_PASSWORD=change-me
POSTGRES_DB=shop⚠️ Добавьте
localhostвDJANGO_ALLOWED_HOSTS, если health check контейнера обращается кhttp://localhost:8000/...; иначе Django ответит вашей же пробе400 DisallowedHost.