Как использовать Django, PostgreSQL и Docker

Как использовать Django, PostgreSQL и Docker

  • 30 мая, 2022
  • читать 20 мин

Сегодня мы создадим новый проект Django, используя Docker и PostgreSQL. Django поставляется со встроенной поддержкой SQLite, но даже для локальной разработки лучше использовать «настоящую» базу данных, такую как PostgreSQL, которая соответствует производственной.

Можно запускать PostgreSQL локально, используя такой инструмент, как Postgres.app, однако сегодня среди многих разработчиков предпочтительным является использование Docker, инструмента для создания изолированных операционных систем. Проще всего представлять это как виртуальную среду, которая содержит все необходимое для нашего проекта Django: зависимости, базы данных, службы кэширования и любые другие необходимые инструменты.

Основная причина использования Docker заключается в том, что он полностью устраняет любые проблемы, связанные с локальной разработкой. Вместо того, чтобы беспокоиться о том, какие программные пакеты установлены или работают с локальной базой данных вместе с проектом, вы просто запускаете образ Docker всего проекта. Лучше всего то, что этим можно поделиться в группах и значительно упростить разработку команды.

Инсталяция Docker

Первым шагом является установка настольного приложения Docker для вашего локального компьютера:

Первоначальная загрузка Docker может занять некоторое время для загрузки.

После завершения установки Docker мы можем подтвердить, что запущена правильная версия. В вашем терминале запустите команду docker —version.

$ docker --version
Docker version 19.03.2, build 6a30dfc

Docker Compose — это дополнительный инструмент, который автоматически включается в загрузку Docker для Mac и Windows. Однако, если вы используете Linux, вам нужно будет добавить его вручную. Вы можете сделать это, выполнив команду sudo pip install docker-compose после завершения установки Docker.

Проект Django

Создайте новый каталог проекта вместе с новым проектом 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 уже должен был завершить установку к этому моменту. Чтобы убедиться, что установка прошла успешно, закройте локальный сервер с помощью Control + c, а затем введите в командной строке docker run hello-world. Вы должны увидеть ответ вроде этого:

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
d1725b59e92d: Pull complete
Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image whi
ch runs the executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client,
which sent it to your terminal.

To try something more ambitious, you can run an Ubuntu container
with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Образы и контейнеры

В Docker есть две важные концепции: образы (images) и контейнеры (containers).

  • Image: список инструкций для всех программных пакетов в ваших проектах
  • Container: экземпляр образа во время выполнения

Другими словами, образ (image) описывает, что произойдет, а контейнер (container) — это то, что фактически выполняется.

Для настройки образов и контейнеров в Docker мы используем два файла: Dockerfile и docker-compose.yml.

Dockerfile содержит список инструкций для образа, иначе говоря, что на самом деле происходит в среде контейнера.

Создадим новый файл Dockerfile.

(env) $ touch 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 для Python 3.8. Далее мы создаем две переменные окружения.

Затем мы устанавливаем рабочий каталог вместе с двумя переменными среды:

PYTHONUNBUFFERED гарантирует, что наш вывод консоли выглядит знакомым и не буферизируется Docker, что нам не нужно. PYTHONDONTWRITEBYTECODE означает, что Python не будет пытаться создавать файлы .pyc, которые мы также не желаем.

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

Мы не можем запустить Docker-контейнер, пока у нас не будет созданного образа, поэтому давайте сделаем это, создав его.

(env) $ docker build .

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

Sending build context to Docker daemon  162.3kB
Step 1/8 : FROM python:3.7
3.7: Pulling from library/python
c7b7d16361e0: Pull complete 
b7a128769df1: Pull complete 
1128949d0793: Pull complete 
667692510b70: Pull complete 
bed4ecf88e6a: Pull complete 
8a8c75f3996a: Pull complete 
10b7379e5573: Pull complete 
ca1b6fe24628: Pull complete 
9a90211ec083: Pull complete 
Digest: sha256:fc0a398e1987fb1e58909053c11630e06adb3df265fe693391779020b9253f5e
Status: Downloaded newer image for python:3.7
 ---> 9fa56d0addae
Step 2/8 : ENV PYTHONDONTWRITEBYTECODE 1
 ---> Running in 5e7a7983814d
Removing intermediate container 5e7a7983814d
 ---> 3aff2533de96
....
Successfully built 98329412f14c

Далее нам нужен новый файл docker-compose.yml. Он говорит Docker, как запустить наши Docker-контейнеры. У нас будут 2 контейнера. Один для базы, другой для приложения.

(app) $ touch 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

Обновите переменные 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' должен быть в виде одной строки с хостами разделенными символом пробела
# Для примера: '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

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

Сначала добавим новый сервис db в 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/» в контейнере.

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

Поэтому внесем соответствующие изменения в файл .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

Затем обновите файл settings.py, чтобы указать, что мы будем использовать PostgreSQL, а не SQLite.

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"),
    }
}

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

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

# 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

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

$ 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

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

$ 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:

#!/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 и запустите его как команду точки входа Docker:

# 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.

Во-вторых, вы можете закомментировать команды очистки (flush) и миграции (migrate) базы данных в сценарии 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

Небольшой список команд Docker

Когда вы закончите, не можете погасить контейнер Docker: docker-compose down

Просто приостановить контейнер: docker stop CONTAINER ID

Запустить ранее остановленный контейнер: docker start CONTAINER ID

Перегрузить контейнер: docker restart CONTAINER ID

Увидеть работающие контейнеры: docker ps

Увидеть вообще все контейнеры: docker ps -a

Увидеть список всех образов: docker images

Удалить образ: docker rmi CONTAINER ID или docker rmi -f CONTAINER ID

Иногда может понадобиться зайти в работающий контейнер. Для этого нужно запустить команду запуска интерактивной оболочкой bash: docker exec -it CONTAINER ID bash

Gunicorn

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

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

Поскольку мы все еще хотим использовать встроенный сервер Django для разработки, создайте новый файл compose под названием 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. При таком подходе вы добавляете базовую конфигурацию в файл docker-compose.yml, а затем используете файл docker-compose.override.yml для переопределения этих параметров конфигурации в зависимости от среды.

Обратите внимание на команду command. Мы используем 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

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

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

$ docker-compose down -v

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

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

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

Производственный Dockerfile

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

Файл entrypoint.prod.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

exec "$@"

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

$ chmod +x app/entrypoint.prod.sh

Чтобы использовать этот файл, создайте новый Dockerfile с именем 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"]

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

Вы заметили, что мы создали пользователя без полномочий 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, чтобы он действовал как обратный прокси-сервер для Gunicorn для обработки клиентских запросов, а также для обслуживания статических файлов.

Добавим сервис nginx в docker-compose.prod.yml:

nginx:
  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;
    }

}

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

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. И это порт больше не будет опубликован на хост-машине.

Проверяем, как это работает:

$ 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

Для производственной среды добавьте volume в 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
  2. Изменить права доступа к каталогу после его монтирования

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

Затем обновите конфигурацию 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:

Далее снова остановим контейнеры:

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

Media файлы

Чтобы проверить обработку мультимедийных файлов, начните с создания нового модуля 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.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

Для производственной среды добавим новый том volume в сервисы 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.

Заключение

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

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

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

Работать с Back-end учат на курсах Пайтон 👇

Рекомендуем курс по теме