Докеризация Django c PostrgeSQL, Gunicorn и Nginx.

Докеризация Django c PostrgeSQL, Gunicorn и Nginx.

Перевод статьи Michael Herman: Dockerizing Django with Postgres, Gunicorn, and Nginx (opens new window).

Это пошаговое руководство, в котором подробно описано, как настроить Django в связке с PostgreSQL для работы в контейнере Docker. Для производственных сред мы добавим Nginx и Gunicorn. Мы также рассмотрим, как обслуживать статические и мультимедийные файлы Django через Nginx.

Серия статей о Django на Docker:

  1. Dockerizing Django with Postgres, Gunicorn, and Nginx (opens new window)
  2. Securing a Containerized Django Application with Let's Encrypt (opens new window)
  3. Deploying Django to AWS with Docker and Let's encrypt (opens new window)

# Настройка проекта

Создадим новый каталог с проектом Django:

$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.8 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.0.7
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver

Перейдите в браузере по адресу http://localhost:8000/, чтобы просмотреть проверить работоспособность Django. Завершите работу сервера и выйдите из виртуальной среды после завершения. Теперь у нас есть простой проект Django, с которым можно работать.

Создайте файл requirements.txt в каталоге app и добавьте Django в зависимости:

Django==3.0.7

Поскольку мы перейдем к Postgres, удалите файл db.sqlite3 из каталога "app".

Каталог вашего проекта должен выглядеть так:

└── app
    ├── hello_django
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── manage.py
    └── requirements.txt

# Docker

Установите Docker (opens new window), если он еще не установлен у вас, затем добавьте файл Dockerfile в директорию "app" со следующим содержимым:

# app/Dockerfile

# pull official base image
FROM python:3.8.3-alpine

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# copy project
COPY . .

Итак, мы начали с образа Docker (opens new window) на базе Alpine (opens new window) для Python 3.8.3. Затем мы устанавливаем рабочий каталог (opens new window) вместе с двумя переменными среды:

  1. PYTHONDONTWRITEBYTECODE: запрещает Python запись pyc файлов на диск (эквивалент опции (opens new window) `python -B')
  2. PYTHONUNBUFFERED: запрещает Python выполнять вывод stdout и stderr (эквивалент опции (opens new window) python -u)

Наконец, мы обновили Pip, скопировали файл requirements.txt, установили зависимости и скопировали сам проект Django.

Просмотрите Docker для разработчиков Python (opens new window), чтобы узнать больше о структурировании файлов Docker, а также о некоторых передовых методах настройки Docker для разработки на основе Python.

Затем добавьте файл docker-compose.yml в корень проекта:

# docker-compose.yml
version: '3.7'

services:
  web:
    build: ./app
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./app/:/usr/src/app/
    ports:
      - 8000:8000
    env_file:
      - ./.env.dev

Информация о Compose файле и том как это работает можно почитать здесь (opens new window).

Обновите переменные SECRET_KEY, DEBUG и ALLOWED_HOSTS в settings.py:

SECRET_KEY = os.environ.get("SECRET_KEY")

DEBUG = int(os.environ.get("DEBUG", default=0))

# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")

Затем создайте файл .env.dev в корне проекта для хранения переменных среды для разработки:

DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]

Создайте образ:

$ docker-compose build

Как только образ будет создан запустите контейнер:

$ docker-compose up -d

Снова перейдите в браузере по адресу http://localhost:8000/, чтобы проверить работоспособность созданного образа (должен быть отображен экран приветствия).

Если что-то пошло не так и экран приветствия не отображен, проверьте лог-файлы на наличие ошибок: docker-compose logs -f

# PostgreSQL

Чтобы настроить PostgreSQL, нам нужно добавить новый сервис в файл docker-compose.yml, обновить настройки Django и установить Psycopg2 (opens new window).

Для начала добавим новый сервис с именем db в файл docker-compose.yml:

# docker-compose.yml
version: '3.7'

services:
  web:
    build: ./app
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./app/:/usr/src/app/
    ports:
      - 8000:8000
    env_file:
      - ./.env.dev
    depends_on:
      - db
  db:
    image: postgres:12.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=hello_django
      - POSTGRES_PASSWORD=hello_django
      - POSTGRES_DB=hello_django_dev

volumes:
  postgres_data:

Чтобы сохранить данные по завершению работы контейнера, мы настроили volume. Эта конфигурация привяжет postgres_data к каталогу "/var/lib/postgresql/data/" в контейнере.

Мы также добавили переменную окружения, чтобы задать имя для базы данных по умолчанию и установить имя пользователя и пароль.

Для получения дополнительной информации просмотрите раздел «Переменные среды» на странице Postgres Docker Hub (opens new window).

Нам также понадобятся некоторые новые переменные окружения для веб-службы, поэтому отредактируйте .env.dev следующим образом:

DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432

Update the DATABASES dict in settings.py:

# settings.py

DATABASES = {
    "default": {
        "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
        "NAME": os.environ.get("SQL_DATABASE", os.path.join(BASE_DIR, "db.sqlite3")),
        "USER": os.environ.get("SQL_USER", "user"),
        "PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
        "HOST": os.environ.get("SQL_HOST", "localhost"),
        "PORT": os.environ.get("SQL_PORT", "5432"),
    }
}

Теперь база данных настроена с помощью переменных окружения которые мы определили в файле .env.dev, обратите внимание на значения по умолчанию.

Обновите Dockerfile, чтобы установить соответствующие пакеты, необходимые для Psycopg2:

# Dockerfile

# pull official base image
FROM python:3.8.3-alpine

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# copy project
COPY . .

Добавьте Psycopg2 в requirements.txt:

Django==3.0.7
psycopg2-binary==2.8.5

Ознакомьтесь с GitHub Issue (opens new window) для получения дополнительной информации об установке Psycopg2 в образ Docker на базе Alpine.

Создайте новый образ и запустите два контейнера:

$ docker-compose up -d --build

Запустите миграцию:

$ docker-compose exec web python manage.py migrate --noinput

Получили следующую ошибку?

django.db.utils.OperationalError: FATAL: database "hello_django_dev" does not exist

Запустите команду docker-compose down -v, чтобы удалить тома вместе с контейнерами. Затем заново соберите образы, запустите контейнеры и примените миграции.

Убедитесь, что таблицы Django по умолчанию созданы:

$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev

psql (12.0)
Type "help" for help.

hello_django_dev=# \l
                                          List of databases
       Name       |    Owner     | Encoding |  Collate   |   Ctype    |       Access privileges
------------------+--------------+----------+------------+------------+-------------------------------
 hello_django_dev | hello_django | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres         | hello_django | UTF8     | en_US.utf8 | en_US.utf8 |
 template0        | hello_django | UTF8     | en_US.utf8 | en_US.utf8 | =c/hello_django              +
                  |              |          |            |            | hello_django=CTc/hello_django
 template1        | hello_django | UTF8     | en_US.utf8 | en_US.utf8 | =c/hello_django              +
                  |              |          |            |            | hello_django=CTc/hello_django
(4 rows)

hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".

hello_django_dev=# \dt
                     List of relations
 Schema |            Name            | Type  |    Owner
--------+----------------------------+-------+--------------
 public | auth_group                 | table | hello_django
 public | auth_group_permissions     | table | hello_django
 public | auth_permission            | table | hello_django
 public | auth_user                  | table | hello_django
 public | auth_user_groups           | table | hello_django
 public | auth_user_user_permissions | table | hello_django
 public | django_admin_log           | table | hello_django
 public | django_content_type        | table | hello_django
 public | django_migrations          | table | hello_django
 public | django_session             | table | hello_django
(10 rows)

hello_django_dev=# \q

Вы можете проверить, что том был создан, запустив:

$ docker volume inspect django-on-docker_postgres_data

Вы должны увидеть что-то похожее на:

[
    {
        "CreatedAt": "2020-06-13T18:43:56Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "django-on-docker",
            "com.docker.compose.version": "1.25.4",
            "com.docker.compose.volume": "postgres_data"
        },
        "Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
        "Name": "django-on-docker_postgres_data",
        "Options": null,
        "Scope": "local"
    }
]

Затем добавьте файл entrypoint.sh в каталог "app", чтобы проверить работоспособность Postgres перед применением миграции и запуском сервера разработки Django:

# app/entrypoint.sh

#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

python manage.py flush --no-input
python manage.py migrate

exec "$@"

Обновите права доступа к файлу локально:

$ chmod +x app/entrypoint.sh

Затем обновите файл Dockerfile, чтобы скопировать файл entrypoint.sh и запустить его как команду entrypoint (opens new window) Docker:

# Dockerfile

# pull official base image
FROM python:3.8.3-alpine

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# copy entrypoint.sh
COPY ./entrypoint.sh .

# copy project
COPY . .

# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]

Добавьте переменную окружения DATABASE в .env.dev:

DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres

Проверьте сборку снова:

  1. Пересобирите образы.
  2. Запустите контейнеры
  3. Проверьте вывод в браузере http://localhost:8000/

Примечание

Во-первых, несмотря на добавление Postgres, мы все еще можем создать независимый образ Docker для Django, если для переменной среды DATABASE не задано значение postgres. Для тестирования создайте новый образ, а затем запустите новый контейнер:

$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
    -p 8006:8000 \
    -e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
    hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000

Вы должны увидеть страницу приветствия по адресу http://localhost:8006.

Во-вторых, вы можете закомментировать команды очистки базы данных и миграции в сценарии entrypoint.sh, чтобы они не запускались при каждом запуске или перезапуске контейнера:

# entrypoint.sh

#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

# python manage.py flush --no-input
# python manage.py migrate

exec "$@"

Вместо этого вы можете запускать их вручную после того, как контейнеры запустятся, например:

$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate

# Gunicorn

Двигаясь дальше, для создания production окружения давайте добавим Gunicorn, сервер WSGI production уровня, в файл requirements.txt:

Django==3.0.7
gunicorn==20.0.4
psycopg2-binary==2.8.5

Хотите узнать о WSGI и Gunicorn? Прочтите главу WSGI (opens new window) из курса Создание собственной веб-инфраструктуры Python (opens new window).

Поскольку мы по-прежнему хотим использовать встроенный сервер Django в разработке, создайте новый файл набора с именем docker-compose.prod.yml для продакшн:

#docker-compose.prod.yml

version: '3.7'

services:
  web:
    build: ./app
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    ports:
      - 8000:8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:12.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db

volumes:
  postgres_data:

Если у вас несколько сред, вы можете использовать файл конфигурации docker-compose.override.yml (opens new window). При таком подходе вы должны добавить свою базовую конфигурацию в файл docker-compose.yml, а затем использовать файл docker-compose.override.yml для переопределения этих параметров конфигурации в зависимости от среды.

Обратите внимание на значение по умолчанию для сommand. Мы используем Gunicorn, а не сервер разработки Django. Мы также удалили том из сервиса web, поскольку он нам не нужен в рабочей среде. Наконец, мы используем отдельные файлы переменных окружения для обеих служб, которые будут передаваться в контейнер во время выполнения.

.env.prod

DEBUG=0
SECRET_KEY=change_me
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres

.env.prod.db

POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod

Добавьте два файла в корень проекта. Вероятно, вы захотите сохранить их вне системы контроля версий, поэтому добавьте их в файл .gitignore.

Удалите контейнеры разработки и связанные тома (флаг -v):

$ docker-compose down -v

Затем создайте рабочие образы и запустите контейнеры:

$ docker-compose -f docker-compose.prod.yml up -d --build

Убедитесь, что база данных hello_django_prod была создана вместе с таблицами Django по умолчанию. Протестируйте страницу администратора по адресу http://localhost:8000/admin. Статические файлы больше не загружаются. Это ожидаемо, поскольку режим отладки (Debug) отключен. Скоро мы это исправим.

И снова, если что-то пошло не так и экран приветствия не отображен, проверьте лог-файлы на наличие ошибок: docker-compose -f docker-compose.prod.yml logs -f

# Production Dockerfile

Вы заметили, что мы все еще запускаем команду flush (opens new window) (которая очищает базу данных) и переносим команды при каждом запуске контейнера? Это нормально при разработке, но давайте создадим новый файл точки входа для продакшн.

entrypoint.prod.sh


if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

exec "$@"

Обновите права доступа к файлу локально:

$ chmod +x app/entrypoint.prod.sh

Чтобы использовать этот файл, создайте новый Dockerfile с именем Dockerfile.prod для использования с продакшн сборками:

# Dockerfile.prod

###########
# BUILDER #
###########

# pull official base image
FROM python:3.8.3-alpine as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

# lint
RUN pip install --upgrade pip
RUN pip install flake8
COPY . .
RUN flake8 --ignore=E501,F401 .

# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt


#########
# FINAL #
#########

# pull official base image
FROM python:3.8.3-alpine

# create directory for the app user
RUN mkdir -p /home/app

# create the app user
RUN addgroup -S app && adduser -S app -G app

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*

# copy entrypoint-prod.sh
COPY ./entrypoint.prod.sh $APP_HOME

# copy project
COPY . $APP_HOME

# chown all the files to the app user
RUN chown -R app:app $APP_HOME

# change to the app user
USER app

# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]

Здесь мы использовали многоступенчатую сборку Docker, чтобы уменьшить размер окончательного образа. По сути, builder - это временный образ, который используется для создания wheel пакетов Python. Затем wheel пакеты копируются в продакшн образ, а образ builder удаляется.

Вы можете пойти дальше в многоэтапном подходе к сборке (opens new window) и использовать один Dockerfile вместо создания двух Dockerfile. Подумайте о плюсах и минусах использования этого подхода для двух разных файлов.

Вы заметили, что мы создали пользователя без полномочий root? По умолчанию Docker запускает контейнерные процессы как root внутри контейнера. Это плохая практика, поскольку злоумышленники могут получить root-доступ к хосту Docker, если им удастся выйти за пределы контейнера. Если вы являетесь пользователем root в контейнере, вы будете меть права root и на хосте.

Обновите сервис web в файле docker-compose.prod.yml для сборки с помощью Dockerfile.prod:

web:
  build:
    context: ./app
    dockerfile: Dockerfile.prod
  command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
  ports:
    - 8000:8000
  env_file:
    - ./.env.prod
  depends_on:
    - db

Опробуте изменения:

$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput

# Nginx

Следующим этапом, давайте добавим в этот микс Nginx, который будет действовать как реверс прокси-сервер (opens new window) для Gunicorn с целью обработки клиентских запросов, а также обслуживания статических файлов.

Добавьте сервис nginx в файл docker-compose.prod.yml

  build: ./nginx
  ports:
    - 1337:80
  depends_on:
    - web

Затем в корне локального проекта создайте следующие файлы и папки:

└── nginx
    ├── Dockerfile
    └── nginx.conf

Dockerfile

FROM nginx:1.19.0-alpine

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

nginx.conf

upstream hello_django {
    server web:8000;
}

server {

    listen 80;

    location / {
        proxy_pass http://hello_django;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

}

Ознакомьтесь с разделом Использование NGINX и NGINX Plus в качестве шлюза приложений с uWSGI и Django (opens new window) для получения дополнительной информации о настройке Nginx для работы с Django.

Затем обновите сервис web в docker-compose.prod.yml, заменив ports на expose:

web:
  build:
    context: ./app
    dockerfile: Dockerfile.prod
  command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
  expose:
    - 8000
  env_file:
    - ./.env.prod
  depends_on:
    - db

Теперь порт 8000 доступен для других служб Docker только изнутри. Порт больше не будет публиковаться на хост-машине.

Чтобы узнать больше о ports и expose, просмотрите этот вопрос на Stack Overflow (opens new window).

Протестируйте обновленную сборку:

$ docker-compose -f docker-compose.prod.yml down -v
$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput

Убедитесь, что приложение запущено и работает по адресу http://localhost:1337.

Структура вашего проекта должна теперь выглядеть следующим образом:

├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│   ├── Dockerfile
│   ├── Dockerfile.prod
│   ├── entrypoint.prod.sh
│   ├── entrypoint.sh
│   ├── hello_django
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── manage.py
│   └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
    ├── Dockerfile
    └── nginx.conf

Когда закончите остановите контенеры:

$ docker-compose -f docker-compose.prod.yml down -v

Поскольку Gunicorn является сервером приложений, он не отвечает за публикацию статических файлов. Итак, каким образом опубликовать статические и мультимедийные файлы в текущей конфигурации?

# Статические файлы

Обновите settings.py

STATIC_URL = "/staticfiles/"
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")

# Режим разработки (development)

Теперь любой запрос к http://localhost:8000/staticfiles/* загружать статику из каталога "staticfiles".

Чтобы проверить, сначала заново соберите образы и запустите новые контейнеры в обычном порядке. Убедитесь, что статические файлы загружаются при обращении по адресу http://localhost:8000/admin.

# Продакшн (production)

Для продакшн сборки добавьте том в сервисы web и nginx в docker-compose.prod.yml, чтобы каждый контейнер имел общий каталог с именем "staticfiles":

version: '3.7'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
    expose:
      - 8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:12.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db
  nginx:
    build: ./nginx
    volumes:
      - static_volume:/home/app/web/staticfiles
    ports:
      - 1337:80
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:

Нам также потребуется создать директорию "/home/app/web/staticfiles" в Dockerfile.prod

...

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME

...

Зачем это нужно?

Docker Compose обычно монтирует именованные тома как root. И поскольку мы используем пользователя без полномочий root, мы получим ошибку отказа доступа при запуске команды collectstatic, если каталог еще не существует.

Чтобы обойти это, вы можете:

  1. Создать папку в Dockerfile (источник (opens new window))
  2. Изменить права доступа к каталогу после его монтирования (источник (opens new window))

Мы использовали первое.

Затем обновите конфигурацию Nginx для маршрутизации и обработки запросов статических файловов по пути "staticfiles":

upstream hello_django {
    server web:8000;
}

server {

    listen 80;

    location / {
        proxy_pass http://hello_django;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /staticfiles/ {
        alias /home/app/web/staticfiles/;
    }

}

Сверните контейнеры:

$ docker-compose down -v

Протестируйте:

$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear

Опять же, запросы к http://localhost:1337/staticfiles/* будут возвращать файлы из каталога "staticfiles".

Перейдите по адресу http://localhost:1337/admin и убедитесь, что статические ресурсы загружаются правильно.

Вы также можете проверить в журналах - через docker-compose -f docker-compose.prod.yml logs -f - что запросы к статическим файлам успешно обслуживаются через Nginx:

nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 1928 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /staticfiles/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /staticfiles/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /staticfiles/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /staticfiles/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /staticfiles/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/staticfiles/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"
nginx_1  | 172.31.0.1 - - [13/Jun/2020:20:35:47 +0000] "GET /staticfiles/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/staticfiles/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" "-"

Когда закончите остановите контейнеры:

$ docker-compose -f docker-compose.prod.yml down -v

# Медиа файлы

Чтобы проверить работу с медиафайлами, начните с создания нового приложения Django:

$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload

Добавьте новое приложение в список INSTALLED_APPS в settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "upload",
]

app/upload/views.py:

from django.shortcuts import render
from django.core.files.storage import FileSystemStorage


def image_upload(request):
    if request.method == "POST" and request.FILES["image_file"]:
        image_file = request.FILES["image_file"]
        fs = FileSystemStorage()
        filename = fs.save(image_file.name, image_file)
        image_url = fs.url(filename)
        print(image_url)
        return render(request, "upload.html", {
            "image_url": image_url
        })
    return render(request, "upload.html")

Добавьте каталог "templates" в каталог "app/upload", а затем добавьте новый шаблон с именем upload.html:

{% block content %}

  <form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <input type="file" name="image_file">
    <input type="submit" value="submit" />
  </form>

  {% if image_url %}
    <p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
  {% endif %}

{% endblock %}

app/hello_django/urls.py:

from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

from upload.views import image_upload

urlpatterns = [
    path("", image_upload, name="upload"),
    path("admin/", admin.site.urls),
]

if bool(settings.DEBUG):
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

app/hello_django/settings.py:

MEDIA_URL = "/mediafiles/"
MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles")

# Режим разработки (development)

Протестируйте:

$ docker-compose up -d --build

Вы должны иметь возможность загрузить изображение по адресу http://localhost:8000/, а затем просмотреть изображение по адресу http://localhost:8000/mediafiles/IMAGE_FILE_NAME.

# Продакшн (production)

Для продакшн добавтье другие тома в службы web и nginx:

version: '3.7'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:12.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db
  nginx:
    build: ./nginx
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    ports:
      - 1337:80
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:
  media_volume:

Создайте папку «/home/app/web/mediafiles» в Dockerfile.prod:

...

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME

...

Снова обновите конфигурацию Nginx:

upstream hello_django {
    server web:8000;
}

server {

    listen 80;

    location / {
        proxy_pass http://hello_django;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /staticfiles/ {
        alias /home/app/web/staticfiles/;
    }

    location /mediafiles/ {
        alias /home/app/web/mediafiles/;
    }

}

Пересобреите контейнеры:

$ docker-compose down -v

$ docker-compose -f docker-compose.prod.yml up -d --build
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear

Проверьте работоспособность еще раз:

  1. Загрузите изображение по адресу http://localhost:1337/.
  2. Затем просмотрите изображение по адресу http://localhost:1337/mediafiles/IMAGE_FILE_NAME.

Если вы видите ошибку 413 Request Entity Too Large, вам необходимо увеличить максимально разрешенный размер тела запроса клиента (opens new window) в секции server или location конфигурации Nginx.

Пример:

location / {
   proxy_pass http://hello_django;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header Host $host;
   proxy_redirect off;
   client_max_body_size 100M;
}

# Заключение

В этом руководстве мы рассмотрели, как поместить веб-приложение Django в контейнер с помощью Postgres для разработки. Мы также создали готовый к работе файл Docker Compose, который добавляет Gunicorn и Nginx в микс для обработки статических и мультимедийных файлов. Теперь вы можете протестировать продакшн сборку локально.

Что касается фактического развертывания в производственной среде, вы, вероятно, захотите использовать:

  1. Полностью управляемую службу базы данных, такую как RDS (opens new window) или Cloud SQL (opens new window), вместо управления собственным экземпляром Postgres внутри контейнера.
  2. Пользователь без полномочий root для служб db и nginx

Вы можете найти код в репозитории django-on-docker (opens new window).

Здесь (opens new window) также доступна более старая версия кода Pipenv.

Спасибо за чтение!