Тема 05

Управление контейнерами в среде выполнения

Управление контейнерами в среде выполнения

Предыдущие темы курса были посвящены созданию контейнерного образа как воспроизводимого артефакта поставки. Однако образ сам по себе не является работающим приложением. Он представляет собой инертное описание файловой системы и параметров запуска, которое приобретает практический смысл лишь тогда, когда на его основе создаётся и запускается контейнер. Именно на этом этапе начинается управление контейнером в среде выполнения: инженер должен определить, как приложение будет запущено, каким образом оно взаимодействует с внешним окружением, как контролировать его поведение и как корректно обновлять.

Управление контейнерами на этапе выполнения принципиально отличается от управления образами на этапе сборки. Если сборка отвечает на вопрос «что содержит артефакт», то среда выполнения отвечает на вопрос «как именно этот артефакт функционирует в конкретных условиях». Один и тот же образ может быть запущен с различными переменными окружения, ограничениями ресурсов, сетевыми настройками и политиками перезапуска. Поэтому корректная эксплуатация контейнера требует не только умения собрать образ, но и понимания того, какие параметры определяют поведение контейнера после его создания.

В рамках данной темы рассматриваются четыре взаимосвязанных аспекта. Во-первых, параметры запуска контейнера и их влияние на поведение приложения. Во-вторых, роль основного процесса и механизм завершения контейнера. В-третьих, средства инспекции и диагностики. В-четвёртых, принцип неизменяемой инфраструктуры и модель обновления контейнерных приложений. Вместе эти аспекты формируют представление о контейнере не как о «чёрном ящике», а как об управляемом объекте с определённым жизненным циклом.

Параметры запуска контейнера

Запуск контейнера — это не просто создание экземпляра образа. Это конфигурирование среды выполнения, в которой приложение будет работать. Параметры запуска определяют, какие ресурсы доступны контейнеру, как он взаимодействует с сетью, какие данные получает из внешнего окружения и каким образом среда выполнения реагирует на его остановку или аварийное завершение. Понимание этих параметров необходимо для перехода от разового экспериментального запуска к воспроизводимому и контролируемому развёртыванию.

Переменные окружения и аргументы команды

Одним из основных способов передачи конфигурации контейнеру являются переменные окружения (environment variables). Они задаются при запуске и доступны процессу внутри контейнера точно так же, как переменные окружения в обычной операционной системе. Переменные окружения удобны тем, что позволяют отделить конфигурацию от образа: один и тот же артефакт может быть запущен с разными параметрами подключения к базе данных, адресами внешних сервисов или режимами работы приложения.

Вместе с тем переменные окружения имеют ограничения. Они не предназначены для передачи сложных структурированных данных и не обеспечивают конфиденциальности значений без дополнительных мер. Если приложение требует передачи секретов, таких как пароли или ключи доступа, использование переменных окружения в явном виде создаёт риск: значения могут быть видны при инспекции контейнера или в журналах среды выполнения. Этот аспект становится особенно важным при переходе к эксплуатационным средам, где управление секретами требует отдельных механизмов.

Помимо переменных окружения, при запуске контейнера можно передавать аргументы командной строки, которые переопределяют команду, заданную в образе инструкцией CMD. Это позволяет, например, запустить контейнер с тем же образом, но в диагностическом режиме, с альтернативной конфигурацией или с выполнением вспомогательной задачи. Такое разделение между образом и параметрами запуска подчёркивает принципиальную идею: образ описывает «что может быть запущено», а параметры запуска определяют «как именно это запускается в данный момент».

Точка входа и команда по умолчанию

Для правильного понимания параметров запуска необходимо различать два механизма, определяющих поведение контейнера при старте: точку входа (entrypoint) и команду по умолчанию (cmd). Точка входа задаёт основной исполняемый файл или сценарий, который будет вызван при запуске контейнера. Команда по умолчанию определяет аргументы, передаваемые точке входа. Если точка входа не задана явно, среда выполнения использует стандартную оболочку операционной системы.

На практике разделение между entrypoint и cmd позволяет создавать образы, которые ведут себя как «параметризуемые команды». Например, образ утилиты может использовать в качестве точки входа саму утилиту, а аргументы командной строки, переданные при запуске контейнера, будут подставлены как параметры. Это удобно для инструментов, выполняющих конкретные задачи, но нежелательно для сервисных приложений, где точка входа и команда обычно фиксируются на этапе сборки образа.

Типичной ошибкой является смешение ролей entrypoint и cmd. Если обе инструкции используются одновременно, но их взаимодействие не продумано, результат может оказаться неожиданным. Например, если пользователь передаёт аргументы при запуске контейнера, они заменяют значение cmd, но не entrypoint. Непонимание этой логики приводит к трудно диагностируемым сбоям при запуске, особенно если поведение образа не документировано.

Проброс портов и сетевые параметры

Контейнер по умолчанию работает в изолированном сетевом пространстве. Приложение внутри контейнера может слушать сетевой порт, но это не означает, что порт доступен извне. Для предоставления доступа к сервису из внешней сети используется проброс портов (port mapping): при запуске контейнера указывается соответствие между портом хостовой машины и портом внутри контейнера.

Важно различать два уровня описания. Инструкция EXPOSE в Dockerfile лишь документирует намерение приложения использовать определённый порт. Она не выполняет реального проброса. Реальное сетевое подключение формируется только при запуске контейнера, когда указывается конкретное соответствие портов. Смешение этих двух уровней является распространённой ошибкой, которая приводит к тому, что разработчик ожидает сетевой доступности, не выполнив необходимого действия при запуске.

Сетевые параметры запуска не ограничиваются пробросом портов. Контейнер может быть подключён к пользовательской сети, в которой действуют собственные правила разрешения имён и маршрутизации. Эти вопросы подробно рассматриваются в последующей теме, посвящённой сетевому взаимодействию контейнерных приложений. Однако уже на данном этапе необходимо понимать, что сетевая конфигурация является частью параметров запуска, а не свойством образа.

Ограничение ресурсов

Среда выполнения контейнеров позволяет задавать ограничения на использование вычислительных ресурсов: процессорного времени и оперативной памяти. Эти ограничения реализуются через механизм групп управления ресурсами (control groups, cgroups), который был рассмотрен в теме, посвящённой механизмам изоляции.

Ограничение ресурсов выполняет несколько функций. Во-первых, оно защищает хостовую систему от ситуации, в которой один контейнер потребляет все доступные ресурсы и вызывает деградацию соседних процессов. Во-вторых, оно позволяет моделировать условия, приближённые к реальной эксплуатации, где приложение работает в рамках выделенного бюджета ресурсов. В-третьих, ограничения служат инструментом раннего обнаружения проблем: если приложение не способно работать в заданных рамках, это сигнализирует о необходимости оптимизации или пересмотра архитектуры.

Следует учитывать, что поведение контейнера при превышении лимита зависит от типа ресурса. Превышение лимита памяти обычно приводит к принудительному завершению контейнера операционной системой (механизм OOM Killer). Превышение лимита процессорного времени приводит к замедлению, но не к завершению. Это различие имеет практическое значение: приложение, не рассчитанное на ограниченный объём памяти, может аварийно завершаться без видимой ошибки в журналах приложения, поскольку завершение инициировано не самим процессом, а ядром операционной системы.

Политика перезапуска

При запуске контейнера можно задать политику перезапуска (restart policy), которая определяет поведение среды выполнения при завершении контейнера. Существуют несколько вариантов: контейнер не перезапускается; перезапускается всегда; перезапускается только в случае ошибки; перезапускается всегда, кроме явной остановки пользователем.

Политика перезапуска является простейшим механизмом обеспечения живучести сервиса на уровне одного узла. Она полезна для длительно работающих приложений, которые должны автоматически восстанавливаться после кратковременного сбоя. Однако этот механизм имеет существенные ограничения. Он не способен обнаружить ситуацию, когда приложение формально работает, но функционально неработоспособно. Он не позволяет распределить нагрузку между несколькими экземплярами. Он не обеспечивает управляемого обновления. Все эти задачи требуют более развитых средств, которые предоставляются оркестраторами и рассматриваются в последующих темах курса.

Параметры запуска контейнера и их связь с образом
Параметры запуска контейнера и их связь с образом

Связь параметров запуска с воспроизводимостью

Совокупность параметров запуска определяет не только текущее поведение контейнера, но и воспроизводимость развёртывания. Если параметры задаются вручную при каждом запуске, высока вероятность расхождения между средами разработки, тестирования и эксплуатации. Этот риск нарастает по мере увеличения числа параметров и числа контейнеров в составе приложения. Поэтому уже на данном этапе следует осознавать необходимость фиксации параметров запуска в формализованном описании. Средства такой фиксации, в первую очередь Docker Compose, рассматриваются в последующих темах. Однако важно понимать, что потребность в декларативном описании развёртывания возникает не из абстрактного стремления к порядку, а из конкретных проблем, связанных с ручным управлением параметрами запуска.

Основной процесс и завершение контейнера

Контейнер в среде Docker не является самостоятельной операционной системой и не содержит полноценного менеджера процессов. Он представляет собой оболочку для выполнения одного основного процесса, который определяет жизненный цикл контейнера. Понимание этого принципа является ключевым для корректной эксплуатации контейнерных приложений.

Процесс PID 1

При запуске контейнера среда выполнения создаёт в изолированном пространстве имён процесс, который получает идентификатор PID 1. Этот процесс является корневым для всего дерева процессов внутри контейнера. В обычной операционной системе PID 1 выполняет особую роль: он является процессом инициализации (init), который отвечает за порождение и контроль дочерних процессов, а также за корректную обработку сигналов.

В контейнерной среде процесс PID 1 обычно является самим приложением, а не специализированным менеджером инициализации. Это означает, что приложение принимает на себя обязанности, которые в традиционной системе выполнялись бы системным процессом init. В частности, оно должно корректно обрабатывать сигналы завершения и, при необходимости, управлять дочерними процессами.

Связь между PID 1 и жизненным циклом контейнера является прямой. Когда процесс с PID 1 завершается, контейнер прекращает существование, вне зависимости от того, продолжают ли работать какие-либо дочерние процессы. Код завершения процесса PID 1 становится кодом завершения контейнера. Следовательно, проектирование основного процесса контейнера — это не только задача разработки приложения, но и задача корректной интеграции с контейнерной средой.

Обработка сигналов и корректное завершение

Когда пользователь или среда выполнения инициирует остановку контейнера, Docker отправляет процессу PID 1 сигнал SIGTERM. Этот сигнал предлагает процессу завершиться корректно: закрыть сетевые соединения, сбросить буферы данных, освободить ресурсы. Если процесс не завершается в течение установленного времени ожидания (по умолчанию 10 секунд), Docker отправляет сигнал SIGKILL, который принудительно прекращает выполнение процесса без возможности корректного завершения.

Проблема состоит в том, что не все приложения корректно обрабатывают SIGTERM. Если приложение запущено через оболочку (shell), например через CMD ["sh", "-c", "python app.py"], то PID 1 получает оболочка, а не приложение. Оболочка, как правило, не передаёт полученный сигнал дочернему процессу. В результате приложение не получает уведомления о необходимости завершения и продолжает работать до истечения тайм-аута, после чего принудительно завершается сигналом SIGKILL. Такое поведение приводит к потере данных, незавершённым транзакциям и увеличению времени остановки контейнера.

Для избежания этой проблемы рекомендуется использовать exec-форму инструкций CMD и ENTRYPOINT в Dockerfile (например, CMD ["python", "app.py"] вместо CMD python app.py). В exec-форме приложение становится процессом PID 1 непосредственно и получает сигналы напрямую. Альтернативный подход — использование легковесного процесса инициализации (init process), который берёт на себя роль PID 1 и корректно передаёт сигналы основному приложению. Docker предоставляет встроенную поддержку такого механизма через параметр запуска --init.

Процесс PID 1 и обработка сигналов в контейнере
Процесс PID 1 и обработка сигналов в контейнере

Типичные причины немедленного завершения контейнера

Одна из наиболее частых проблем при начальном знакомстве с контейнерами — контейнер, который завершается сразу после запуска. Это поведение, как правило, объясняется одной из нескольких причин.

Во-первых, основной процесс может завершаться, потому что он предназначен для выполнения одноразовой задачи, а не для длительной работы. Например, если в качестве команды указана утилита, выводящая текст и завершающаяся, контейнер прекратит существование после её выполнения. Контейнер существует ровно столько, сколько работает его основной процесс.

Во-вторых, процесс может завершаться с ошибкой из-за отсутствия необходимых файлов, переменных окружения или зависимостей. В этом случае код завершения контейнера будет ненулевым, а журналы могут содержать диагностическое сообщение.

В-третьих, приложение может быть запущено в фоновом режиме (как демон), после чего основной процесс, породивший демон, завершается. С точки зрения контейнерной модели это означает завершение PID 1 и, следовательно, остановку контейнера. Приложения, предназначенные для работы в контейнере, должны выполняться на переднем плане (foreground), а не уходить в фоновый режим.

Понимание этих причин позволяет избежать типичной ошибки начинающих, которые пытаются решить проблему немедленного завершения добавлением искусственных задержек или бесконечных циклов ожидания. Такие «решения» маскируют реальную проблему и создают контейнеры, которые формально работают, но не выполняют полезной функции.

Инспекция и сопровождение контейнера

Работающий контейнер является объектом наблюдения и диагностики. Способность получить информацию о состоянии контейнера, его конфигурации и поведении приложения внутри него — это обязательная часть эксплуатационной дисциплины. Docker предоставляет несколько инструментов для решения этой задачи, каждый из которых отвечает на определённый класс вопросов.

Журналы приложения

Основным источником информации о поведении приложения в контейнере являются журналы (logs). В контейнерной модели стандартной практикой считается направление вывода приложения в стандартные потоки: стандартный поток вывода (stdout) и стандартный поток ошибок (stderr). Docker перехватывает эти потоки и сохраняет их содержимое, предоставляя доступ через команду docker logs.

Такой подход имеет несколько достоинств. Он не требует настройки файлового журналирования внутри контейнера. Он позволяет централизованно собирать журналы из множества контейнеров. Он совместим с различными драйверами журналирования, которые могут перенаправлять вывод во внешние системы хранения и анализа.

Вместе с тем следует учитывать ограничения. Не все приложения по умолчанию пишут в стандартные потоки. Некоторые пишут журналы в файлы внутри контейнера, что требует дополнительной настройки для их извлечения. Кроме того, объём журналов, хранимых Docker, по умолчанию не ограничен, что при интенсивном журналировании может привести к исчерпанию дискового пространства на хосте. Поэтому в эксплуатационных средах политика ротации и ограничения размера журналов является обязательной мерой.

Инспекция конфигурации контейнера

Команда docker inspect предоставляет детальную информацию о конфигурации контейнера: его параметрах запуска, сетевых настройках, подключённых томах, переменных окружения, состоянии и метаданных. Эта информация полезна для диагностики расхождений между ожидаемой и фактической конфигурацией. Например, если приложение не подключается к базе данных, инспекция может показать, что переменная окружения с адресом подключения содержит неверное значение или не задана.

Инспекция также позволяет проверить фактические ограничения ресурсов, назначенные контейнеру, его сетевой адрес в контейнерной сети, политику перезапуска и другие параметры, которые могли быть заданы при запуске. Это делает docker inspect инструментом верификации: инженер может убедиться, что контейнер запущен именно с теми параметрами, которые были запланированы.

Выполнение команд внутри контейнера

Команда docker exec позволяет запустить дополнительный процесс внутри работающего контейнера. Чаще всего это интерактивная оболочка, с помощью которой можно исследовать файловую систему, проверить наличие файлов, просмотреть сетевую конфигурацию или выполнить диагностическую команду. Этот инструмент полезен при отладке и локализации проблем.

Однако использование docker exec требует осознанного подхода. Выполнение команд внутри контейнера допустимо как средство диагностики, но не должно становиться инструментом ручной модификации среды. Если инженер вручную устанавливает пакет, изменяет конфигурационный файл или перезапускает процесс внутри контейнера, эти изменения не отражаются в образе и будут потеряны при пересоздании контейнера. Такой подход противоречит принципу неизменяемой инфраструктуры, который рассматривается далее.

Мониторинг потребления ресурсов

Команда docker stats предоставляет информацию о текущем потреблении ресурсов контейнерами: использование процессора, оперативной памяти, сетевого ввода-вывода и дискового ввода-вывода. Эти данные полезны для оценки фактической нагрузки и обнаружения аномалий: утечек памяти, чрезмерного потребления процессора или неожиданной сетевой активности.

На уровне единичного контейнера docker stats является инструментом оперативного наблюдения. Для систематического мониторинга, хранения истории метрик и настройки оповещений требуются внешние системы наблюдаемости, которые рассматриваются в теме, посвящённой безопасности и наблюдаемости контейнерных приложений.

Средства инспекции и диагностики контейнера
Средства инспекции и диагностики контейнера

Границы допустимого вмешательства

Инструменты инспекции и диагностики формируют важную дисциплинарную границу. Наблюдение за контейнером, чтение его журналов, проверка конфигурации и анализ потребления ресурсов — это нормальные эксплуатационные действия. Однако ручное изменение содержимого работающего контейнера, установка дополнительного программного обеспечения, модификация конфигурационных файлов внутри контейнера — это действия, нарушающие воспроизводимость среды и создающие расхождение между артефактом (образом) и реальным состоянием системы. Разграничение между допустимой диагностикой и нежелательной модификацией является одним из ключевых навыков инженера, работающего с контейнерами.

Неизменяемость среды и обновление приложений

Контейнерная модель развёртывания основана на принципе неизменяемой инфраструктуры (immutable infrastructure). Этот принцип утверждает, что компоненты среды выполнения не модифицируются после создания. Вместо внесения изменений в работающий экземпляр создаётся новый экземпляр на основе обновлённого артефакта. Применительно к контейнерам это означает, что обновление приложения производится не путём изменения содержимого работающего контейнера, а путём пересборки образа и пересоздания контейнера.

Суть принципа неизменяемости

В традиционной модели эксплуатации серверов обновление приложения нередко выполнялось путём ручного копирования файлов, установки пакетов и изменения конфигурации на работающем сервере. Такой подход порождает ряд проблем. Состояние сервера постепенно отклоняется от первоначального описания (явление, известное как «дрейф конфигурации», configuration drift). Воспроизвести точное состояние системы на другом узле становится невозможно без детальной документации каждого выполненного действия. Откат к предыдущей версии затруднён, поскольку изменения накапливаются без формальной фиксации.

Принцип неизменяемости устраняет эти проблемы путём жёсткого разделения двух операций. Первая операция — конструирование артефакта (образа), которая выполняется один раз и фиксируется. Вторая операция — запуск экземпляра (контейнера) на основе этого артефакта. Если требуется изменение, создаётся новый артефакт, а прежний экземпляр заменяется новым. Контейнер не модифицируется; он заменяется.

Цикл обновления приложения

Корректный цикл обновления контейнерного приложения состоит из нескольких последовательных этапов. Сначала вносятся изменения в исходный код, конфигурацию сборки или зависимости приложения. Затем выполняется пересборка образа, в результате которой создаётся новый артефакт с обновлённым содержимым. После этого существующий контейнер останавливается, и на его месте создаётся новый контейнер на основе обновлённого образа.

Этот цикл обеспечивает несколько важных свойств. Во-первых, каждая версия приложения представлена конкретным образом с определённым тегом, что позволяет точно знать, какой артефакт развёрнут в данный момент. Во-вторых, откат к предыдущей версии сводится к запуску контейнера на основе прежнего образа. В-третьих, устраняется проблема дрейфа конфигурации, поскольку состояние контейнера определяется исключительно его образом и параметрами запуска.

Принцип неизменяемой инфраструктуры и цикл обновления
Принцип неизменяемой инфраструктуры и цикл обновления

Ошибки ручной модификации контейнера

Несмотря на ясность принципа неизменяемости, на практике возникает соблазн внести «быстрое исправление» непосредственно в работающий контейнер. Типичные примеры: ручная правка конфигурационного файла через docker exec, установка дополнительного пакета для решения текущей проблемы, изменение прав доступа к файлам внутри контейнера.

Такие действия создают несколько проблем. Изменение не зафиксировано в образе и будет потеряно при следующем пересоздании контейнера. Другие экземпляры, запущенные из того же образа, не получат данного изменения, что приведёт к несогласованности между средами. Откат к предыдущему состоянию невозможен, поскольку изменение не является частью управляемого жизненного цикла артефакта. Кроме того, ручные вмешательства затрудняют диагностику: если система ведёт себя иначе, чем ожидается на основании образа, инженер не может исключить, что причиной является незафиксированное ручное изменение.

Корректный подход требует возвращения к этапу сборки. Если в приложении обнаружена ошибка, она исправляется в исходном коде, образ пересобирается, и контейнер пересоздаётся. Если требуется изменение конфигурации, оно вносится в параметры запуска или конфигурационные файлы, передаваемые контейнеру извне. Этот подход может казаться более длительным, но именно он обеспечивает прослеживаемость, воспроизводимость и управляемость, которые составляют основу контейнерной модели эксплуатации.

Границы применимости принципа неизменяемости

Принцип неизменяемости распространяется на программную среду контейнера, но не на все аспекты его работы. Данные, создаваемые приложением в процессе работы, по определению изменяются. Если приложение работает с базой данных, принимает загружаемые пользователями файлы или формирует кэш, эти данные не могут быть частью неизменяемого образа. Для их хранения используются тома и другие механизмы внешнего хранения, рассматриваемые в теме, посвящённой хранению данных в контейнерных средах.

Таким образом, неизменяемость образа и контейнера не означает неизменяемости всех данных приложения. Она означает неизменяемость программной среды: исполняемых файлов, библиотек, конфигурации, встроенной в образ. Разделение между изменяемыми данными и неизменяемой средой является фундаментальным архитектурным решением, которое должно быть принято на этапе проектирования контейнерного приложения.

Итоги темы

Управление контейнерами в среде выполнения является самостоятельной инженерной задачей, не сводимой к знанию команд запуска и остановки. Параметры запуска — переменные окружения, проброс портов, ограничения ресурсов и политика перезапуска — определяют поведение контейнера и должны задаваться осознанно, с учётом требований воспроизводимости и сопровождаемости.

Контейнер существует ровно столько, сколько работает его основной процесс. Понимание роли PID 1, механизма обработки сигналов и причин немедленного завершения контейнера позволяет избежать распространённых ошибок и проектировать приложения, корректно интегрированные с контейнерной средой.

Инструменты инспекции и диагностики — журналы, команды инспекции, выполнение команд внутри контейнера и мониторинг потребления ресурсов — обеспечивают наблюдаемость и контроль. Однако диагностика должна оставаться наблюдением, а не ручной модификацией: изменение содержимого работающего контейнера нарушает принцип неизменяемой инфраструктуры и создаёт неуправляемое расхождение между артефактом и реальным состоянием среды.

Принцип неизменяемости связывает этап сборки образа с этапом эксплуатации контейнера. Обновление приложения выполняется через пересборку образа и пересоздание контейнера, а не через модификацию работающего экземпляра. Этот подход обеспечивает прослеживаемость версий, управляемый откат и согласованность между средами. Вместе с разделением изменяемых данных и неизменяемой программной среды он формирует основу для перехода к более сложным сценариям развёртывания: сетевому взаимодействию, хранению данных и композиции многоконтейнерных приложений.

Лабораторная работа 5. Управление контейнерами в среде выполнения

  • Объём: 4 академических часа
  • Раздел курса: учебное пособие, тема 5 «Управление контейнерами в среде выполнения»

Введение

В предыдущей лабораторной работе образ был рассмотрен как воспроизводимый артефакт поставки: студент сосредоточился на том, что упаковано в контейнер. Настоящая работа смещает фокус на следующий этап жизненного цикла — управление контейнером в среде выполнения. Один и тот же образ ведёт себя по-разному в зависимости от переменных окружения, ограничений ресурсов, политики перезапуска, способа завершения основного процесса. Освоение этих параметров — необходимое условие для перехода от разового экспериментального запуска к управляемой эксплуатации.

Результаты работы готовят студента к последующим темам: сетевому взаимодействию контейнеров, хранению данных и многоконтейнерной композиции. На этом этапе важно увидеть контейнер не как «чёрный ящик», а как управляемый объект со своим жизненным циклом и набором эксплуатационных характеристик.

Цель работы

Освоить управление контейнером на этапе выполнения: настройку параметров запуска, диагностику состояния и реализацию принципа неизменяемой инфраструктуры на материале учебного веб-приложения.

После выполнения работы студент сможет:

  • запускать контейнер с заданными переменными окружения, ограничениями процессора и памяти, политикой перезапуска и пробросом портов;
  • определять процесс с PID 1 в контейнере и обосновывать выбор формы инструкции CMD (exec form vs shell form) с точки зрения обработки сигналов;
  • наблюдать поведение контейнера при штатной остановке (SIGTERM) и принудительном завершении (SIGKILL) и интерпретировать результат;
  • собирать диагностические сведения о работающем контейнере штатными средствами среды выполнения: журналы, конфигурация, метрики, выполнение команд внутри контейнера;
  • обновлять приложение через пересоздание контейнера из нового образа без ручной модификации работающего экземпляра.

Теоретический минимум

Теоретический материал, необходимый для выполнения работы, раскрыт в учебном пособии, тема 5: разделы «Параметры запуска контейнера», «Основной процесс и завершение контейнера», «Инспекция и сопровождение контейнера», «Неизменяемость среды и обновление приложений». Ниже приведены только справочные сведения об используемых командах и опциях, нужные непосредственно по ходу работы.

Опции запуска docker run, используемые в задании:

  • -e KEY=VALUE — задать переменную окружения внутри контейнера;
  • -p HOST:CONTAINER — пробросить порт хоста на порт контейнера;
  • --cpus=N — ограничить доступное процессорное время эквивалентом N ядер;
  • --memory=SIZE — ограничить максимальный объём оперативной памяти;
  • --restart=POLICY — политика перезапуска: no (по умолчанию), on-failure[:N], unless-stopped, always;
  • --name NAME — присвоить контейнеру имя, удобное для последующих команд.

Команды инспекции:

  • docker ps [-a] — список запущенных (-a — всех) контейнеров;
  • docker logs [-f] NAME — журнал вывода stdout/stderr основного процесса (-f — потоковый режим);
  • docker inspect NAME — полный JSON-документ конфигурации и состояния контейнера;
  • docker stats [NAME ...] — потоковые метрики потребления ресурсов;
  • docker exec [-it] NAME CMD — выполнение команды внутри уже работающего контейнера;
  • docker top NAME — список процессов контейнера с точки зрения хоста.

Сигналы остановки. docker stop посылает основному процессу SIGTERM и через таймаут (по умолчанию 10 с) — SIGKILL. docker kill посылает SIGKILL немедленно, без возможности корректного завершения. Поведение приложения на SIGTERM определяется самим приложением: если оно не установило обработчик, процесс будет завершён только после SIGKILL.

Формы инструкции CMD. Exec form (CMD ["python", "app.py"]) запускает процесс напрямую — он становится PID 1 и получает сигналы от среды выполнения. Shell form (CMD python app.py) запускает процесс через /bin/sh -c, который и становится PID 1; сигналы при этом не пробрасываются дочернему процессу, и приложение не видит SIGTERM.

Перечень оснащения

  • Локальная машина с установленным Docker Engine 24.0+ или Docker Desktop, проверка готовности — docker version, docker info.
  • Сетевой доступ к Docker Hub для загрузки базовых образов python:3.12-slim.
  • Свободные TCP-порты 8090–8093 на хосте.
  • Не менее 1 ГБ свободного места на диске и не менее 2 ГБ свободной оперативной памяти (для проверки лимитов памяти).
  • Утилита curl для проверки HTTP-доступности приложения.
  • Текстовый редактор для подготовки app.py и Dockerfile.

Эталонные решения и индивидуальные варианты не предусмотрены: задание выполняется всеми студентами в общей формулировке.

Порядок выполнения работы

Работа состоит из четырёх частей. Все команды выполняются в каталоге ~/lab05, который создаётся в начале работы:

mkdir -p ~/lab05 && cd ~/lab05

Часть 1. Подготовка учебного приложения и параметры запуска

Задание

  1. В каталоге ~/lab05 создайте файл app.py:
    from http.server import HTTPServer, SimpleHTTPRequestHandler
    import os
    import signal
    import sys
    import time
    
    port = int(os.environ.get("PORT", 8000))
    greeting = os.environ.get("GREETING", "default")
    
    class Handler(SimpleHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(f"{greeting}, порт {port}\n".encode())
    
        def log_message(self, format, *args):
            sys.stderr.write("%s - %s\n" % (self.address_string(), format % args))
    
    server = HTTPServer(("0.0.0.0", port), Handler)
    
    def shutdown(signum, frame):
        sys.stderr.write(f"получен сигнал {signum}, завершаюсь корректно\n")
        sys.stderr.flush()
        server.server_close()
        sys.exit(0)
    
    signal.signal(signal.SIGTERM, shutdown)
    signal.signal(signal.SIGINT, shutdown)
    
    sys.stderr.write(f"запуск, PID={os.getpid()}, порт={port}\n")
    sys.stderr.flush()
    server.serve_forever()
    
  2. Создайте Dockerfile в каталоге ~/lab05:
    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY app.py .
    
    ENV PORT=8000
    ENV GREETING="контейнерное приложение"
    
    EXPOSE 8000
    
    CMD ["python", "-u", "app.py"]
    

    Обратите внимание на форму CMD (exec form, JSON-массив) и флаг -u интерпретатора Python: они потребуются в части 2 для корректной работы с сигналами и журналом.
  3. Соберите образ:
    docker build -t lab05-app:v1 .
    
  4. Запустите контейнер с переопределением переменной окружения и пробросом порта:
    docker run -d --name lab05-run1 \
        -e GREETING="лабораторная 5" \
        -p 8090:8000 \
        lab05-app:v1
    curl http://localhost:8090
    

    Зафиксируйте вывод и сравните его с тем, что было бы при запуске без -e GREETING=....
  5. Запустите второй контейнер с ограничениями ресурсов и политикой перезапуска:
    docker run -d --name lab05-run2 \
        --cpus=0.5 \
        --memory=128m \
        --restart=on-failure:3 \
        -p 8091:8000 \
        lab05-app:v1
    

    Проверьте сохранённые параметры:
    docker inspect lab05-run2 \
        --format '{{.HostConfig.NanoCpus}} {{.HostConfig.Memory}} {{.HostConfig.RestartPolicy}}'
    

    Зафиксируйте полученные значения и поясните, как они соотносятся с переданными ключами --cpus, --memory, --restart.
  6. Оставьте контейнер lab05-run1 запущенным до конца части 2; контейнер lab05-run2 остановите и удалите:
    docker stop lab05-run2 && docker rm lab05-run2
    

Результат

Работающий контейнер lab05-run1, доступный по http://localhost:8090, и зафиксированные параметры lab05-run2: значения CPU/памяти/политики перезапуска, прочитанные через docker inspect, с пояснением соответствия исходным ключам командной строки.

Часть 2. PID 1 и обработка сигналов

Задание

  1. Определите процесс с PID 1 в работающем контейнере:
    docker top lab05-run1
    docker exec lab05-run1 ps -o pid,ppid,cmd
    

    Зафиксируйте имя процесса с PID 1 и поясните, почему именно он получает сигналы от Docker.
  2. Выполните корректную остановку с измерением времени реакции:
    time docker stop lab05-run1
    docker logs lab05-run1 | tail -5
    

    В журнале должна появиться строка вида получен сигнал 15, завершаюсь корректно — это отклик обработчика SIGTERM в приложении. Зафиксируйте время остановки.
  3. Удалите контейнер и пересоздайте его с альтернативным запуском в shell form (без exec form):
    docker rm lab05-run1
    
    docker run -d --name lab05-shell \
        -p 8090:8000 \
        --entrypoint sh \
        lab05-app:v1 \
        -c "python -u app.py"
    

    Аргументы --entrypoint sh ... -c "..." намеренно эмулируют типичную ошибку: запуск приложения через оболочку, при которой PID 1 — это sh, а не Python.
  4. Проверьте PID 1 нового контейнера и попытайтесь его остановить:
    docker exec lab05-shell ps -o pid,ppid,cmd
    time docker stop lab05-shell
    docker logs lab05-shell | tail -5
    

    Зафиксируйте: имя процесса с PID 1, время выполнения docker stop, наличие или отсутствие строки об обработке SIGTERM в журнале. Объясните, почему оболочка не пробросила сигнал дочернему процессу и какие последствия это имеет в эксплуатации.
  5. Выполните принудительное завершение для сравнения:
    docker run -d --name lab05-kill -p 8092:8000 lab05-app:v1
    sleep 1
    time docker kill lab05-kill
    docker logs lab05-kill | tail -5
    

    Зафиксируйте время завершения и наличие строки об обработке сигнала. Поясните разницу между docker stop и docker kill с точки зрения приложения.
  6. Удалите все контейнеры этой части:
    docker rm lab05-shell lab05-kill
    

Результат

Сравнительная таблица из трёх запусков (exec form + docker stop, shell form + docker stop, exec form + docker kill) с тремя столбцами: PID 1, время завершения, наличие записи об обработке сигнала в журнале. Текстовое объяснение наблюдаемых различий со ссылкой на форму CMD и тип сигнала.

Часть 3. Инспекция и диагностика работающего контейнера

Задание

  1. Запустите контейнер заново для использования в этой части:
    docker run -d --name lab05-diag \
        -e GREETING="диагностика" \
        --memory=128m \
        -p 8090:8000 \
        lab05-app:v1
    
  2. Сгенерируйте искусственную нагрузку запросами:
    for i in $(seq 1 20); do curl -s http://localhost:8090 > /dev/null; done
    
  3. Изучите журнал приложения двумя способами:
    docker logs lab05-diag
    docker logs --tail 5 --timestamps lab05-diag
    

    Зафиксируйте, какие потоки попадают в журнал (stdout/stderr) и какую информацию даёт флаг --timestamps.
  4. Снимите конфигурацию и состояние контейнера:
    docker inspect lab05-diag \
        --format '{{.State.Status}} {{.State.Pid}} {{.NetworkSettings.IPAddress}}'
    
    docker inspect lab05-diag --format '{{json .Config.Env}}' | python3 -m json.tool
    

    Зафиксируйте состояние контейнера, PID процесса с точки зрения хоста, IP-адрес во внутренней сети и список переменных окружения.
  5. Получите потоковые метрики потребления ресурсов одной выборкой:
    docker stats --no-stream lab05-diag
    

    Зафиксируйте значения CPU %, MEM USAGE/LIMIT, NET I/O. Сопоставьте лимит памяти со значением, переданным при запуске.
  6. Войдите внутрь контейнера для диагностики и осмотрите файловую систему:
    docker exec -it lab05-diag sh -c "ls /app && cat /etc/os-release | head -3"
    

    Зафиксируйте имя приложения, найденное в /app, и базовый дистрибутив, использованный в образе.
  7. Завершите контейнер:
    docker stop lab05-diag && docker rm lab05-diag
    

Результат

Журнал контейнера с временными метками, выдержки docker inspect (статус, PID, IP, переменные окружения), снимок docker stats, результат docker exec с подтверждением содержимого образа. Краткое пояснение, какой инструмент следует выбирать для каждой типовой задачи диагностики: «приложение упало», «приложение медленно отвечает», «приложение не видит конфигурацию».

Часть 4. Неизменяемое обновление приложения

Задание

  1. Запустите контейнер версии v1:
    docker run -d --name lab05-prod -p 8093:8000 lab05-app:v1
    curl http://localhost:8093
    

    Зафиксируйте ответ приложения.
  2. Сначала примените антипаттерн — измените файл прямо внутри работающего контейнера:
    docker exec lab05-prod sh -c "sed -i 's/default/изменено вручную/' /app/app.py"
    docker exec lab05-prod cat /app/app.py | grep greeting
    

    Поясните в отчёте, почему такое изменение не приведёт к фактическому обновлению поведения приложения (даже без перезапуска контейнера) и какие последствия оно имеет для воспроизводимости.
  3. Удалите изменённый контейнер и проверьте, что в образе ничего не сохранилось:
    docker rm -f lab05-prod
    docker run --rm lab05-app:v1 grep greeting /app/app.py
    

    Зафиксируйте: образ остался прежним; правки docker exec затрагивали только слой записи удалённого контейнера.
  4. Теперь выполните корректное обновление через пересборку. Внесите изменение в app.py: замените значение по умолчанию переменной greeting на "приложение v2", после чего соберите новый образ с новой версией:
    docker build -t lab05-app:v2 .
    docker images lab05-app
    
  5. Выполните управляемое обновление: остановите старую версию, удалите контейнер и запустите новый из обновлённого образа:
    docker stop lab05-prod 2>/dev/null
    docker rm lab05-prod 2>/dev/null
    docker run -d --name lab05-prod -p 8093:8000 lab05-app:v2
    curl http://localhost:8093
    

    Зафиксируйте, что ответ приложения изменился. Поясните, чем такая модель обновления (пересоздание контейнера из нового образа) отличается от ручной правки внутри контейнера и в чём её преимущество для эксплуатации.
  6. Проверьте, что обе версии образа продолжают существовать локально и можно вернуться к предыдущей:
    docker stop lab05-prod && docker rm lab05-prod
    docker run -d --name lab05-prod-rollback -p 8093:8000 lab05-app:v1
    curl http://localhost:8093
    

    Зафиксируйте: откат сводится к запуску предыдущего тегированного образа без каких-либо «отмен» правок.
  7. Выполните очистку:
    docker stop lab05-prod-rollback && docker rm lab05-prod-rollback
    docker image rm lab05-app:v1 lab05-app:v2
    

Результат

Зафиксированная пара ответов приложения «до» и «после» обновления, подтверждение неизменности образа после ручной правки внутри контейнера, выполненный откат к версии v1 без модификаций образа. Текстовое объяснение принципа неизменяемой инфраструктуры в одном-двух абзацах.

Форма отчёта

Базовые требования к оформлению отчёта (титульный лист, структура файлов в репозитории, порядок сдачи через MR) — в shared/submission.md. Здесь указано только специфичное для данной работы.

В отчёт включить:

  • сведения об окружении: версия Docker Engine (docker version), операционная система, объём оперативной памяти хоста;
  • по каждой из четырёх частей — последовательность выполненных команд, их вывод (как текст в блоках кода) и фиксацию указанного в разделе «Результат» части;
  • сравнительную таблицу из части 2 (3 строки × 3 столбца: PID 1, время завершения, обработка сигнала);
  • снимок docker stats из части 3;
  • общий вывод (5–10 предложений): какие параметры запуска оказались определяющими для воспроизводимости поведения контейнера, в чём состоит роль PID 1 и почему форма CMD критична для корректной остановки, как соотносятся принципы неизменяемой инфраструктуры с задачами обновления и отката.

Файлы app.py и Dockerfile приложить к отчёту в неизменном виде. Скриншоты — только при необходимости подтвердить визуальные особенности (например, цвет/символ статуса в docker stats).

Контрольные вопросы

  1. Чем переменные окружения отличаются от аргументов командной строки как способ передачи конфигурации контейнеру? В каких случаях каждый из них предпочтителен?
  2. Что происходит, если контейнер запущен с ограничением --memory=128m, а приложение пытается выделить 200 МБ? В чём отличие этого поведения от системы без cgroups?
  3. Почему процесс с PID 1 в контейнере играет особую роль? Что произойдёт, если этот процесс не установит обработчик сигнала SIGTERM?
  4. Сравните exec form и shell form инструкции CMD. Почему shell form может приводить к тому, что приложение завершается только по таймауту docker stop?
  5. В чём принципиальное отличие команд docker stop и docker kill с точки зрения возможности корректного завершения работы приложения?
  6. Какие задачи диагностики решаются с помощью docker logs, docker inspect и docker stats? Приведите по одному эксплуатационному сценарию для каждого инструмента.
  7. Почему изменение файлов приложения внутри работающего контейнера через docker exec считается антипаттерном? Что произойдёт с этими изменениями при пересоздании контейнера?
  8. В чём состоит принцип неизменяемой инфраструктуры? Каким образом он реализуется при обновлении контейнерного приложения и при откате к предыдущей версии?