Готовим проект: django, nginx, gunicorn, postgresql в docker

Дата выхода: 13 августа 2023 г.

Редактировался: 17 августа 2023 г.

Если вы интересуетесь вопросом как скомпоновать ваш учебный, пет или другой проект в контейнеры то предлагаю вам ознакомиться с данной статьёй. Здесь я покажу вам простой пример веб-приложения состоящего из django, nginx, gunicorn и postgresql, которые будут работать в отдельных docker-контейнерах.

Введение

Перед началом небольшое пояснение, мы создадим django-проект, который будет работать с СУБД postgresql, в качестве WSGI мы будем использовать gunicorn, nginx выступит http сервером для нашего веб-приложения.

WSGI - интерфейс маршрутизирующий запросы http-серверов (nginx, apache) на веб приложение (в данном случае django).

Наш django-проект (вместе с WSGI), СУБД postgresql и nginx будут работать в отдельных docker-контейнерах.

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

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

  1. code – здесь будет храниться django проект.
  2. data – данная папка будет хранить базу данных.
  3. nginx – хранит файл конфигурации nginx.

Теперь в папке code  создадим директории: templates для наших будущих шаблонов, static для статических файлов и media для загружаемых файлов.

Теперь создадим несколько пустых файлов, в директории code создадим Dockerfile и requirements.txt (файл зависимостей), в nginx создадим Dockerfile и nginx.conf (конфигурационный файл), а в общей директории проекта создадим пустой YAML-файл docker-compose.yaml. Под конец сделаем виртуальное окружение для удобства работы:

python -m venv ./venv
source ./venv/bin/activate

Такими образом у нас получится следующая картина:

Django и PostgreSQL в контейнер

Теперь подготовим небольшой django проект, сразу оговорюсь, что он будет исключительно тестовый, ничего сверхъестественного в нём мы делать не будем.

В файл requirements.txt записываем все необходимые зависимости в данном случае это:

  1. django
  2. psycopg2-binary
  3. gunicorn

Устанавливаем всё в виртуальное окружение:

pip install -r ./code/requirements.txt 

Далее, в папке code создаём проект и какое-то приложение (app):

django-admin startproject mysite ./code 
cd code
python manage.py startapp post

Изменение settings.py

Сразу пойдём в settings.py приложения и выставим все необходимые настройки.

Импортируем функцию environ для того чтобы доставать переменные среды из ОС.

from os import environ

Затем изменим ALLOWED_HOSTS и добавим созданный app post в INSTALLED_APPS:

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'post.apps.PostConfig',
]

Указываем путь к шаблонам:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        # Указываем папку с шаблонами
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'django.template.context_processors.media',
            ],
        },
    },
]

 В настройках СУБД укажем, что имя базы данных, логин и пароль мы будем доставить из переменных окружения ОС. В качестве хоста БД укажем db это будет имя контейнера с postgresql, порт оставим стандартный:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': environ.get('POSTGRES_DB'),
        'NAME': environ.get('POSTGRES_NAME'),
        'USER': environ.get('POSTGRES_USER'),
        'PASSWORD': environ.get('POSTGRES_PASSWORD'),
        'HOST': 'db',
        'PORT': 5432
    }
}

Изменим настройки времени и укажем путь к статическим и медиа файлам:

LANGUAGE_CODE = 'ru-ru'

TIME_ZONE = 'Europe/Moscow'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'static/'

MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media/'

Изменяем views.py

Заходим в папку post открываем views.py и создаём класс представления с одним единственным методом который будет генерировать нам немного рандомных данных:

from django.shortcuts import render
from django.views import View
from string import ascii_letters
from random import choice

class AnyDataView(View):
    """Представление создающее какие-то данные"""
    def get(self, request):
        """Метод обработки GET"""
        template = 'post/main.html'
        data = []
        for i in range(10):
            value = ''.join([choice(ascii_letters) for _ in range(10)])
            data.append(value)
        context = {
            'data': data
        }
        return render(request, template, context)

Шаблоны

В папке templates создадим директорию post (имя нашего app) и создадим в ней два шаблона: базовый и основной:

base.html

{% load static %}
<html lang="ru" data-bs-theme="dark">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
        <title>{% block title %} {% endblock %}</title>
        <link href="{% static  "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet">
        <link rel = "stylesheet" type = "text/css" href="{% static 'highlight/styles/default.min.css'%}">
        <script src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>
        <script src="{% static "post/js/tools.js" %}"></script>
        <script src = "{% static "highlight/highlight.min.js" %}"></script>
    </head>

    <body class="d-flex flex-column min-vh-100 bg-dark">
       
        <nav class="navbar navbar-expand-lg bg-body-tertiary">
            <div class="container-fluid">
              <a class="navbar-brand" href="#">Navbar</a>
              <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
              </button>
              <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                  <li class="nav-item">
                    <a class="nav-link active" aria-current="page" href="#">Home</a>
                  </li>
                  <li class="nav-item">
                    <a class="nav-link" href="#">Link</a>
                  </li>
                  <li class="nav-item dropdown">
                    <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                      Dropdown
                    </a>
                    <ul class="dropdown-menu">
                      <li><a class="dropdown-item" href="#">Action</a></li>
                      <li><a class="dropdown-item" href="#">Another action</a></li>
                      <li><hr class="dropdown-divider"></li>
                      <li><a class="dropdown-item" href="#">Something else here</a></li>
                    </ul>
                  </li>
                  <li class="nav-item">
                    <a class="nav-link disabled" aria-disabled="true">Disabled</a>
                  </li>
                </ul>
                <form class="d-flex" role="search">
                  <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                  <button class="btn btn-outline-success" type="submit">Search</button>
                </form>
              </div>
            </div>
          </nav>

        <!--MAIN-->
        <main class="bg-dark">
            <div class="container-fluid">
                <div class="row justify-content-center">
                    <!--NAV 2-->
                    <nav class="col">
                        {% block nav2 %} {% endblock %}
                    </nav>

                    <!--ARTICLE-->
                    <article class="col-13 col-sm-9 col-md-9 col-lg-7 col-xl-7 col-xxl-7 justify-content-center">
                        {% block content %} {% endblock %}
                    </article>

                    <!--ASIDE-->
                    <aside class="col">
                        {% block aside %} {% endblock %}
                    </aside>
                </div>
            </div>
        </main>
        
    </body>

</html>

И main.html который будет вызываться в post/views.py. В этом шаблоне просто будем выводить стандартные карточки из bootstrap:

{% extends "post/base.html" %}
{% load static %}

{% block title %}Главная{% endblock %}

{% block content %}

    
        <div class="container">
            <div class="row">
                <div class="col">
                    {% for i in data %}
                    <div class="card my-5">
                        <img src="{% static  "img/test.jpeg" %}" class="card-img-top" alt="...">
                        <div class="card-body">
                            <h5 class="card-title">{{i}}</h5>
                            <p class="card-text">Очень важные и глубоки рассуждения.</p>
                        </div>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>

{% endblock %}

Для работы этого шаблона я скачал рандомную картинку и положил её в проект по следующему пути static/img/test.jpeg.

Изменяем urls.py

urls.py в данном случае сделал таким:

from django.contrib import admin
from django.urls import path
from django.conf.urls.static import static
from django.conf import settings
from post.views import AnyDataView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', AnyDataView.as_view(), name='any_data'),   
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

+ как видите я подключил статические файлы поскольку пока нет nginx, который сможет их передавать.

Dockerfile

Ранее мы создали Dockerfile в директории code заходим в него и выставляем следующие настройки:

FROM python:3
# Определяем переменные среды
# Чтобы python не создавал файлы .pyc
ENV PYTHONDONTWRITEBYTECODE=1
# Чтобы видеть выходные данные приложения в реальном времени
ENV PYTHONUNBUFFERED=1
# Устанавливаем рабочую директорию
WORKDIR /code
# Копируем в рабочую директорию файл зависимостей
COPY requirements.txt /code/
# Обновляем pip, устанавливаем зависимости
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
# Копируем содержимое локальной директории code в рабочую директорию
COPY . /code/

Редактируем docker-compose

Заходим в docker-compose.yaml и выставляем следующие настройки:

Начинаем работу с того что выставляем версию version: '3', далее указываем параметры двух сервисов:

  1. db – это СУБД в ней мы указываем переменные окружения с названием базы данных, данными пользователя, так же в PGDATA определяется место хранения БД. Чтобы БД всегда была с нами и не жила в контейнере создаём volumes, который явно прокидывает нашу локальную папку data в контейнер (в указанное в PGDATA расположение).
  2. web – это django проект, тут в build мы указываем путь к Dockerfile для указания инструкций сборки образа, прописываем те же переменные окружение, что и в db кроме PGDATA. Далее links прокидывает хост с БД к django чтобы они могли взаимодействовать (ранее указывали имя хоста в settings.py).  В command производятся четыре команды, первые две производят миграции, третья собирает статические файлы, четвёртая запускает наше приложение через gunicorn на 8001 порту (такой подход к command не является правильным).

Запуск

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

docker-compose up

Если всё было сделано правильно то можно будет зайти на http://0.0.0.0:8001/ и увидеть нечто подобное:

Отлично Django, gunicorn и postgresql теперь работают совместно в контейнерах docker.

NGINX

Файл конфигураций

Заходим в файл конфигурации nginx который мы создали в начале nginx/nginx.conf и выставляем следующие настройки:

upstream mysite {
    #Список бэкэнд серверов для проксирования
    server web:8001;
}

server {
    # Прослушивается 80 порт
    listen 80;
    
    location / {
        proxy_pass http://mysite;
        # Устанавливаем заголовки
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        # Отключаем перенаправление
        proxy_redirect off;
    }

    # подключаем статические файлы
    location /static/ {
        alias /mysite/static/;
    }
    # подключаем медиа файлы
    location /media/ {
        alias /mysite/media/;
    }

}

В данном конфигурационном файле мы указываем, что все поступающие на 80 порт запросы будут перенаправлены нашему хосту с django. Так же ниже указывается путь к статическим и медиа файлам.

 

Dockerfile

Теперь необходим привести файл nginx/Dockerfile к следующему виду:

FROM nginx
# Удаляем дефолтный файл конфигураций
RUN rm /etc/nginx/conf.d/default.conf
COPY ./nginx.conf /etc/nginx/conf.d/
# Создаём папки в которых будут храниться
# статические и медиа файлы
RUN mkdir /mysite
RUN mkdir /mysite/static
RUN mkdir /mysite/media

 

Редактируем docker-compose

Теперь создадим сервис NGINX и немного изменим настройки web получается следующая картина:

Через volumes мы прокидываем папки статических и медиафайлов в подобные папки в контейнере, в ports прокинем локальный порт 8001 в 80 порт nginx (т.к. в конфиге мы указали nginx прослушивать именно 80 порт).

 

Изменяем urls.py

В urls.py теперь можно убрать static(settings.STATIC_URL, document_root=settings.STATIC_ROOT), статические файлы мы будем получать от nginx.

Ниже я закомментировал данную строчку:

from django.contrib import admin
from django.urls import path
from django.conf.urls.static import static
from django.conf import settings
from post.views import AnyDataView


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', AnyDataView.as_view(), name='any_data'),   
] #+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

 

Запуск

Выполняем: 

docker-compose up

Для чистоты проверки работы сайта нужно почистить кэш, после чего опять заходим на http://0.0.0.0:8001/ и увидеть мы должны ровно тоже самое что и в прошлый раз:

Только теперь трафик идёт через nginx, что можно заметить по логам docker-а:

Итог

В данной статье мы разобрались как собрать простенькое приложение в docker-контейнерах. Данный пример является обучающим для людей, которые только начали знакомиться с docker. Если есть желание ознакомиться с исходным кодом то вот он на github.

 

docker nginx python разработка