Як використовувати Django, PostgreSQL та Docker

  • 4566
  • 4
  • 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 вчать на курсах Пайтон 👇

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