HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-26
1 <p>В этом уроке мы научимся запускать готовое приложение в Docker и разберем все элементы, необходимые для его работы и обслуживания. Так мы сможем сложить цельную картину использования Docker, которую затем разберем по частям, изучая образы, файловую систему, сеть и другие составляющие.</p>
1 <p>В этом уроке мы научимся запускать готовое приложение в Docker и разберем все элементы, необходимые для его работы и обслуживания. Так мы сможем сложить цельную картину использования Docker, которую затем разберем по частям, изучая образы, файловую систему, сеть и другие составляющие.</p>
2 <p>Для примера возьмем Node.js приложение, созданное с помощью веб-фреймворка Fastify. Если вы не знакомы с Fastify или Node.js, то ничего страшного, потому что в уроке мы работаем только с Docker. Все то же самое можно сделать с практически любым другим приложением.</p>
2 <p>Для примера возьмем Node.js приложение, созданное с помощью веб-фреймворка Fastify. Если вы не знакомы с Fastify или Node.js, то ничего страшного, потому что в уроке мы работаем только с Docker. Все то же самое можно сделать с практически любым другим приложением.</p>
3 <p>После запуска это приложение отдает по HTTP страницу с приветственным текстом. Это приложение создано специально для курса. Оно уже упаковано в Docker и доступно для запуска под именем<em>hexletcomponents/devops-example-app</em>. Исходный код доступен<a>здесь</a>.</p>
3 <p>После запуска это приложение отдает по HTTP страницу с приветственным текстом. Это приложение создано специально для курса. Оно уже упаковано в Docker и доступно для запуска под именем<em>hexletcomponents/devops-example-app</em>. Исходный код доступен<a>здесь</a>.</p>
4 <h2>Запуск</h2>
4 <h2>Запуск</h2>
5 <p>Команда запуска этого приложения выглядит так:</p>
5 <p>Команда запуска этого приложения выглядит так:</p>
6 <p>После запуска команды мы увидим следующий процесс:</p>
6 <p>После запуска команды мы увидим следующий процесс:</p>
7 <ol><li>Скачивание образа, в случае первого старта</li>
7 <ol><li>Скачивание образа, в случае первого старта</li>
8 <li>Запуск приложения командой, указанной внутри образа: fastify start server/plugin.js -a 0.0.0.0 -l info -P. Эта команда была указана при создании образа. Подробно мы разберем этот момент в соответствующем уроке.</li>
8 <li>Запуск приложения командой, указанной внутри образа: fastify start server/plugin.js -a 0.0.0.0 -l info -P. Эта команда была указана при создании образа. Подробно мы разберем этот момент в соответствующем уроке.</li>
9 <li>Вывод лога запущенного приложения 21:46:46 ✨ Server listening at http://0.0.0.0:3000</li>
9 <li>Вывод лога запущенного приложения 21:46:46 ✨ Server listening at http://0.0.0.0:3000</li>
10 </ol><p>Если все сделано правильно, открыв в браузере http://0.0.0.0:3000 или http://localhost:3000, вы получите такую страницу:</p>
10 </ol><p>Если все сделано правильно, открыв в браузере http://0.0.0.0:3000 или http://localhost:3000, вы получите такую страницу:</p>
11 <p>Теперь если снова посмотреть в терминал, то там в лог добавятся новые записи:</p>
11 <p>Теперь если снова посмотреть в терминал, то там в лог добавятся новые записи:</p>
12 <p>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</p>
12 <p>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</p>
13 <h2>Логи</h2>
13 <h2>Логи</h2>
14 <p>Независимо от того, с каким приложением мы работаем, Docker требует от его создателей определенного подхода в логировании. Логи не должны сохраняться в файлы, их нужно выводить в STDOUT. Благодаря этому мы видим их в терминале после запуска приложения. Откуда берется такое требование?</p>
14 <p>Независимо от того, с каким приложением мы работаем, Docker требует от его создателей определенного подхода в логировании. Логи не должны сохраняться в файлы, их нужно выводить в STDOUT. Благодаря этому мы видим их в терминале после запуска приложения. Откуда берется такое требование?</p>
15 <p>В 12 факторах есть<a>пункт посвященный логированию</a>. Он говорит о том, что масштабируемое приложение не должно самостоятельно заниматься хранением и обработкой логов. Вместо этого, каждый процесс должен отправлять логи в STDOUT, что позволяет гибко и универсально управлять процессом сбора логов. К тому же это удобно для разработчиков и администраторов, так как им не нужно разбираться с самим приложением, чтобы понимать как посмотреть его логи.</p>
15 <p>В 12 факторах есть<a>пункт посвященный логированию</a>. Он говорит о том, что масштабируемое приложение не должно самостоятельно заниматься хранением и обработкой логов. Вместо этого, каждый процесс должен отправлять логи в STDOUT, что позволяет гибко и универсально управлять процессом сбора логов. К тому же это удобно для разработчиков и администраторов, так как им не нужно разбираться с самим приложением, чтобы понимать как посмотреть его логи.</p>
16 <p>Так как в нашем случае приложение запускается через Docker, то Docker и является той системой, которая управляет сбором логов. Если посмотреть его<a>документацию</a>, то можно увидеть, что Docker поддерживает десяток мест, куда он может отправлять логи самостоятельно. Кроме этого есть возможность подключать к нему внешние плагины, для поддержки любых других систем логирования. Среди поддерживаемых из коробки:<em>syslog</em>,<em>journald</em>,<em>awslogs</em>,<em>fluentd</em>,<em>gcplogs</em>и другие.</p>
16 <p>Так как в нашем случае приложение запускается через Docker, то Docker и является той системой, которая управляет сбором логов. Если посмотреть его<a>документацию</a>, то можно увидеть, что Docker поддерживает десяток мест, куда он может отправлять логи самостоятельно. Кроме этого есть возможность подключать к нему внешние плагины, для поддержки любых других систем логирования. Среди поддерживаемых из коробки:<em>syslog</em>,<em>journald</em>,<em>awslogs</em>,<em>fluentd</em>,<em>gcplogs</em>и другие.</p>
17 <h2>Автозапуск</h2>
17 <h2>Автозапуск</h2>
18 <p>Запущенное, описанным выше способом приложение, завершится при закрытии терминала. Поэтому подобный способ подходит только для разработки. В продакшене же, для запуска нужно демонизировать приложение. Для этого используется флаг -d:</p>
18 <p>Запущенное, описанным выше способом приложение, завершится при закрытии терминала. Поэтому подобный способ подходит только для разработки. В продакшене же, для запуска нужно демонизировать приложение. Для этого используется флаг -d:</p>
19 <p>При таком запуске приложение оказывается в фоне. Оно останется открытым даже если мы закроем терминал, но все же этого недостаточно для полноценного продакшена. Представьте если внутри приложения случится ошибка и оно остановится. Что произойдет в этом случае? По умолчанию Docker ничего не будет делать. Если контейнер остановился изнутри, то больше он не запустится. Это поведение можно изменить, так как Docker работает в режиме супервизора. Мы<a>можем указать</a>ему на необходимость перезапуска в случае ошибок:</p>
19 <p>При таком запуске приложение оказывается в фоне. Оно останется открытым даже если мы закроем терминал, но все же этого недостаточно для полноценного продакшена. Представьте если внутри приложения случится ошибка и оно остановится. Что произойдет в этом случае? По умолчанию Docker ничего не будет делать. Если контейнер остановился изнутри, то больше он не запустится. Это поведение можно изменить, так как Docker работает в режиме супервизора. Мы<a>можем указать</a>ему на необходимость перезапуска в случае ошибок:</p>
20 <p>В случае указания on-failure контейнер перезапустится если внутри произошла ошибка. В большинстве случаев это и есть желаемое поведение, но иногда нужно перезапускать контейнер в любом случае, для этого используется вариант always. Такой контейнер перезапустится даже если его попытаться остановить командой docker stop. Если же нужно исключить этот вариант, то подойдет unless-stopped.</p>
20 <p>В случае указания on-failure контейнер перезапустится если внутри произошла ошибка. В большинстве случаев это и есть желаемое поведение, но иногда нужно перезапускать контейнер в любом случае, для этого используется вариант always. Такой контейнер перезапустится даже если его попытаться остановить командой docker stop. Если же нужно исключить этот вариант, то подойдет unless-stopped.</p>
21 <p>В реальной жизни перезапуск контейнеров, почти всегда, выполняется внешними, по отношению к Docker, средствами. Либо это Kubernetes с его настройками, либо Systemd.</p>
21 <p>В реальной жизни перезапуск контейнеров, почти всегда, выполняется внешними, по отношению к Docker, средствами. Либо это Kubernetes с его настройками, либо Systemd.</p>
22 <h2>Проброс портов</h2>
22 <h2>Проброс портов</h2>
23 <p>Наше приложение стартует веб-сервер, который слушает определенный порт на каком-то ip-адресе. В большинстве веб-серверов это 127.0.0.1</p>
23 <p>Наше приложение стартует веб-сервер, который слушает определенный порт на каком-то ip-адресе. В большинстве веб-серверов это 127.0.0.1</p>
24 <p>, то есть localhost. Без использования Docker, такой запуск позволяет работать с веб-сервером локально, например, открывая страницы в браузере. С Docker же, подобный запуск не сработает как мы ожидаем.</p>
24 <p>, то есть localhost. Без использования Docker, такой запуск позволяет работать с веб-сервером локально, например, открывая страницы в браузере. С Docker же, подобный запуск не сработает как мы ожидаем.</p>
25 <p>Сеть внутри Docker контейнера изолированная. Localhost внутри контейнера и снаружи это разные вещи. Поэтому для выхода наружу Docker использует механизм проброса портов, который состоит из двух частей:</p>
25 <p>Сеть внутри Docker контейнера изолированная. Localhost внутри контейнера и снаружи это разные вещи. Поэтому для выхода наружу Docker использует механизм проброса портов, который состоит из двух частей:</p>
26 <p>Во-первых, нужно сделать так, чтобы сервер внутри контейнера стартовал по адресу<em>0.0.0.0</em>. В нашем приложении это достигается явным указанием в строке запуска:</p>
26 <p>Во-первых, нужно сделать так, чтобы сервер внутри контейнера стартовал по адресу<em>0.0.0.0</em>. В нашем приложении это достигается явным указанием в строке запуска:</p>
27 <p>Запущенное таким образом приложение все еще недоступно снаружи, так как Docker требует явного указания того, какой порт мы хотим пробросить. По умолчанию, наше приложение стартует на порту 3000, поэтому его и нужно пробрасывать. Делается это с помощью флага -p.</p>
27 <p>Запущенное таким образом приложение все еще недоступно снаружи, так как Docker требует явного указания того, какой порт мы хотим пробросить. По умолчанию, наше приложение стартует на порту 3000, поэтому его и нужно пробрасывать. Делается это с помощью флага -p.</p>
28 <p>Формат задается двумя числами, где справа - это порт внутри контейнера, который мы хотим выставить наружу, а слева порт, через который мы сможем попасть во внутрь. В нашем примере они совпадают, но это не обязательно, внешний порт может быть любым свободным.</p>
28 <p>Формат задается двумя числами, где справа - это порт внутри контейнера, который мы хотим выставить наружу, а слева порт, через который мы сможем попасть во внутрь. В нашем примере они совпадают, но это не обязательно, внешний порт может быть любым свободным.</p>
29 <h2>Переменные окружения</h2>
29 <h2>Переменные окружения</h2>
30 <p>Приложения соответствующие<a>12 факторам</a>конфигурируются переменными окружениями. Сами переменные задаются либо на уровне системы, либо при запуске приложения. В случае с Docker первый способ не работает, так как контейнер изолирован от внешних переменных окружения. Работает только второй способ, но не так как это происходит обычно. Без Docker мы можем сделать так:</p>
30 <p>Приложения соответствующие<a>12 факторам</a>конфигурируются переменными окружениями. Сами переменные задаются либо на уровне системы, либо при запуске приложения. В случае с Docker первый способ не работает, так как контейнер изолирован от внешних переменных окружения. Работает только второй способ, но не так как это происходит обычно. Без Docker мы можем сделать так:</p>
31 <p>Docker такую переменную проигнорирует. Передача переменных окружения в контейнер работает только через явное указание:</p>
31 <p>Docker такую переменную проигнорирует. Передача переменных окружения в контейнер работает только через явное указание:</p>
32 <p>Передавать можно любое количество переменных:</p>
32 <p>Передавать можно любое количество переменных:</p>
33 <p>docker run -p 3000</p>
33 <p>docker run -p 3000</p>
34 <p>-e NAME=value -e SERVER_MESSAGE="Hexlet Awesome Server" hexletcomponents/devops-example-app</p>
34 <p>-e NAME=value -e SERVER_MESSAGE="Hexlet Awesome Server" hexletcomponents/devops-example-app</p>
35 <h2>Как приложение отображается на контейнеры?</h2>
35 <h2>Как приложение отображается на контейнеры?</h2>
36 <ol><li>Все приложение - один контейнер, внутри которого поднимается дерево процессов: приложение, веб-сервер, база данных и все в этом духе</li>
36 <ol><li>Все приложение - один контейнер, внутри которого поднимается дерево процессов: приложение, веб-сервер, база данных и все в этом духе</li>
37 <li>Каждый запущенный контейнер - атомарный сервис. Другими словами каждый контейнер представляет собой ровно одну программу, будь то веб-сервер или приложение</li>
37 <li>Каждый запущенный контейнер - атомарный сервис. Другими словами каждый контейнер представляет собой ровно одну программу, будь то веб-сервер или приложение</li>
38 </ol><p>На практике все преимущества Docker достигаются только со вторым подходом. Во-первых, сервисы, как правило, разнесены по разным машинам и нередко перемещаются по ним (например, в случае выхода из строя сервера), во-вторых, обновление одного сервиса не должно приводить к остановке остальных.</p>
38 </ol><p>На практике все преимущества Docker достигаются только со вторым подходом. Во-первых, сервисы, как правило, разнесены по разным машинам и нередко перемещаются по ним (например, в случае выхода из строя сервера), во-вторых, обновление одного сервиса не должно приводить к остановке остальных.</p>
39 <p>Первый подход крайне редко, но бывает нужен. Например, Хекслет работает в двух режимах. Сам сайт с его сервисами использует вторую модель, когда каждый сервис отдельно, но вот практика, выполняемая в браузере, стартует по принципу "один пользователь - один контейнер". Внутри контейнера может оказаться все что угодно в зависимости от практики. Как минимум, там всегда стартует сама среда Хекслет IDE, а она в свою очередь порождает терминалы (процессы). В курсе по базам данных в этом же контейнере стартует и база данных, в курсе, связанном с вебом, стартует веб-сервер. Такой подход позволяет создать иллюзию работы на настоящей машине и резко снижает сложность в поддержке упражнений. Повторюсь, что такой вариант использования очень специфичен и вам вряд ли понадобится.</p>
39 <p>Первый подход крайне редко, но бывает нужен. Например, Хекслет работает в двух режимах. Сам сайт с его сервисами использует вторую модель, когда каждый сервис отдельно, но вот практика, выполняемая в браузере, стартует по принципу "один пользователь - один контейнер". Внутри контейнера может оказаться все что угодно в зависимости от практики. Как минимум, там всегда стартует сама среда Хекслет IDE, а она в свою очередь порождает терминалы (процессы). В курсе по базам данных в этом же контейнере стартует и база данных, в курсе, связанном с вебом, стартует веб-сервер. Такой подход позволяет создать иллюзию работы на настоящей машине и резко снижает сложность в поддержке упражнений. Повторюсь, что такой вариант использования очень специфичен и вам вряд ли понадобится.</p>
40 <p>Другой важный аспект при работе с контейнерами касается состояния. Например, если база данных запускается в контейнере, то ее данные не должны храниться там же, внутри. Контейнер - это процесс операционной системы, то есть его наличие всегда временно, его довольно легко уничтожить. Docker содержит механизмы для хранения и использования данных, лежащих в основной файловой системе. О них поговорим в уроках дальше по курсу.</p>
40 <p>Другой важный аспект при работе с контейнерами касается состояния. Например, если база данных запускается в контейнере, то ее данные не должны храниться там же, внутри. Контейнер - это процесс операционной системы, то есть его наличие всегда временно, его довольно легко уничтожить. Docker содержит механизмы для хранения и использования данных, лежащих в основной файловой системе. О них поговорим в уроках дальше по курсу.</p>