В прошлом уроке мы подготовили инфраструктуру: сервер, домен, SSH, код проекта на сервере через Git, установленные зависимости. Теперь настроим саму связку Django → Gunicorn → Nginx, благодаря которой страницы filmsite станут доступны по доменному имени в браузере.
Работаем по-прежнему в SSH-сессии на сервере, в каталоге /var/www/filmsite, с активированным виртуальным окружением (source venv/bin/activate).
Напоминание про развилку из прошлого урока: везде, где действия отличаются в зависимости от базы данных, стоит пометка [Только PostgreSQL] или [Только SQLite].
Все изменения в этом шаге вносим в файл:
filmsite/settings.py
(путь указывайте относительно корня проекта, где лежит manage.py).
Перед изменением settings.py проверьте, не использует ли ваш проект переменные окружения.
Если в репозитории есть файл .env.example, а рабочего .env нет — создайте .env на основе .env.example:
cp .env.example .envПосле этого все значения, которые отличаются для вашего сервера (например, пароль базы данных, SECRET_KEY, настройки базы данных и другие переменные), изменяйте в .env, а не в .env.example.
Файл .env не должен попадать в Git. .env.example, наоборот, обычно хранится в репозитории как шаблон с названиями необходимых переменных, но без настоящих секретных значений.
Если ваш проект уже использует
.envи загрузку переменных окружения вsettings.py, не нужно создавать эту настройку заново. Используйте существующий механизм и меняйте необходимые значения в.env.
В самом начале файла добавьте импорт, если его ещё нет:
import osОн понадобится для путей к статике и медиафайлам.
По умолчанию Django блокирует все внешние запросы — это защита от случайного запуска в продакшене без явного разрешения. Найдите переменную ALLOWED_HOSTS и укажите домен и IP сервера:
ALLOWED_HOSTS = [
'filmsite.ru', # ваш домен
'www.filmsite.ru',
'123.123.123.123', # IP вашего сервера — пригодится при диагностике до подключения SSL
]Забегая вперёд: IP сервера в
ALLOWED_HOSTSпригодится в уроке 35 при получении SSL-сертификата — Certbot иногда обращается к серверу напрямую по IP на этапе проверки. Если этой строки не будет — столкнётесь сDisallowedHostв самый неподходящий момент. Добавим сразу, чтобы не возвращаться к этому файлу лишний раз.
Если не сделать этот шаг — при открытии сайта вы получите ошибку DisallowedHost.
Если в файле есть строка:
INTERNAL_IPS = ["127.0.0.1"]— закомментируйте её, она используется только для отладки (Django Debug Toolbar) и не нужна в продакшене.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'filmsite_db',
'USER': 'filmsite_user',
'PASSWORD': 'strong_password_here',
'HOST': 'localhost',
'PORT': '',
}
}Здесь используются имя базы и пользователь, которых мы создали в уроке 33.
Про пароль прямо в
settings.py— это временное упрощение для первого запуска. Вынесем его в переменные окружения в конце этого урока.
Если вы работали только с SQLite — ничего менять не нужно, оставляем стандартную конфигурацию:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}База данных — это файл db.sqlite3 внутри проекта, который приехал вместе с кодом при git clone (если вы закоммитили его) или будет создан заново при первой миграции.
В нашем проекте есть и то, и другое: статика (CSS, JS) и медиафайлы, которые загружают пользователи (постеры фильмов, фото актёров и режиссёров, аватары в UserProfile). Это две разные сущности, и путать их — частая ошибка.
Добавьте в settings.py:
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')Если у вас есть переменная STATICFILES_DIRS — закомментируйте её:
# STATICFILES_DIRS = [...]Почему так:
STATIC_ROOT— это папка, куда Django соберёт все статические файлы проекта в одно место командойcollectstatic. В продакшене статику отдаёт Nginx напрямую, без участия Django.MEDIA_ROOT— папка, куда сохраняются файлы, загруженные черезImageField/FileField(постеры, аватары). Это не то же самое, что статика: медиафайлы создаются пользователями во время работы сайта, а не собираются заранее.STATICFILES_DIRSв продакшене не нужен — эта настройка используется для локальной разработки, когда статика лежит в нескольких местах проекта.collectstaticсоберёт всё содержимое приложений вSTATIC_ROOTсамостоятельно.
Из корня проекта, с активированным окружением, выполняем:
python manage.py makemigrations
python manage.py migrate[Только PostgreSQL] — на этом шаге Django создаст все таблицы проекта заново в пустой базе filmsite_db. Если вы переносили данные из SQLite в прошлом курсе через dumpdata/loaddata — процедура ниже, в шаге 6.
Собираем статику:
python manage.py collectstaticDjango спросит подтверждение — вводим yes.
Убедитесь, что в проекте появилась папка static/, а внутри неё — CSS, JS и другие файлы, собранные из всех приложений (включая django.contrib.admin, чьи стили тоже должны подгрузиться для админки).
Прежде чем настраивать Gunicorn и Nginx, убедимся, что сама конфигурация Django корректна.
Откроем порт 8000 временно:
sudo ufw allow 8000Запускаем:
python manage.py runserver 0.0.0.0:8000В браузере открываем:
http://ваш_домен:8000
- сайт открывается;
- каталог фильмов может быть пустым, если данные ещё не перенесены — это нормально;
- если видите
DisallowedHost— вернитесь кALLOWED_HOSTSв шаге 1.
Ctrl + C.
Если хотите перенести реальные данные каталога (фильмы, режиссёров, пользователей) с локальной машины на сервер:
Локально:
python manage.py dumpdata --indent=2 -o db.jsonЗагрузите db.json на сервер любым способом (можно через git add/git push, если файл небольшой и не содержит чувствительных данных, либо через scp).
На сервере:
python manage.py loaddata db.jsonПосле этого на сайте появятся фильмы, режиссёры и пользователи из вашей локальной базы, включая суперпользователя.
Если вы тестировали регистрацию/пароли локально — учтите, что хэши паролей перенесутся вместе с пользователями, и локальные пароли будут работать и на сервере.
Для production-сервера рекомендуется использовать отдельный SECRET_KEY, отличный от ключа локальной разработки.
Создать новый ключ можно прямо в терминале:
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"Скопируйте получившееся значение.
Если проект использует .env, сохраните новый ключ в .env:
SECRET_KEY=сюда_вставьте_новый_ключЕсли переменные окружения в проекте пока не используются — временно замените значение SECRET_KEY в settings.py. В конце урока в бонусе показано, как вынести секретные данные в .env.
В settings.py:
DEBUG = FalseDEBUG = True Django показывает подробные страницы ошибок с кусками кода и путями на сервере — в открытом доступе это серьёзная угроза безопасности.
Находясь в виртуальном окружении:
gunicorn --bind 0.0.0.0:8000 filmsite.wsgiЕсли ваш основной модуль настроек называется иначе (например,
core, если вы называли проект по-другому приstartproject) — команда будетcore.wsgi.
В браузере снова открываем http://ваш_домен:8000.
- сайт работает;
- стили не загружаются — это ожидаемо: Gunicorn отдаёт только Python-логику, статику пока некому раздавать (этим займётся Nginx);
- если вместо страницы — ошибка, проверьте вывод в терминале: чаще всего это опечатка в имени модуля
wsgiили незавершённые миграции.
Останавливаем сервер (Ctrl + C) и выходим из окружения:
deactivateТакой процесс работает только пока открыт терминал и запущен сам процесс Gunicorn. Поэтому после проверки мы остановили его с помощью Ctrl + C.
Дальше запускать Gunicorn вручную больше не нужно. В следующем шаге мы настроим его как systemd-сервис.
После этого Gunicorn будет запускаться системой автоматически, работать в фоне и перезапускаться при сбое. Управлять им можно будет через systemctl, например:
sudo systemctl start gunicorn
sudo systemctl stop gunicorn
sudo systemctl restart gunicorn
sudo systemctl status gunicornПостоянно держать открытый SSH-терминал с запущенным Gunicorn неудобно и ненадёжно — при обрыве соединения процесс завершится. Настроим Gunicorn как системный сервис, который запускается автоматически и перезапускается при сбое.
Создаём файл сервиса:
sudo nano /etc/systemd/system/gunicorn.serviceСодержимое:
[Unit]
Description=gunicorn daemon for filmsite
After=network.target
[Service]
User=root
Group=www-data
WorkingDirectory=/var/www/filmsite
ExecStart=/var/www/filmsite/venv/bin/gunicorn \
--workers 3 \
--bind unix:/var/www/filmsite/filmsite.sock \
filmsite.wsgi:application
Restart=on-failure
[Install]
WantedBy=multi-user.targetЕсли ваш модуль настроек называется не
filmsite, а иначе — заменитеfilmsite.wsgi:applicationна своё значение, напримерcore.wsgi:application.
Сохраняем файл (Ctrl+O, Enter, Ctrl+X в nano).
Запускаем сервис:
sudo systemctl enable --now gunicorn
sudo systemctl status gunicorn- статус active (running);
- в каталоге проекта появился файл
filmsite.sock.
Если статус failed — смотрите подробности:
sudo journalctl -u gunicorn -n 50Чаще всего причина — опечатка в пути WorkingDirectory или в имени модуля wsgi.
Создаём конфигурацию:
sudo nano /etc/nginx/sites-available/filmsiteСодержимое:
server {
listen 80;
server_name filmsite.ru www.filmsite.ru;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
root /var/www/filmsite;
}
location /media/ {
root /var/www/filmsite;
}
location / {
include proxy_params;
proxy_pass http://unix:/var/www/filmsite/filmsite.sock;
}
}Обратите внимание: root /var/www/filmsite; в блоках /static/ и /media/ должен соответствовать реальному расположению папок static/ и media/, которые мы создали в шагах 3–4. Если у вас, например, проект лежит не прямо в /var/www/filmsite, а на уровень глубже — этот путь тоже нужно поправить, иначе Nginx будет искать файлы не там и вернёт 404 на все картинки и стили.
filmsite.ru и www.filmsite.ru нужно заменить на ваш домен.
Активируем конфигурацию:
sudo ln -s /etc/nginx/sites-available/filmsite /etc/nginx/sites-enabledОбязательно проверяем синтаксис перед перезапуском:
sudo nginx -tЕсли видите syntax is ok и test is successful — перезапускаем:
sudo systemctl restart nginx
sudo systemctl restart gunicornПочему так важно
nginx -tпередrestart/reload: если в конфиге ошибка, а вы сразу перезапустите Nginx — он может не подняться вообще, и сайт станет недоступен полностью, а не только с ошибкой на одной странице. Проверка синтаксиса — это 2 секунды, которые экономят от простоя сайта.
Закрываем временный порт 8000, которым пользовались для тестов, и открываем стандартные порты для Nginx:
sudo ufw delete allow 8000
sudo ufw allow 'Nginx Full'Открываем в браузере:
http://filmsite.ru
- сайт открывается по обычному домену, без порта в адресе;
- стили и изображения (постеры, аватары) подгружаются корректно;
- каталог фильмов, авторизация и формы работают так же, как локально.
Проверьте права доступа:
sudo chmod 755 /var/www/filmsite/static
sudo chmod 755 /var/www/filmsite/mediaЕсли это не помогло — сверьте путь в location /static/ конфига Nginx с реальным STATIC_ROOT из settings.py. Несовпадение путей — самая частая причина 404 на статику, а не права доступа. Подробный разбор этой и других частых ошибок — в следующем уроке.
Прежде чем переходить к HTTPS, стоит убрать пароль от базы данных (и в будущем — секретный ключ Django) из settings.py в открытом виде, особенно если репозиторий публичный на GitHub.
Устанавливаем библиотеку:
pip install python-dotenvСоздаём файл .env в корне проекта на сервере (не коммитим его в Git):
# .env
SECRET_KEY=ваш_секретный_ключ
DB_PASSWORD=strong_password_hereВ settings.py:
from dotenv import load_dotenv
load_dotenv()
SECRET_KEY = os.environ.get('SECRET_KEY')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'filmsite_db',
'USER': 'filmsite_user',
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': 'localhost',
'PORT': '',
}
}Добавьте .env в .gitignore локально, чтобы файл случайно не попал в репозиторий:
# .gitignore
.env
После изменения settings.py не забудьте перезапустить Gunicorn:
sudo systemctl restart gunicornК этому моменту у вас должно быть:
ALLOWED_HOSTS,DATABASES,STATIC_ROOT,MEDIA_ROOTнастроены под сервер;- миграции применены, статика собрана через
collectstatic; DEBUG = False;- Gunicorn работает как systemd-сервис (
active (running)); - Nginx настроен, отдаёт сайт по домену на порту 80, включая статику и медиафайлы;
- чувствительные данные (пароль от БД,
SECRET_KEY) вынесены в.env.
Сайт уже полностью работает по http://filmsite.ru — в следующем уроке подключим SSL-сертификат и переведём сайт на https://, а также разберём расширенный чек-лист типичных ошибок деплоя.