Предисловие
Для написания этой статьи ушло очень много сил и времени. Я натыкался на множество инструкций, как на английском, так и на русском языках, но как я понял, - они все были клонами оригинальной статьи на Digital Ocean. Спросите вы, почему я так считаю, а все потому, что все ошибки и неточности передаются с одного ресурса на другой без всяких изменений.
Подготовка
У нас есть обычный VPS c ОС Ubuntu, и мы уже написали в PyCharm или в блокноте свой сайт на Django и его осталось всего лишь опубликовать, привязать домен, установить сертификат и в путь.
Первым делом необходимо обновить список репозиториев и установить сразу же пакет nginx:
apt-get update
apt-get install nginx
Я решил хранить файлы сайта в каталоге: /var/www/. В данном случае перемещаемся в каталог cd /var/www/ и создаем новый каталог mkdir geekhero и получаем такой путь: /var/www/geekhero/
Переходим в новый каталог geekhero: cd geekhero и создаем виртуальное окружение: python3 -m venv geekhero_env
Активируем виртуальное окружение: source geekhero_env/bin/activate и устанавливаем в него Django: pip install Django и сразу же ставим pip install gunicorn
Создаем проект: django-admin startproject ghproj
Далее нужно произвести все первичные миграции; для этого прописываем: python manage.py makemigrations
, затем python manage.py migrate
.
После этого создаем административную учетную запись: python manage.py createsuperuser
и следуем инструкции.
Далее уже создаем applications, но так как вы читаете данную статью, то вы уже умеете все это делать.
Заходим в Settings.py и прописываем, если отсутствует:import os
– в заголовке, остальное в самом низу текстового файла:
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
Настройка Gunicorn
Идем в каталог /etc/systemd/system/ и создаем два файла: gunicorn.service и gunicon.socket:
Содержимое файла gunicorn.service:
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target
[Service]
User=root
WorkingDirectory=/var/www/geekhero #путь до каталога с файлом manage.py
ExecStart=/var/www/geekhero/geekhero_env/bin/gunicorn --workers 5 --bind unix:/run/gunicorn.sock ghproj.wsgi:application
#путь до файла gunicorn в виртуальном окружении
[Install]
WantedBy=multi-user.target
Содержимое файла gunicorn.socket:
[Unit]
Description=gunicorn socket
[Socket]
ListenStream=/run/gunicorn.sock
[Install]
WantedBy=sockets.target
Для проверки файла gunicorn.service на наличие ошибок:
systemd-analyze verify gunicorn.service
Настройка NGINX
Далее идем в каталог: /etc/nginx/sites-available/ и создаем файл geekhero (название вашего сайта) без расширения:
server {
listen 80;
server_name example.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
root /var/www/geekhero; #путь до static каталога
}
location /media/ {
root /var/www/geekhero; #путь до media каталога
}
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn.sock;
}
}
Для того, чтобы создать символическую ссылку на файл в каталоге /etc/nginx/site-enabled/ необходимо ввести следующую команду:
sudo ln -s /etc/nginx/sites-available/geekhero /etc/nginx/sites-enabled/
При любых изменениях оригинального файла, ярлык из sites-enabled нужно удалять и пересоздавать заново командой выше или выполнять команду: sudo systemctl restart nginx
Финальный этап
Для проверки конфигурации nginx нужно ввести команду:
sudo nginx -t
sudo nginx -t
Далее запускаем службу gunicorn и создаем socket:
sudo systemctl enable gunicorn
sudo systemctl start gunicorn
Для отключения выполняем команды:
sudo systemctl disable gunicorn
sudo systemctl stop gunicorn
Также, эти обе команды пригодятся, если вы будете вносить какие-либо изменения в HTML или python файлы, чтобы обновить свой сайт, но помните, что, если вносите изменения в модели, то обязательно нужно сделать python manage.py makemigrations <app> и migrate <app> из каталога с проектом.
Для первичного запуска / полной остановки сервиса Gunicorn: service gunicorn start / service gunicorn stop
Чтобы посмотреть статус запущенного сервиса нужно ввести:
sudo systemctl status gunicorn
или
sudo journalctl -u gunicorn.socket
(с последней командой правильный вывод такой: мар 05 16:40:19 byfe systemd[1]: Listening on gunicorn socket. )
Для проверки создания сокета, необходимо ввести команду:
file /run/gunicorn.sock
Такой вывод считается правильным: /run/gunicorn.sock: socket
Если внес какие-либо изменения в файл gunicorn.service или .socket, необходимо выполнить команду:
systemctl daemon-reload
Если все нормально сработало, то можем запустить nginx:
sudo service nginx start
Получаем сертификат SSL для домена
Установим certbot от Let's Encrypt: sudo apt-get install certbot python-certbot-nginx
Произведем первичную настройку certbot: sudo certbot certonly --nginx
И наконец-то автоматически поправим конфигурацию nginx: sudo certbot install --nginx
Осталось только перезапустить сервис nginx: sudo systemctl restart nginx
Итог
В рамках этой статьи мы разобрали как вывести наш сайт в production, установив Django, Gunicorn, nginx и даже certbot от Let's Encrypt.
TheGodfather
Только один вопрос — почему без контейнеров?
Tanner
Контейнеры не нужны.
TheGodfather
Это шутка такая, да?
Когда у вас VPS даже с парой-тройкой сервисов, изоляция решает. Сегодня у вас джанга с SQLite, завтра сервис с MariaDB, а послезавтра — условный телеграм бот на PHP с Postgre. И что, все это на хост будете ставить? А когда обновите версию джанги в одном из проектов, руками пересоздавать venv на хосте будете? Или будете писать чудесный деплой-скрипт, который ходит по файловой системе, обновляет репо, создает виртуальные окружения, пересоздает все что можно? *картинка с троллейбусом*
Тот же certbot — в чем смысл ставить лишний софт на хост, который вам нужен раз в 3 месяца, запускается в одну строчку из контейнера и абсолютно независим от всего остального, что вы там наставили?
andreymal
Ну да, я делаю всё именно так. В качестве "скрипта" использую Ansible-плейбук, в нём уже есть модуль для управления venv и прочие радости жизни. В чём проблема?
TheGodfather
Да нет, если вам норм, кто ж вам запретит, нет никакой проблемы. Но мне кажется, что условные
git pull && docker-compose up -d --build
достигают той же цели, но куда меньшими затратами и намного проще и надежнее. Ну и docker/docker-compose нынче куда более распространены, нежели Ansible.andreymal
А затраты на написание докерфайлов и организацию взаимодействия между контейнерами вы решили тактично умолчать :) Серьёзно, я как-то пробовал автоматизировать развёртывание одного реального PHP-проекта (хоть я и питонист, ну да ладно) — Ansible-плейбук написался без проблем, а довести докер-контейнеры до пригодного для продашкена состояния у меня так и не получилось
TheGodfather
Я говорю о проекте уровня описанного в статье — простой веб-сервис с условным nginx+uwsgi/gunicorn/php+mysql/mariadb/postgre. Бложик или пет-прожект на джанге или обычный монолит на PHP. Какая еще «организация взаимодействия»?
environment: {DB_HOST: db}
иdepends_on: db
для зависимых контейнеров? Вот и все «взаимодействие». Ну да, простите великодушно, «тактично умолчал», потому что это три строчки, которые после первых пары раз пишутся на автомате и уже не думаешь даже. Секундное дело. Но зато после этого вы получаете docker-compose файл, который кто угодно может на своей машине запустить, имея из зависимостей лишь, собственно, докер и компоуз. Один клик — и весь сервис поднят. Да, естественно, если вам захочется больше плюшек, health-чеков, определенного порядка запуска контейнеров, дальше будет сложнее, но для описанного в посте уровня простенького проекта ничего этого не нужно.Не говоря уж про то, что написание Dockerfile это как минимум «правило хорошего тона», по которому можно понять, какие у вашего проекта зависимости. Если мы говорим про питон, то каждая вторая нетривильная зависимость из requirements.txt будет требовать какой-нибудь нативной библиотеки, про которую, естественно, автор не упомянет в ридми. А если вы напишете в докерфайле «pip install -r requirements.txt», то это банальный тест — если оно билдится, значит побилдится и у всех остальных. Не раз и не два такое было, что на хосте вроде работает, а пишешь в докерфайл — опа, оказывается, еще десяток нативных зависимостей надо доставлять (которые на хосте были поставлены неизвестно когда и уж все забыли про них).
Кэп подсказывает, что Ansible playbook у вас написался без проблем, потому что у вас есть опыт в написании плэйбуков. А у меня наоборот — я подобные простые 2-3 сервиса быстро и без проблем оберну в Dockerfile+docker-compose, а вот если надо будет Ansible юзать, то я уйду гуглить на неопределенное время.
andreymal
Окей, начнём с банального — раздача статических файлов. В какой контейнер их класть? В каком контейнере запустить "manage.py collectstatic" и как при этом не наплодить стопицот лишних докер-слоёв?
Чуть менее банальное — почти любой Django-проект однажды дорастёт до необходимости использовать Celery, где и как запускать его воркеры? Если придётся запускать несколько контейнеров — как им всем обеспечить беспроблемный доступ на запись в media-каталог — возможно, делать chmod 777 и umask 000, или есть что-то более умное?
Ну ладно, если запустить два контейнера из одного образа, у них будут одинаковые uid и проблем с доступом возможно не будет. А что если однажды придётся собрать ещё один образ (из моей личной практики — syncthing, например) и дать ему доступ на запись к тому же общему media-каталогу — как это организовать, чтобы было достаточно безопасно и чтобы при этом не было проблем?
Ага, а контейнеры тут при чём? Пусть автор просто прогонит установку на чистой системе (да хоть в том же контейнере, лол) и в ридми допишет зависимости, да и всё. В плейбуке, кстати, зависимости тоже прописаны.
То же самое можно сделать через CI/CD — в скрипт установки для условного Travis CI или Github Actions тоже придётся прописать все зависимости.
Так изначально его не было, у меня знания тоже не из воздуха появились.
А я до сих пор продолжаю гуглить инфу про докер, но так и не могу собрать что-то пригодное для продакшена ?\_(?)_/?
TheGodfather
Вы намекаете, что мне тоже статью стоит написать? Честно говоря, я понимаю, потому что если гуглить «django docker», то во всех этих хипстерских seo-friendly бложиках ни один чел не идет дальше хелловорлда и тему статических файлов не затрагивает. Я сам довольно долго гуглил и спрашивал в чатиках насчет «правильного» способа.
На сегодняшний день я использую один из следующих вариантов:
— Shared volume лишь между контейнерами (но не с хостом, см. SO). Тогда collectstatic пишется в контейнере с джангой и nginx контейнер просто видит папку со статиками
— Если же volume не вариант (например, в Docker Swarm), то можно сделать multistage билд для nginx контейнера. Ну т.е. что-то подобное:
FROM python:3.9 as builder
RUN pip install Django
COPY . /app
WORKDIR /app
RUN manage.py collectstatic --noinput
FROM nginx
COPY config/nginx.conf /etc/nginx/config
COPY --from=builder /static /static
— Ну и «плохой» вариант, но в общем-то вполне рабочий для небольших проектиков без сотен реквестов в секунду — используйте
manage.py runserver
. Тогда вам даже второй контейнер с nginx не нужен. Есть несколько библиотек типа whitenoise, которые в теории несколько улучшают ситуацию с «runserver не для продакшна».Не могу ответить, поскольку ни в одном из проектов Celery не использовал, не было нужды. Но есть подозрение, что это может выглядеть как один контейнер, в котором воркеры сами скейлятся как надо (собственно, как uwsgi для того же джанго сам делает).
Понятно, что хороший автор опишет все, но Dockerfile (как и ваш плейбук, видимо) — это не просто текст, это реальные инструкции, которые должны работать, чтобы проект поднялся. И само средство вынуждает автора описать environment. Лишь один коммент — в каком проценте open source проектов вы видели плейбук (или, например, Vagrantfile?) в репозитории? Сравните с процентом репозиториев, имеющих Dockerfile.
Ну видите, мы в одинаковой ситуации, только с разными тулами :) Только в самом начале я пошел гуглить про довер и теперь знаю о докере и применяю его, а вы пошли гуглить про Ansible и применяете его. 1-1 :)
Tanner
Нет, это не шутка, я просто недостаточно развил свою мысль. Она действительно не для всех очевидна, так что исправляюсь.
В определённый исторический период сложился такой консенсус, что лучше иметь универсальный механизм изоляции для всего, то есть контейнеризацию через Docker. Предполагалось, что девопс-инженерам не придётся изучать тонкости языков и платформ, они просто будут воспроизводить одинаковую среду на серверах и машинах разработчиков, и все будут счастливы.
Но сегодня стало очевидно, что этот универсальный механизм себя не оправдал, не реализовал обещаний. Для разработчиков затраты на поддержание той же системы контейнеров, что и на серверах, оказались неподъёмны: сложно, требует изучения и поддержки, нужно мощное железо, намного усложнились задачи, которые раньше были простыми, типа отладки. Девопсы так и не смогли обойтись без изучения целевых платформ, просто вместо прямого подхода (прочитать немного про CPython, pip и virtualenv) им приходится начинать погружение в чудесный мир Python с отлаживания необъяснимых багов (почему cryptography не собирается в контейнере с Alpine).
В результате поддержка простых и понятных деплой-скриптов на Fabric оказывается намного практичней, чем монструозная система изоляции всего и вся. Особенно если основа продакшена ? Python, который уже имел специфические, весьма развитые и взрослые средства изоляции с отличным уровнем переносимости, когда докера ещё не было в проекте.
lampslave
Контейнеры — ладно, но хоть virtualenv-то надо, там же не один проект будет крутиться наверняка.
Tanner
Вы правы, virtualenv и pyenv ? обязательно. Даже если проект будет один, зависимости надо фиксировать.