Запуск в продакшн с помощью Docker
С тех пор, как я перешёл на разработку в Docker, моя программистская жизнь не будет прежней. Настолько это оказалось удобно, что словами не передать!
Любые версии PHP, любые версии Node, локальная отладка отправленных писем и т.д. Подключить можно какие угодно службы, для всяких разных нужд.
Естественно, мне захотелось перенести этот опыт и в боевой режим на сервере, чтобы мои контейнеры так же замечательно работали в продакшене.
Как оказалось, сделать это совсем несложно. Нужен только VPS, куда вы сможете установить Docker.
Конфигурация
Думаю, с установкой Docker на сервер вы справитесь и без меня, мануалов на эту тему очень много.
Возможно вы не обращали внимание, но в в репозитории VespShop уже лежат 2 конфига:
- docker/docker-compose.yml - для разработки
- docker/docker-production.yml - для продакшена
Вы можете просто сравнить эти 2 файла и заметить небольшие различия:
- продакшн контейнеры должны перезапускаться при ошибках
- контенеры PHP используют разные конфиги и команды для запуска
- в боевом режиме нет контейнера Mailhog для отлова почты
- порты Node недоступны снаружи
Внутри конфигов PHP еще меньше различий - только установка XDebug.
Дальше проверяем работу новой конфигурации, указывая её при запуске:
docker compose -f ./docker-production.yml up -d --build
Самое классное, что можно всё отладить локально в боевом режиме, перед выгрузкой на хостинг.
Сервис systemd
После того, как вы выгрузили свой проект на сервер, нужно как-то системно с ним работать: запускать, останавливать, перезапускать. Не писать же команды руками каждый раз, тем более, если сервер ночью перезагрузят для технических работ.
В качестве хостовой машины я использую свежую Ubuntu Server LTS, со встроенным systemd - это такой системный демон, который как раз отвечает за работу с разными сервисами, включая пользовательские.
Так что создаём ему новый конфиг в файле /etc/systemd/system/vesp-shop.service:
[Unit]
Description=VespShop
After=docker.service
Requires=docker.service
[Service]
WorkingDirectory=/home/user/vesp-shop/
ExecStart=docker compose -f /home/user/vesp-shop/docker/docker-production.yml up --build
ExecStartPost=service nginx reload
ExecStop=docker compose -f /home/user/vesp-shop/docker/docker-production.yml stop
Restart=on-failure
[Install]
WantedBy=multi-user.target
Обратите внимание, здесь нет флага -d, который означает запуск в фоне (т. е. detach) - потому что systemd должен следить за запущенным сервисом, читать его состояние и перезапускать при необходимости.
Дальше нужно активировать наш сервис и перезапустить демон.
systemctl enable vesp-shop
systemctl daemon-reload
Это создаст символическую ссылку /etc/systemd/system/multi-user.target.wants/vesp-shop.service и тогда systemd будет требовать запуск наших контейнеров сразу после загрузки сервера.
Теперь можно и запустить контейнеры:
systemctl start vesp-shop
Следить за ходом событий можно двумя вариантами
- через systemctl status vesp-shop
- или перейти в docker директорию проекта и там использовать docker compose logs -f --tail=10
Первый вариант показывает вам статус сервиса вместе с последними строчками логов. А второй вариант позволяет следить за логами в прямом эфире и с подстветкой
Глобальный Nginx
Несмотря на то, что в наших контейнерах уже есть Nginx, напрямую выводить наружу я его не хочу. Меня вполне устраивает его роль прокси-сервера для доступа в контейнеры Docker.
А для доступа снаружи я устанавливаю Nginx на хостовую машину и прописываю вот такой конфиг:
server {
listen 80;
listen 443 ssl http2;
server_name vesp-shop.bezumkin.ru;
ssl_certificate /etc/letsencrypt/live/vesp-shop.bezumkin.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vesp-shop.bezumkin.ru/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
include /etc/letsencrypt/options-ssl-nginx.conf;
if ($scheme = 'http') {
return 307 https://$host$request_uri;
}
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Scheme $scheme;
proxy_read_timeout 1m;
proxy_connect_timeout 1m;
proxy_pass http://127.0.0.1:10004;
}
}
С получением сертификатов разберётесь сами (а если нет - спросите в комментариях, расскажу про certbot), но вот откуда взялся локальный порт 10004?
А очень просто - я его прописал в настройках vesp-shop/docker/.env:
NGINX_PORT=10004
Не зря же у нас там есть эта настройка, правильно?
При локальной работе она равна 8080, но локально я запускаю только один проект за раз. А на боевом сервере их обычно больше.
Поэтому каждому проекту я прописываю свой локальный порт, с которым будет работать глобальный Nginx.
Все запросы снаружи на домен vesp-shop.bezumkin.ru полетят на порт 10004, где их получит контейнер Nginx в Docker и переправит в нужных контейнер.
Возможно, это небольшой оверхэд, но как это всё удобно и классно работает!
Серверный рендер
Сейчас у меня в продакшен режиме настроена генерация статических файлов как для админки, так и для сайта. Контейнер Node собирает эти файлы и завершает свою работу - дальше Nginx обращается к файлам напрямую.
Но мы разрабатывали публичный сайт для рендера на сервере, так что пора активировать этот режим в конфигах Docker.
Меняем команду на запуск контейнера Node таким образом:
command: sh -c 'yarn && yarn generate:admin && yarn build:site && yarn start:site'
То есть:
- Установка пакетов из npm
- Генерация статичной админки
- Сборка публичного сайта
- Запуск сервера Nuxt для публичного сайта
Из-за того, что последняя команда не завершается, контейнер не прекращает работу и systemd следит за ним. Нам даже не нужен PM2, потому что Docker вместе с systemd становятся менеджером задач для контейнера и перезапустят его в случае ошибки!
Теперь нужно только указать Nginx обращаться не в статическую директорию, а на порт 4100 - как прописано в команде start:site нашего package.json.
location / {
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_read_timeout 240s;
proxy_pass http://127.0.0.1:4100;
}
И всё!
В зависимости от запрашиваемого адреса Nginx отправит нас или в серверный рендер Nuxt, или покажет статические файлы админки.
Остаётся только перезапустить контейнеры.
Права пользователя
В своих проектах я использую контнейнер php-fpm, собранный на образе Alpine. Его основной процесс запускается от юзера www-data, а не root.
Соответственно, все файлы, создаваемые процессом PHP получают владельца сuid = 82 - потому что именно такой uid у юзера www-data в Alpine.
А в хостовой машине Ubuntu такого юзера просто нет, и получаются разнообразные ошибки доступа, например при попытке записать временный файл в директорию /vesp/tmp, созданую не юзером c uid = 82.
Я столкнулся с этим сам, когда переносил bezumkin.ru в Docker на сервере. Просто перестала отправляться почта, потому что Fenom не мог сохранить скомпилированный шаблон.
Вариантов решения несколько:
- назначить широкие права на директорию с проектом - 0777
- использовать php-fpm на базе Debian - там у www-data должен быть тот же uid, что и в Ubuntu (я не проверял)
- создать юзера с uid 82, который будет работать с Docker.
Именно так я и поступил - поместил все проекты в директорию своему юзеру, а потом поменял ему id в системных файлах:
- /etc/passwd
- /etc/group
- /etc/shadow
Ну и обновил владельца всех файлов и директорию юзера. Теперь user id создаваемых файлов на хосте и в PHP совпадают, проблем нет. Не знаю, насколько это костыльно, но работает уже давно.
Обновление проекта
Лично я для личных проектов давно использую Gitlab, благодаря удобной настройке CI/CD с помощью конфига .gitlab-ci.yml в проекте. Как-то раз вот разобрался, с тех пор и использую.
И возник вопрос, а как обновлять проекты, работающие в Docker? Раньше я их собирал на сервисе, выгружал на сервер и там перезапускал процессы.
Оказалось, что и тут всё стало проще - надо только выгрузить исходники и перезапустить сервис. Они же всё сами делают при запуске!
Вопрос только в невозможности ввода пароля для запуска сервиса при автоматической выгрузке. Так что прописываем такую возможность своему пользователю в /etc/sudoers:
user ALL=NOPASSWD: /bin/systemctl
Теперь он делает sudo systemctl restart vesp-shop без пароля.
Для примера мой .gitlab-ci.yml :
image: alpine:3
before_script:
- which ssh-agent || ( apk update -q && apk add openssh-client -q )
- which rsync || ( apk update -q && apk add rsync -q )
- mkdir -p ~/.ssh
- ssh-keyscan -t rsa $DEPLOYMENT_HOST > ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- ssh-add <(echo "$DEPLOYMENT_KEY")
stage_deploy:
only:
- master
script:
- ssh $DEPLOYMENT_USER@$DEPLOYMENT_HOST "mkdir -p $WWW_PATH"
- ssh $DEPLOYMENT_USER@$DEPLOYMENT_HOST "mkdir -p $FRONTEND_PATH"
- ssh $DEPLOYMENT_USER@$DEPLOYMENT_HOST "mkdir -p $CORE_PATH"
- rsync -r --del --force www/* $DEPLOYMENT_USER@$DEPLOYMENT_HOST:$WWW_PATH
- rsync -r --del --force frontend/* $DEPLOYMENT_USER@$DEPLOYMENT_HOST:$FRONTEND_PATH
- rsync -r --del --force core/* $DEPLOYMENT_USER@$DEPLOYMENT_HOST:$CORE_PATH
- rsync -r --del --force composer.* $DEPLOYMENT_USER@$DEPLOYMENT_HOST:$BASE_PATH
- ssh $DEPLOYMENT_USER@$DEPLOYMENT_HOST "sudo systemctl restart webstartpage"
Даже не надо переделывать для других проектов - все отличия будут в конфигах Docker.
Заключение
На этом я рассказал и показал всё, что хотел.
Мы очень сильно прокачали VespShop, перенесли в него данные из miniShop2 и запустили проект в продакшн.
Думаю, на основе получившегося проекта и описания, как это всё и почему работает, вы сможете добавить любой нужный функционал. И доставку, и скидки, и чёрта в ступе - было бы желание.
Самое главное, что вы ни от кого не зависите, ничего не ждёте, и ничем не ограничены - вы всё пишете своими руками и головой. Такое положение сильно отличается от работы с дополнением для старой CMS.
Надеюсь, вам было интересно. Итоговый коммит, как обычно, в репозитории.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
1 138
14.09.2023, 12:31:14
20 комментариев
NightRider
14.09.2023, 23:56:23
Курс супер! Все темы, которые писал в пожеланиях, были рассмотрены. И даже более того. Спасибо за такой фундаментальный труд!
15.09.2023, 02:59:32
Очень рад, что тебе понравилось!
NightRider
01.10.2023, 15:06:41
Почему может выдавать такую ошибку? В какую сторону копать?
01.10.2023, 16:37:49
Из этого скрина непонятно, надо логи докера смотреть.
Для этого зайди в docker директорию проекта и там набери:
Это выведет записи по всем контенерам и в них уже можно найти ошибки.
NightRider
01.10.2023, 19:45:39
От этой ошибки удалось избавиться, docker-compose старый был. Обновил версию, и она ушла.
Появилась новая:
возникла после того, как обновил конфигурацию для nginx в файле default.conf.template
02.10.2023, 01:52:00
Module not found - наверное, чего-то не хватает?
Скорее всего, с Nginx это никак не связано, попробуй удалить node_modules и запустить контейнер - он должен установить всё заново при запуске.
NightRider
02.10.2023, 10:51:57
Заработало, спасибо. Но появилась ошибка
Connection refused while connecting to upstream.
upstream: "http://127.0.0.1:4100/"
NightRider
02.10.2023, 11:10:25
Всё ок, моя ошибка) Просто нужно было подождать, пока контейнеры запустятся) Ещё раз спасибо
02.10.2023, 11:22:17
На здоровье!
22.12.2023, 10:12:47
А эта проверка выполняется еще на локалке или уже на рабочем сервере? Я использую обычный VESP (не vesp-shop из этого курса). На локалке слепил тестовый сайт, засунул туда docker-production.yml из репозитория vesp-shop. И запустил:
. В итоге выдается ошибка -
Как понял система ругается на то, что на локалке не существует серверной директории /var/lib/docker/tmp/ .
22.12.2023, 11:41:19
Мне кажется, ты забыл создать файл prod.dockerfile внутри docker/php. Проверь, пожалуйста.
22.12.2023, 15:12:31
Не то что забыл) Не знал про это. Скопировал prod.dockerfile с репозитория vesp-shop и запустил конфигурацию production. Теперь запустилось, но работает косячно:
- На фронте нет вывода моих страниц (картинка + заголовок);
- А в админку не пускает с ошибкой "Неправильное имя или пароль". Без разницы как авторизоваться: admin/admin или user/user .
22.12.2023, 20:01:02
Не знаю, что сказать.
Как обычно, наверное, ты где-то поторопился.
23.12.2023, 09:47:43
Да тут и ошибиться вроде негде. Сайт и админка работают в Докере, если запускать не в конфигурации production. Далее я скопировал 2 файла с репозитория vesp-shop и положил себе: docker/docker-production.yml и docker/php-fpm/prod.dockerfile . И далее запускаю docker-compose -f ./docker-production.yml up -d --build . Ранее я спрашивал у тебя, можно ли обычный VESP (не vesp-shop) запустить на продакшене. Ты сказал что можно закинуть туда docker-production.yml и должно работать. Про prod.dockerfile я и не знал ничего. Закинул его тоже - выдает ошибку, требует composer.lock. Скопировал его также с vesp-shop и добавил - работает косячно. Может просто чистый VESP еще чем-то отличается от vesp-shop и поэтому на нем не запускается конфигурации production?
23.12.2023, 19:57:24
Снес свои поделки с локального Докера и поставил опять чистый VESP. Сразу закинул туда docker-production.yml и prod.dockerfile . Потом запустил, засеял базу и попробовал запустить конфигурацию production. Там все довольно долго грузилось, потом запустилось. Но в админку попасть не смог. И контейнер vesp-php-fpm-1 постоянно перегружается. Посмотрел логи контейнера vesp-php-fpm-1 - он ругается на отсутствие файла composer.lock .
Тут два варианта, как понял теперь у меня:
- Разобраться как генерируется composer.lock, создать его и, возможно, после этого все наладится.
- Или я просто "побежал впереди паровоза" и чистый VESP пока не доработан под "Докер в продакшене". Тогда буду ждать "официального релиза".) Мне просто сама идея "из Докера в Докер" понравилась, но vesp-shop сложно для меня. Вот я и уперся в чистый VESP с docker-production.yml.
24.12.2023, 08:50:02
То есть, ты просто бездумно копируешь конфиги другого проекте себе и удивляешься, что оно не работает?
Так нельзя.
Если в твоём dev конфиге не монтируется файл composer.lock, то не надо его монтировать и в конфиге для продакшена.
Я для того и написал эту заметку, чтобы объяснить принципы запуска в продакшн. Тебе нужно взять свой dev конфиг и убрать всё ненужное.
Не копировать мой конфиг из другого проекта, а сделать свой - основывая на информации из этой заметки.
24.12.2023, 11:09:56
Спасибо большое за помощь! Буду изучать вопрос. Я вообще-то хотел закинуть сайт на сервер в Докер, чисто "в научных целях" и на этом пока успокоиться, подучить базу. Но, оказалось, что для это уже нужно немало навыков иметь.))
А можно взять c github твой готовый проект vesp-shop и запустить его в локальном Докере? Т.е. не разрабатывать с нуля. Будет это работать? И на нем посмотрю как там production работает. Там главное, видимо, не запутаться с миграциями после установки и базу MODX импортировать.
И еще вопрос - но ты же потом в итоге сделаешь обычный установочный VESP с конфигурацией production?
24.12.2023, 12:13:13
Да, конечно.
Наверняка, но сроков назвать не могу.
24.12.2023, 12:33:56
С Наступающим тебя и твоих ближних! Отдохни хорошенько от трудов своих))
24.12.2023, 14:26:13
Спасибо!
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо!
Извини, тупанул.
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!
Запуск в продакшн с помощью Docker
20