Готовим проект: 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-контейнерах.
Структура проекта
Любой проект начинается с его структуры, для начала подготовим три основных папки:
- code – здесь будет храниться django проект.
- data – данная папка будет хранить базу данных.
- 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 записываем все необходимые зависимости в данном случае это:
- django
- psycopg2-binary
- 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', далее указываем параметры двух сервисов:
- db – это СУБД в ней мы указываем переменные окружения с названием базы данных, данными пользователя, так же в PGDATA определяется место хранения БД. Чтобы БД всегда была с нами и не жила в контейнере создаём volumes, который явно прокидывает нашу локальную папку data в контейнер (в указанное в PGDATA расположение).
- 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.