- Запуск
- Логи
- Автозапуск
- Проброс портов
- Переменные окружения
- Как приложение отображается на контейнеры?
В этом уроке мы научимся запускать готовое приложение в Docker и разберем все элементы, необходимые для его работы и обслуживания. Так мы сможем сложить цельную картину использования Docker, которую затем разберем по частям, изучая образы, файловую систему, сеть и другие составляющие.
Для примера возьмем Node.js приложение, созданное с помощью веб-фреймворка Fastify. Если вы не знакомы с Fastify или Node.js, то ничего страшного, потому что в уроке мы работаем только с Docker. Все то же самое можно сделать с практически любым другим приложением.
После запуска это приложение отдает по HTTP страницу с приветственным текстом. Это приложение создано специально для курса. Оно уже упаковано в Docker и доступно для запуска под именем hexletcomponents/devops-example-app. Исходный код доступен здесь.
Запуск
Команда запуска этого приложения выглядит так:
# -p - проброс портов
# -e - переменная окружения
docker run -p 3000:3000 \
-e SERVER_MESSAGE="Hexlet Awesome Server" \
hexletcomponents/devops-example-app
Unable to find image 'hexletcomponents/devops-example-app:latest' locally
9cc8d6197555: Download complete
45796f425418: Download complete
cd666498bb13: Download complete
57b89cfa1177: Download complete
339fa49b454d: Download complete
1d1b4cabe4ab: Download complete
de82c3f2de88: Download complete
42c077c10790: Download complete
fd2a83b16756: Download complete
f691a4cef6f1: Download complete
633d34d77448: Download complete
f428e85c5188: Download complete
> devops-example-app@1.0.0 start
> fastify start server/plugin.js -a 0.0.0.0 -l info -P
21:46:46 ✨ Server listening at http://0.0.0.0:3000
После запуска команды мы увидим следующий процесс:
- Скачивание образа, в случае первого старта
- Запуск приложения командой, указанной внутри образа:
fastify start server/plugin.js -a 0.0.0.0 -l info -P
. Эта команда была указана при создании образа. Подробно мы разберем этот момент в соответствующем уроке. - Вывод лога запущенного приложения
21:46:46 ✨ Server listening at http://0.0.0.0:3000
Если все сделано правильно, открыв в браузере http://0.0.0.0:3000
или http://localhost:3000
, вы получите такую страницу:
Теперь если снова посмотреть в терминал, то там в лог добавятся новые записи:
21:48:29 ✨ incoming request GET xxx /
21:48:30 ✨ request completed 358ms
21:48:30 ✨ incoming request GET xxx /assets/css/bootstrap.min.css
21:48:30 ✨ incoming request GET xxx /images/app.png
21:48:30 ✨ request completed 40ms
21:48:30 ✨ request completed 63ms
21:48:30 ✨ incoming request GET xxx /favicon.ico
21:48:30 ✨ Route GET:/favicon.ico not found
21:48:30 ✨ request completed 5ms
Логи
Независимо от того, с каким приложением мы работаем, Docker требует от его создателей определенного подхода в логировании. Логи не должны сохраняться в файлы, их нужно выводить в STDOUT. Благодаря этому мы видим их в терминале после запуска приложения. Откуда берется такое требование?
В 12 факторах есть пункт посвященный логированию. Он говорит о том, что масштабируемое приложение не должно самостоятельно заниматься хранением и обработкой логов. Вместо этого, каждый процесс должен отправлять логи в STDOUT, что позволяет гибко и универсально управлять процессом сбора логов. К тому же это удобно для разработчиков и администраторов, так как им не нужно разбираться с самим приложением, чтобы понимать как посмотреть его логи.
Так как в нашем случае приложение запускается через Docker, то Docker и является той системой, которая управляет сбором логов. Если посмотреть его документацию, то можно увидеть, что Docker поддерживает десяток мест, куда он может отправлять логи самостоятельно. Кроме этого есть возможность подключать к нему внешние плагины, для поддержки любых других систем логирования. Среди поддерживаемых из коробки: syslog, journald, awslogs, fluentd, gcplogs и другие.
Автозапуск
Запущенное, описанным выше способом приложение, завершится при закрытии терминала. Поэтому подобный способ подходит только для разработки. В продакшене же, для запуска нужно демонизировать приложение. Для этого используется флаг -d
:
docker run -d -p 3000:3000 \
-e SERVER_MESSAGE="Hexlet Awesome Server" \
hexletcomponents/devops-example-app
При таком запуске приложение оказывается в фоне. Оно останется открытым даже если мы закроем терминал, но все же этого недостаточно для полноценного продакшена. Представьте если внутри приложения случится ошибка и оно остановится. Что произойдет в этом случае? По умолчанию Docker ничего не будет делать. Если контейнер остановился изнутри, то больше он не запустится. Это поведение можно изменить, так как Docker работает в режиме супервизора. Мы можем указать ему на необходимость перезапуска в случае ошибок:
docker run -d -p 3000:3000 --restart on-failure \
hexletcomponents/devops-example-app
В случае указания on-failure
контейнер перезапустится если внутри произошла ошибка. В большинстве случаев это и есть желаемое поведение, но иногда нужно перезапускать контейнер в любом случае, для этого используется вариант always
. Такой контейнер перезапустится даже если его попытаться остановить командой docker stop
. Если же нужно исключить этот вариант, то подойдет unless-stopped
.
В реальной жизни перезапуск контейнеров, почти всегда, выполняется внешними, по отношению к Docker, средствами. Либо это Kubernetes с его настройками, либо Systemd.
Проброс портов
Наше приложение стартует веб-сервер, который слушает определенный порт на каком-то ip-адресе. В большинстве веб-серверов это 127.0.0.1:80, то есть localhost. Без использования Docker, такой запуск позволяет работать с веб-сервером локально, например, открывая страницы в браузере. С Docker же, подобный запуск не сработает как мы ожидаем.
Сеть внутри Docker контейнера изолированная. Localhost внутри контейнера и снаружи это разные вещи. Поэтому для выхода наружу Docker использует механизм проброса портов, который состоит из двух частей:
Во-первых, нужно сделать так, чтобы сервер внутри контейнера стартовал по адресу 0.0.0.0. В нашем приложении это достигается явным указанием в строке запуска:
# По умолчанию используется порт 3000
fastify start server/plugin.js -a 0.0.0.0 -l info -P
Запущенное таким образом приложение все еще недоступно снаружи, так как Docker требует явного указания того, какой порт мы хотим пробросить. По умолчанию, наше приложение стартует на порту 3000, поэтому его и нужно пробрасывать. Делается это с помощью флага -p
.
docker run -p 3000:3000 hexletcomponents/devops-example-app
# -p <external port>:<internal port>
Формат задается двумя числами, где справа – это порт внутри контейнера, который мы хотим выставить наружу, а слева порт, через который мы сможем попасть во внутрь. В нашем примере они совпадают, но это не обязательно, внешний порт может быть любым свободным.
Переменные окружения
Приложения соответствующие 12 факторам конфигурируются переменными окружениями. Сами переменные задаются либо на уровне системы, либо при запуске приложения. В случае с Docker первый способ не работает, так как контейнер изолирован от внешних переменных окружения. Работает только второй способ, но не так как это происходит обычно. Без Docker мы можем сделать так:
NAME=value <команда запуска>
Docker такую переменную проигнорирует. Передача переменных окружения в контейнер работает только через явное указание:
docker run -p 3000:3000 \
-e SERVER_MESSAGE="Hexlet Awesome Server" \
hexletcomponents/devops-example-app
Передавать можно любое количество переменных:
docker run -p 3000:3000 -e NAME=value -e SERVER_MESSAGE="Hexlet Awesome Server" hexletcomponents/devops-example-app
Как приложение отображается на контейнеры?
- Все приложение — один контейнер, внутри которого поднимается дерево процессов: приложение, веб-сервер, база данных и все в этом духе
- Каждый запущенный контейнер — атомарный сервис. Другими словами каждый контейнер представляет собой ровно одну программу, будь то веб-сервер или приложение
На практике все преимущества Docker достигаются только со вторым подходом. Во-первых, сервисы, как правило, разнесены по разным машинам и нередко перемещаются по ним (например, в случае выхода из строя сервера), во-вторых, обновление одного сервиса не должно приводить к остановке остальных.
Первый подход крайне редко, но бывает нужен. Например, Хекслет работает в двух режимах. Сам сайт с его сервисами использует вторую модель, когда каждый сервис отдельно, но вот практика, выполняемая в браузере, стартует по принципу «один пользователь — один контейнер». Внутри контейнера может оказаться все что угодно в зависимости от практики. Как минимум, там всегда стартует сама среда Хекслет IDE, а она в свою очередь порождает терминалы (процессы). В курсе по базам данных в этом же контейнере стартует и база данных, в курсе, связанном с вебом, стартует веб-сервер. Такой подход позволяет создать иллюзию работы на настоящей машине и резко снижает сложность в поддержке упражнений. Повторюсь, что такой вариант использования очень специфичен и вам вряд ли понадобится.
Другой важный аспект при работе с контейнерами касается состояния. Например, если база данных запускается в контейнере, то ее данные не должны храниться там же, внутри. Контейнер — это процесс операционной системы, то есть его наличие всегда временно, его довольно легко уничтожить. Docker содержит механизмы для хранения и использования данных, лежащих в основной файловой системе. О них поговорим в уроках дальше по курсу.
Самостоятельная работа
- Выполните все шаги из урока
Запустите внутри контейнера веб-сервер Httpd:
docker run -it -p 80:80 httpd
Проверьте, что по адресу http://localhost отображается текст It works!
Остановите контейнер
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.