Django-Admin.ru

Django в Docker: Dockerfile и docker compose

Как поднять Docker-контейнер с Django-приложением: Dockerfile, docker compose с PostgreSQL, переменные окружения, миграции при старте, Gunicorn и healthcheck.

Редакция Django-Admin.ru
· обновлено · 10 мин чтения

Содержание · 11 разделов
  1. Структура проекта
  2. Настройки Django из переменных окружения
  3. Dockerfile для Django
  4. Миграции и collectstatic при старте
  5. docker-compose.yml с PostgreSQL
  6. Запуск и управление
  7. Статика: Nginx или WhiteNoise
  8. Режим разработки
  9. Частые ошибки
  10. could not translate host name “db” / failed to resolve host ‘db’
  11. Connection refused при старте web
  12. password authentication failed после смены пароля
  13. Админка без стилей
  14. Изменения в коде не видны
  15. Частые вопросы
  16. Как открыть базу из контейнера в DBeaver или pgAdmin?
  17. Как сделать резервную копию базы из контейнера?
  18. Как добавить Celery и Redis в docker compose?
  19. Как применять миграции, если контейнеров web несколько?
  20. Нужен ли Docker, чтобы разрабатывать на Django?
  21. Итог

Чтобы поднять Docker-контейнер с Django-приложением, нужны три файла: Dockerfile описывает образ с Python, зависимостями и кодом проекта; entrypoint.sh при старте применяет миграции и собирает статику; docker-compose.yml запускает рядом PostgreSQL и связывает сервисы. После этого весь стек поднимается одной командой:

docker compose up -d --build

Ниже — готовые файлы с объяснением каждой строки: настройки Django из переменных окружения, Gunicorn вместо runserver, тома для базы и статики, healthcheck и порядок запуска. Нужен установленный Docker с плагином Compose v2 (команда docker compose, без дефиса). Проект создан командой startproject с пакетом настроек config.

Структура проекта

mysite/
├── config/
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── blog/
├── manage.py
├── requirements.txt
├── Dockerfile
├── entrypoint.sh
├── docker-compose.yml
├── .dockerignore
└── .env              # секреты, не коммитится

В requirements.txt должны быть Django, сервер приложений и драйвер PostgreSQL:

Django>=5.2
gunicorn>=23
psycopg[binary]>=3.2

Настройки Django из переменных окружения

В контейнере нельзя держать секреты и параметры базы прямо в settings.py: один и тот же образ запускается локально, на тестовом и боевом сервере. Читайте их из окружения:

# config/settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["POSTGRES_DB"],
        "USER": os.environ["POSTGRES_USER"],
        "PASSWORD": os.environ["POSTGRES_PASSWORD"],
        "HOST": os.environ.get("POSTGRES_HOST", "db"),
        "PORT": os.environ.get("POSTGRES_PORT", "5432"),
    }
}

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"

HOST по умолчанию — db: так будет называться сервис PostgreSQL в compose, и Docker сам резолвит это имя в адрес контейнера. Подробно о параметрах подключения — в статье «Как подключить PostgreSQL к Django».

Сами значения положите в .env рядом с docker-compose.yml:

# .env — добавьте в .gitignore
DJANGO_SECRET_KEY=замените-на-длинную-случайную-строку
DJANGO_DEBUG=0
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,example.com
POSTGRES_DB=mysite
POSTGRES_USER=mysite
POSTGRES_PASSWORD=надёжный-пароль

Сгенерировать ключ можно так: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())".

Dockerfile для Django

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

RUN chmod +x /app/entrypoint.sh \
    && useradd --create-home app \
    && mkdir -p /app/staticfiles /app/media \
    && chown -R app:app /app/staticfiles /app/media
USER app

EXPOSE 8000

ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-"]

Что здесь важно:

  • python:3.12-slim — официальный образ на Debian без лишних пакетов. Для psycopg[binary] компилятор не нужен.
  • PYTHONUNBUFFERED=1 — логи Python сразу попадают в docker compose logs, без буферизации.
  • Сначала requirements.txt, потом код. Docker кеширует слои: пока зависимости не менялись, pip install при пересборке не выполняется, и правка кода собирается за секунды.
  • Пользователь app. Процесс в контейнере не должен работать от root. Папки для статики и медиа заранее создаются и отдаются ему: туда пишет collectstatic.
  • Gunicorn вместо runserver. Встроенный сервер Django не предназначен для продакшна. Адрес 0.0.0.0 обязателен: на 127.0.0.1 Gunicorn будет недоступен снаружи контейнера. Стартовое число воркеров, которое рекомендует Gunicorn, — 2 × ядра + 1. --access-logfile - выводит журнал запросов в stdout.

Чтобы в образ не попали виртуальное окружение, локальная база и секреты, создайте .dockerignore:

.venv
__pycache__
*.pyc
.git
.env
db.sqlite3
staticfiles
media

Миграции и collectstatic при старте

Применять миграции на этапе сборки образа нельзя: при docker build база недоступна и переменных окружения продакшна нет. Поэтому их выполняет скрипт, который запускается перед основной командой контейнера:

#!/bin/sh
set -e

python manage.py migrate --noinput
python manage.py collectstatic --noinput

exec "$@"

set -e останавливает контейнер, если миграция упала, — сайт не запустится с несовместимой схемой базы. exec "$@" заменяет процесс скрипта командой из CMD, то есть Gunicorn. Так он получает сигналы остановки от Docker напрямую и корректно завершает воркеры. Как устроены миграции и что делать при конфликтах, рассказано в статье «Миграции в Django».

Важно: если вы работаете в Windows, сохраните entrypoint.sh с переводами строк LF, а не CRLF. Иначе контейнер упадёт с exec /app/entrypoint.sh: no such file or directory, хотя файл на месте. В VS Code формат переключается в правом нижнем углу (CRLF → LF), для git задайте *.sh text eol=lf в .gitattributes. Чтобы скрипт оставался исполняемым и при монтировании кода в контейнер, сохраните этот бит в git: git update-index --chmod=+x entrypoint.sh.

docker-compose.yml с PostgreSQL

services:
  db:
    image: postgres:18
    env_file: .env
    volumes:
      - postgres_data:/var/lib/postgresql
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
      interval: 5s
      timeout: 5s
      retries: 10
    restart: unless-stopped

  web:
    build: .
    env_file: .env
    ports:
      - '8000:8000'
    volumes:
      - static_data:/app/staticfiles
      - media_data:/app/media
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test:
        [
          'CMD',
          'python',
          '-c',
          "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')",
        ]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    restart: unless-stopped

volumes:
  postgres_data:
  static_data:
  media_data:

Разберём ключевые места.

Переменные окружения. Оба сервиса читают .env. Официальный образ postgres при первом запуске создаёт базу и пользователя из POSTGRES_DB, POSTGRES_USER и POSTGRES_PASSWORD, а Django берёт те же значения для подключения. Обратите внимание: эти переменные срабатывают, только пока том с данными пуст. Смена пароля в .env после первого запуска уже существующую базу не изменит.

Тома (volumes). Именованный том postgres_data хранит данные базы между перезапусками и пересборками — без него docker compose down уничтожил бы все записи. Точка монтирования зависит от версии образа. С postgres:18 том подключают к /var/lib/postgresql, а данные лежат во вложенной папке с номером версии (/var/lib/postgresql/18/docker). Для postgres:17 и старше том монтируют в /var/lib/postgresql/data: если указать /var/lib/postgresql, данные окажутся в анонимном томе и пропадут при пересоздании контейнера. Тома static_data и media_data нужны, чтобы статику и загруженные файлы мог раздавать отдельный веб-сервер (см. ниже), а медиафайлы не терялись при пересоздании контейнера.

Healthcheck и порядок запуска. depends_on с condition: service_healthy запускает Django только после того, как pg_isready подтвердит, что PostgreSQL принимает подключения. Простой depends_on: [db] ждёт лишь старта контейнера, и первые migrate падали бы с Connection refused. Двойной $$ экранирует $ от подстановки самим Compose — переменную раскроет оболочка внутри контейнера.

Для healthcheck сервиса web добавьте лёгкий URL, который не трогает базу:

# config/urls.py
from django.http import HttpResponse
from django.urls import path

urlpatterns = [
    # ...
    path("health/", lambda request: HttpResponse("ok")),
]

В slim-образе нет curl, поэтому проверка написана на Python. Запрос идёт на localhost, так что он должен быть в DJANGO_ALLOWED_HOSTS — иначе Django ответит 400, и контейнер получит статус unhealthy.

Запуск и управление

docker compose up -d --build   # собрать образ и запустить в фоне
docker compose ps              # статус сервисов и healthcheck
docker compose logs -f web     # логи Django и Gunicorn

Откройте http://localhost:8000/. Администратора создайте внутри контейнера — подробно в статье про createsuperuser:

docker compose exec web python manage.py createsuperuser

Другие полезные команды:

docker compose exec web python manage.py shell   # консоль Django
docker compose exec db psql -U mysite mysite      # консоль PostgreSQL
docker compose down                               # остановить и удалить контейнеры
docker compose down -v                            # то же + удалить тома (данные БД!)

После изменения кода выполните docker compose up -d --build — Compose пересоберёт образ и пересоздаст только изменившиеся сервисы.

Статика: Nginx или WhiteNoise

Gunicorn не раздаёт статические файлы, поэтому при DEBUG = False админка откроется без стилей. Два решения:

  • WhiteNoise — пакет, который отдаёт статику прямо из Django. Достаточно установить его и добавить middleware; отдельный контейнер не нужен.
  • Nginx в отдельном контейнере с общим томом static_data:
services:
  # db и web — как выше
  nginx:
    image: nginx:stable-alpine
    ports:
      - '80:80'
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - static_data:/app/staticfiles:ro
      - media_data:/app/media:ro
    depends_on:
      - web
# nginx.conf
server {
    listen 80;

    location /static/ { alias /app/staticfiles/; }
    location /media/  { alias /app/media/; }

    location / {
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

С Nginx уберите ports у сервиса web: снаружи должен быть доступен только порт 80. HTTPS, CSRF_TRUSTED_ORIGINS и чек-лист настроек продакшна разобраны в статье о деплое Django с Gunicorn и Nginx.

Режим разработки

Для локальной работы удобно монтировать код в контейнер и запускать runserver с автоперезагрузкой. Compose автоматически объединяет docker-compose.yml с файлом docker-compose.override.yml, если он лежит рядом:

# docker-compose.override.yml — только для разработки, на сервер не копируйте
services:
  web:
    command: python manage.py runserver 0.0.0.0:8000
    environment:
      DJANGO_DEBUG: '1'
    volumes:
      - .:/app

Частые ошибки

could not translate host name “db” / failed to resolve host ‘db’

Django запущен не в Compose (например, python manage.py runserver на хосте), и имя db ему неизвестно. Вне контейнеров используйте POSTGRES_HOST=localhost и пробросьте порт 5432 у сервиса db.

Connection refused при старте web

PostgreSQL ещё не готов. Проверьте, что у db есть healthcheck, а у web — depends_on с condition: service_healthy.

password authentication failed после смены пароля

Пароль в .env поменяли, а база в томе создана со старым. Смените пароль в самой базе (ALTER USER) или, если данные не нужны, пересоздайте том: docker compose down -v.

Админка без стилей

Не выполнен collectstatic или статику никто не раздаёт: подключите WhiteNoise или Nginx. Заодно проверьте, что STATIC_ROOT совпадает с путём тома.

Изменения в коде не видны

Без bind mount код копируется в образ при сборке. Пересоберите: docker compose up -d --build.

Частые вопросы

Как открыть базу из контейнера в DBeaver или pgAdmin?

Опубликуйте порт PostgreSQL только на локальный интерфейс, чтобы база не стала доступна из интернета:

services:
  db:
    ports:
      - '127.0.0.1:5432:5432'

В клиенте укажите хост localhost, порт 5432 и логин с паролем из .env. На сервере вместо открытия порта удобнее SSH-туннель.

Как сделать резервную копию базы из контейнера?

Запустите pg_dump внутри контейнера и сохраните вывод на хосте; флаг -T отключает псевдотерминал, чтобы дамп не испортили служебные символы:

docker compose exec -T db pg_dump -U mysite mysite > backup.sql
docker compose exec -T db psql -U mysite mysite < backup.sql   # восстановление

В Windows PowerShell 5.1 оператор > сохраняет файл в UTF-16, и psql его не прочитает. Выполните команду в cmd или PowerShell 7 либо запишите дамп внутри контейнера (pg_dump -f /tmp/backup.sql) и скопируйте его командой docker compose cp db:/tmp/backup.sql ..

Как добавить Celery и Redis в docker compose?

Redis — готовый образ, а воркер Celery собирается из того же образа, что и web, только с другой командой:

services:
  redis:
    image: redis:7
    restart: unless-stopped

  celery:
    build: .
    command: celery -A config worker -l INFO
    env_file: .env
    environment:
      CELERY_BROKER_URL: redis://redis:6379/0
    depends_on:
      - redis
      - db

Подключение Celery к проекту описано в статье «Celery в Django». Воркер наследует из образа ENTRYPOINT с миграциями и collectstatic — чтобы не выполнять их при каждом старте, задайте ему entrypoint: [].

Как применять миграции, если контейнеров web несколько?

При масштабировании за Nginx (docker compose up --scale web=3, без ports у web) каждый контейнер запустит migrate из entrypoint.sh одновременно. Надёжнее убрать миграции из скрипта и выполнять их отдельной разовой командой перед обновлением:

docker compose run --rm web python manage.py migrate

Нужен ли Docker, чтобы разрабатывать на Django?

Нет. Для учёбы и небольших проектов хватает виртуального окружения и SQLite. Docker полезен, когда локально нужны те же PostgreSQL, Redis и версии сервисов, что на сервере, или когда над проектом работает команда и окружение должно собираться одинаково у всех.

Итог

Минимальный продакшн-стек Django в Docker — это Dockerfile на python:3.12-slim с Gunicorn и непривилегированным пользователем, entrypoint.sh с migrate и collectstatic, а также docker-compose.yml с PostgreSQL, томом для данных и healthcheck. Секреты живут в .env, а не в образе. Полную схему продакшна с Nginx, HTTPS и systemd смотрите в руководстве по деплою Django, фоновые задачи добавляются отдельным сервисом — см. Celery в Django.

  • #Django
  • #Docker
  • #docker compose
  • #PostgreSQL
  • #Gunicorn
↑ ↓ выборEnter открытьEsc закрыть