Сегодня мы создадим новый проект 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
Проверьте все снова:
- Пересоберем заново образы
- Запустим контейнеры
- Перейдем на страницу 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, если каталог еще не существует.
Чтобы обойти это, вы можете:
- Создайте папку в Dockerfile
- Изменить права доступа к каталогу после его монтирования
Мы использовали первое.
Затем обновите конфигурацию 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
Проверим, как все работает:
- Загрузим файл http://localhost:1337/.
- Затем убедимся, что файл доступен на http://localhost:1337/mediafiles/IMAGE_FILE_NAME.
Заключение
В этой статье мы рассмотрели, как создать контейнер для веб-приложения Django с Postgres. Мы также создали готовый к работе файл Docker Compose, который добавляет Gunicorn и Nginx в нашу конфигурацию для обработки статических и мультимедийных файлов. Теперь вы можете проверить производственную настройку локально.
С точки зрения фактического развертывания в производственной среде, вы, вероятно, захотите использовать:
- Полностью управляемый сервис базы данных, такой как RDS или Cloud SQL, вместо того, чтобы управлять своим собственным экземпляром Postgres в контейнере.
- Пользователь без полномочий root для db и nginx сервисов.
Работать с Back-end учат на курсах Пайтон 👇