Зачем именно связка Django + Gunicorn + Nginx
Django принято запускать за WSGI-приложением. Gunicorn — надёжный и простой WSGI-сервер, а Nginx — быстрый и экономичный reverse proxy с раздачей статики и гибкой маршрутизацией. Такая схема хорошо масштабируется, прозрачно логируется и легко автоматизируется через systemd. Мы пройдём весь путь: подготовка окружения Python, структура каталогов, запуск Gunicorn как службы, проксирование через Nginx, статика и media, миграции, healthcheck и, наконец, базовая SSL-настройка. Если своего сервера ещё нет — возьмите небольшой VDS и следуйте инструкциям.
Предварительные условия
Ниже предполагается, что у вас есть сервер с Linux (например, семейство Ubuntu LTS), доступ по SSH с правами sudo, установленный Python 3.x, пакетный менеджер pip и возможность открыть порты 80/443 в брандмауэре. Если база данных внешняя — убедитесь, что она доступна и правильно защищена. Для простоты примеров можно начать с SQLite, а затем переключиться на PostgreSQL или MySQL. Выбираете панель для управления сервером — посмотрите обзор в статье панелей для VDS.
Структура проекта и системный пользователь
Рекомендуем создать отдельного системного пользователя без shell-доступа для приложения. Каталоги будем держать в одном корне, чтобы упростить пути в конфиге Nginx и юните systemd.
# создание пользователя и каталогов
sudo adduser --system --group --home /srv/myproj myproj
sudo mkdir -p /srv/myproj/app
sudo mkdir -p /srv/myproj/run
sudo mkdir -p /srv/myproj/logs
sudo mkdir -p /srv/myproj/static
sudo mkdir -p /srv/myproj/media
sudo chown -R myproj:myproj /srv/myproj
Каталоги /srv/myproj/static и /srv/myproj/media пригодятся для раздачи статики и пользовательских файлов напрямую из Nginx. Каталог /srv/myproj/run используем для Unix-сокета Gunicorn.

Python: виртуальное окружение и Django
Виртуальное окружение важнее, чем кажется: версии библиотек, сборки Си-зависимостей и обновления не будут мешать системному Python. Создаём и активируем venv, устанавливаем Django и Gunicorn.
sudo -u myproj python3 -m venv /srv/myproj/venv
sudo -u myproj /srv/myproj/venv/bin/pip install --upgrade pip wheel
sudo -u myproj /srv/myproj/venv/bin/pip install django gunicorn
Инициализируем проект. Пример названия — config (внутренний пакет с настройками), а приложение пусть будет core. Вы можете использовать уже существующий репозиторий с кодом, тогда просто разместите его в /srv/myproj/app.
sudo -u myproj /srv/myproj/venv/bin/django-admin startproject config /srv/myproj/app
Проверим локальный запуск через manage.py:
cd /srv/myproj/app
sudo -u myproj /srv/myproj/venv/bin/python manage.py runserver 127.0.0.1:8000
Если стартует — отлично. Остановите сервер и приведите настройки проекта к продакшн-виду.
Базовые настройки Django для продакшна
В файле /srv/myproj/app/config/settings.py задаём минимум: DEBUG = False, список хостов, пути к статике и media. Подставьте свой домен и отредактируйте пути под нашу структуру.
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
STATIC_URL = "/static/"
STATIC_ROOT = "/srv/myproj/static"
MEDIA_URL = "/media/"
MEDIA_ROOT = "/srv/myproj/media"
# Дополнительно для SSL в дальнейшем
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
Если используете PostgreSQL или MySQL, настройте DATABASES с безопасным хранением паролей через переменные окружения. Для старта на SQLite достаточно дефолта, чтобы отладить инфраструктуру.
Миграции и сбор статики
Прежде чем показывать приложение наружу, прогоним миграции и соберём статические файлы. Команды выполняем из каталога приложения.
cd /srv/myproj/app
sudo -u myproj /srv/myproj/venv/bin/python manage.py migrate --noinput
sudo -u myproj /srv/myproj/venv/bin/python manage.py collectstatic --noinput
При необходимости создайте суперпользователя:
sudo -u myproj /srv/myproj/venv/bin/python manage.py createsuperuser
Если у вас сторонние приложения Django, которые генерируют статические файлы, убедитесь, что они оказываются в /srv/myproj/static. В противном случае Nginx не найдёт их без обращения к Gunicorn, теряя производительность.
Gunicorn: тестовый запуск и настройка под systemd
Проверим, что Gunicorn подхватывает WSGI-приложение проекта. В Django точка входа обычно config.wsgi:application.
cd /srv/myproj/app
sudo -u myproj /srv/myproj/venv/bin/gunicorn --bind 127.0.0.1:8001 config.wsgi:application
Если всё ок, выключаем и переходим к штатному Unix-сокету, чтобы Nginx подключался локально без TCP-порта. Настроим systemd-юнит для демонизации и автозапуска.
sudo nano /etc/systemd/system/gunicorn-myproj.service
[Unit]
Description=Gunicorn for Django myproj
After=network.target
[Service]
User=myproj
Group=myproj
WorkingDirectory=/srv/myproj/app
Environment="DJANGO_SETTINGS_MODULE=config.settings"
Environment="PYTHONUNBUFFERED=1"
ExecStart=/srv/myproj/venv/bin/gunicorn --workers 3 --bind unix:/srv/myproj/run/gunicorn.sock --access-logfile - --error-logfile - config.wsgi:application
Restart=on-failure
RestartSec=5
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
Обратите внимание: сокет в /srv/myproj/run/gunicorn.sock лежит в каталоге, доступном пользователю Nginx. По умолчанию Nginx стартует от пользователя www-data, ему нужна хотя бы возможность читать этот сокет. Чаще всего достаточно выставить владельца и группу проекта, а в Nginx оставить группу www-data — Unix-сокет будет читаться благодаря правам.
sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn-myproj.service
sudo systemctl status gunicorn-myproj.service
Если статус активен и сокет существует, можно подключать Nginx.
Nginx: reverse proxy и раздача статики/media
Создадим отдельный upstream на Unix-сокет, раздачу /static/ и /media/, а также базовые заголовки и лимиты. Пример конфига сервера на 80 порту:
sudo nano /etc/nginx/sites-available/myproj.conf
upstream myproj_backend {
server unix:/srv/myproj/run/gunicorn.sock;
}
server {
listen 80;
server_name example.com www.example.com;
client_max_body_size 20m;
access_log /var/log/nginx/myproj.access.log;
error_log /var/log/nginx/myproj.error.log warn;
location /static/ {
alias /srv/myproj/static/;
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
alias /srv/myproj/media/;
expires 1h;
add_header Cache-Control "private";
}
location /healthz {
proxy_pass http://myproj_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 5s;
}
location / {
proxy_pass http://myproj_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
sudo ln -s /etc/nginx/sites-available/myproj.conf /etc/nginx/sites-enabled/myproj.conf
sudo nginx -t
sudo systemctl reload nginx
Проверьте, что /static/ и /media/ отдаются без обращения к Gunicorn (по логам Nginx это видно: доступы к статике не должны идти в backend). Параметр client_max_body_size отвечает за лимит загрузок. Для форм с файлами используйте значение с запасом.
SSL: базовая схема
Когда всё работает по HTTP, переходите на HTTPS. На уровне Nginx вам понадобится сервер на 443 порту с ssl_certificate и ssl_certificate_key, а 80-й порт выдаёт 301-редирект. Дополнительно можно включить HSTS, но осторожно: это необратимо для клиентов на период TTL заголовка. Подробнее про редиректы и HSTS — в материале о 301 и HSTS. Для выпуска и продления используйте проверенные SSL-сертификаты.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
include /etc/nginx/snippets/ssl-params.conf;
client_max_body_size 20m;
location /static/ {
alias /srv/myproj/static/;
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
alias /srv/myproj/media/;
expires 1h;
add_header Cache-Control "private";
}
location /healthz {
proxy_pass http://myproj_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 5s;
}
location / {
proxy_pass http://myproj_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
Не забудьте, что в настройках Django включены параметры для корректной работы за reverse proxy при HTTPS через заголовок X-Forwarded-Proto.
Права доступа и безопасность
Каталоги /srv/myproj/static и /srv/myproj/media должны быть доступны пользователю Nginx для чтения. Для загрузки файлов приложением директории media нужны права на запись от имени пользователя Gunicorn (у нас это myproj). Самый простой вариант — оставить владельца myproj:myproj и режимы 0755 для каталогов и 0644 для файлов, этого достаточно для чтения Nginx. Для записываемых директорий иногда используют группу www-data и setgid-бит, но придерживайтесь минимально необходимых прав.
Следите, чтобы у каталога с сокетом не было слишком жёстких прав, иначе Nginx не сможет открыть сокет. Для журналов используйте journald либо перенаправляйте лог Gunicorn в stdout/stderr (в юните уже так) и читайте через journalctl -u gunicorn-myproj.service. Если хотите файловые логи, переведите --access-logfile и --error-logfile на путь в /srv/myproj/logs и обеспечьте права.
Миграции при деплое
Типичный деплой Django включает несколько шагов: сбор зависимостей, применение миграций, сбор статики, рестарт Gunicorn. Удобно иметь небольшой скрипт деплоя, который выполняет эти команды последовательно и фейлится на первом сбое. При использовании внешней БД не забывайте про резервные копии и откат миграций при необходимости.
cd /srv/myproj/app
sudo -u myproj /srv/myproj/venv/bin/pip install -r requirements.txt
sudo -u myproj /srv/myproj/venv/bin/python manage.py migrate --noinput
sudo -u myproj /srv/myproj/venv/bin/python manage.py collectstatic --noinput
sudo systemctl restart gunicorn-myproj.service
Можно оформить миграции как отдельный oneshot-юнит и вызывать его перед рестартом сервиса. Но проще и прозрачнее держать команды в сценарии деплоя, чтобы видеть пошаговый вывод и быстро диагностировать ошибки миграций.
Healthcheck: приложение и инфраструктура
Healthcheck нужен для мониторинга и автоматических перезапусков в оркестраторах и балансировщиках. На уровне приложения — это эндпоинт, который возвращает 200 OK быстро и без нагруженных запросов. На уровне Nginx можно сделать простой ответ 200, чтобы проверять только web-узел. Ниже пример Django-вью и Nginx-локации.
Django-вью для /healthz
# config/urls.py
from django.urls import path
from django.http import JsonResponse
def healthz(request):
return JsonResponse({"status": "ok"})
urlpatterns = [
path("healthz", healthz),
]
Если хотите глубже проверять доступность БД, добавьте простейший запрос к базе и увеличьте таймаут в локации /healthz Nginx до разумного значения. Важно: такой healthcheck не должен блокироваться долгими внешними вызовами.
Практика: делайте два уровня проверок — лёгкий /healthz без БД для частых проверок балансировщиком и отдельный /healthz/db для редких, но более глубоких проверок мониторингом.
systemd: автозапуск, рестарты и окружение
Наш юнит уже настроен на перезапуск при сбоях. Добавьте переменные окружения через файл, чтобы не хранить секреты в явном виде. Затем подключайте его через EnvironmentFile. Не забывайте, что права на файл с секретами должны быть ограничены.
sudo nano /etc/myproj.env
DJANGO_SETTINGS_MODULE=config.settings
SECRET_KEY=change-me
DATABASE_URL=postgres://user:pass@host:5432/dbname
sudo chmod 600 /etc/myproj.env
sudo chown root:root /etc/myproj.env
# правим юнит
sudo nano /etc/systemd/system/gunicorn-myproj.service
[Service]
EnvironmentFile=/etc/myproj.env
sudo systemctl daemon-reload
sudo systemctl restart gunicorn-myproj.service
Если вам нужно проводить мягкие релизы, можно использовать systemctl restart в сочетании со стратегиями Gunicorn (--max-requests и др.). Для релизов без простоя см. наш разбор приёмов в статье о миграции без даунтайма zero-downtime.

Диагностика и типичные ошибки
- Ошибка 502 Bad Gateway: чаще всего Nginx не может открыть Unix-сокет. Проверьте, что файл
/srv/myproj/run/gunicorn.sockсуществует и права позволяют пользователю Nginx его читать. - Статика не грузится: забыли
collectstaticили неверныйaliasв Nginx. Проверьте, нет ли лишнего слеша или опечатки в пути. - CSRF/Session проблемы после включения HTTPS: проверьте
SECURE_PROXY_SSL_HEADER,CSRF_COOKIE_SECUREиSESSION_COOKIE_SECUREв Django. - Загрузки обрываются: увеличьте
client_max_body_sizeв Nginx и проверьте таймаутыproxy_read_timeout. - Миграции «висят»: убедитесь, что нет долгих блокировок в БД. Локализуйте команду и запускайте в отдельности от перезапуска сервера.
Производительность и масштабирование
Подберите количество воркеров Gunicorn под CPU: часто используют правило 2–4 воркера на ядро для синхронных воркеров, но всё зависит от нагрузки и блокирующих операций. Для I/O-нагруженных участков рассмотрите асинхронные воркеры, однако это требует аккуратности с библиотеками. На стороне Nginx держите кэш статических ответов и корректные заголовки Cache-Control — это разгружает Gunicorn и уменьшает TTFB.
Если проект растёт, вынесите статику в отдельный CDN, а media — в объектное хранилище, но в рамках этой статьи мы фокусируемся на локальном диске для простоты и предсказуемости.
Итоги
Мы прошли весь цикл: от подготовки Python-окружения и структуры каталогов до запуска Django через Gunicorn, проксирования в Nginx, раздачи статики и media, применения миграций, настройки healthcheck и базового SSL. Такая схема проста, воспроизводима и легко автоматизируется. Дальше вы можете дополнять её CI/CD, метриками и алертами, а также усложнять деплой, не меняя фундаментальные компоненты.


