Тема 04

Сборка и сопровождение контейнерных образов

Сборка и сопровождение контейнерных образов

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

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

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

Dockerfile как декларативное описание образа

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

Декларативный характер Dockerfile не означает отсутствия пошаговой логики. Напротив, порядок инструкций имеет принципиальное значение. Сборка обычно начинается с инструкции FROM, которая определяет базовый образ (base image). Базовый образ задаёт исходную среду: тип дистрибутива, набор системных библиотек, иногда интерпретатор языка программирования или среду выполнения. Выбор базового образа влияет на размер итогового артефакта, совместимость приложения, доступность инструментов диагностики и поверхность потенциальных уязвимостей. Поэтому выбор «самого маленького» образа не всегда является лучшим решением: чрезмерное упрощение может затруднить сопровождение и отладку.

После выбора базового образа в Dockerfile задаются операции подготовки среды. Инструкции RUN используются для выполнения команд в процессе сборки, например для установки пакетов, компиляции исходного кода или формирования каталогов приложения. Инструкция COPY переносит файлы из контекста сборки внутрь образа, а ADD выполняет сходную функцию, но обладает дополнительными возможностями, которые без необходимости часто использовать не рекомендуется, поскольку это усложняет предсказуемость сборки. Инструкции ENV, WORKDIR, EXPOSE, CMD и ENTRYPOINT определяют параметры окружения, рабочий каталог, сетевые намерения и поведение контейнера при запуске. Важно понимать, что Dockerfile объединяет как операции подготовки файловой системы, так и описание будущего режима исполнения.

Пример базового Dockerfile для веб-приложения

Рассмотрим упрощённый пример Dockerfile для небольшого приложения на Python:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

ENV PORT=8000
EXPOSE 8000

CMD ["python", "app.py"]

В этом примере инструкция FROM задаёт базовую среду с интерпретатором Python. Инструкция WORKDIR определяет рабочий каталог внутри образа и в дальнейшем делает каталог /app текущей рабочей директорией для последующих инструкций. Это означает, что относительные пути в COPY, RUN и CMD далее интерпретируются уже с учётом каталога /app, если не указан иной абсолютный путь.

Для правильного понимания примера необходимо пояснить, что такое контекст сборки. Когда пользователь запускает команду вида docker build ., точка в конце означает, что контекстом сборки становится текущий каталог на машине пользователя. Именно из этого каталога Docker может брать файлы для инструкций COPY и ADD. Следовательно, Docker не копирует файлы произвольно из всей файловой системы хоста, а работает только с тем набором данных, который был передан ему как контекст. Если нужный файл находится вне контекста, инструкция COPY не сможет его использовать без изменения структуры проекта или явного выбора другого контекста.

Инструкция COPY requirements.txt . читается слева направо: сначала указывается источник в контексте сборки, затем назначение внутри образа. В данном случае Docker берёт файл requirements.txt из корня контекста сборки на хостовой машине и копирует его в текущий рабочий каталог внутри образа, то есть в /app/requirements.txt. После этого инструкция RUN pip install --no-cache-dir -r requirements.txt выполняется уже внутри промежуточного контейнера сборки и использует только что скопированный файл зависимостей. Такое разбиение имеет практический смысл: если исходный код приложения изменится, но файл requirements.txt останется прежним, Docker сможет повторно использовать уже собранный слой с установленными пакетами.

Инструкция COPY . . требует отдельного пояснения, поскольку запись с двумя точками часто интерпретируется неверно. Первая точка обозначает источник, то есть весь текущий контекст сборки на стороне хоста, за исключением файлов, исключённых через .dockerignore. Вторая точка обозначает каталог назначения внутри образа, то есть текущий рабочий каталог /app. Иными словами, после выполнения этой инструкции файлы проекта из локального каталога пользователя переносятся внутрь файловой системы образа в каталог /app. Если в проекте присутствуют, например, app.py, каталог templates/ и файл config.yaml, то при отсутствии исключений они будут скопированы в /app/app.py, /app/templates/ и /app/config.yaml.

Наконец, CMD задаёт команду, которая будет выполнена при запуске контейнера по умолчанию. В рассматриваемом случае это команда python app.py, выполняемая в каталоге /app, где уже находятся скопированные файлы приложения и установленные зависимости.

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

Пример минимального Dockerfile для статического содержимого

Если приложение не требует собственной среды выполнения, Dockerfile может быть значительно проще. Например, для публикации статического сайта достаточно использовать готовый веб-сервер:

FROM nginx:1.27-alpine

COPY site/ /usr/share/nginx/html/

EXPOSE 80

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

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

Формирование слоёв образа из инструкций Dockerfile
Формирование слоёв образа из инструкций Dockerfile

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

Контекст сборки и структура входных данных

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

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

Контекст сборки и роль .dockerignore
Контекст сборки и роль .dockerignore

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

Слои образа и логика пошаговой сборки

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

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

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

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

Кэширование и воспроизводимость сборки

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

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

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

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

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

Практика написания Dockerfile и типичные ошибки

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

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

Ещё одна типичная ошибка состоит в объединении несвязанных команд в одну длинную инструкцию RUN исключительно ради уменьшения числа слоёв. Формально это может уменьшить историю изменений, но ухудшает читаемость, затрудняет сопровождение и делает диагностику сбоев менее прозрачной. Сокращение числа слоёв не должно становиться самоцелью. Более правильный подход заключается в логическом группировании действий: в пределах одного шага объединяются тесно связанные операции, а между шагами сохраняется понятная структура.

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

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

Оптимизация образов и многоэтапная сборка

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

Наиболее важным инструментом оптимизации является многоэтапная сборка (multi-stage build). Её идея состоит в том, что разные стадии Dockerfile выполняют разные роли. На одной стадии могут устанавливаться компиляторы и инструменты сборки, на другой — выполняться компиляция или упаковка приложения, а в итоговый образ переносятся только результаты, необходимые для запуска. Такой подход позволяет отделить среду построения артефакта от среды его исполнения. Практическое следствие очевидно: в финальном образе не остаются лишние инструменты, которые увеличивали бы размер и расширяли бы потенциальную поверхность атаки.

Многоэтапная сборка: стадия построения и финальный образ
Многоэтапная сборка: стадия построения и финальный образ

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

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

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

Версионирование образов и управление изменениями

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

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

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

Жизненный цикл контейнерного образа: от сборки до развёртывания
Жизненный цикл контейнерного образа: от сборки до развёртывания

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

Публикация образов и роль реестров

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

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

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

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

Сопровождение образов в цепочке поставки

На зрелом уровне контейнерный образ рассматривается как объект цепочки поставки программного обеспечения, а не как разовый технический продукт. Это означает, что сборка образа должна быть встроена в процессы проверки исходного кода, автоматического тестирования, анализа состава зависимостей и публикации результатов. Чем меньше ручных действий требуется для получения и размещения образа, тем ниже риск человеческой ошибки и тем выше воспроизводимость.

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

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

Итоги темы

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

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

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

Лабораторная работа 4. Сборка и сопровождение контейнерных образов

Цель работы

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

Предварительные сведения

Для выполнения лабораторной работы необходима рабочая среда Docker, настроенная в предыдущей лабораторной работе (Docker Engine 24.0+ или Docker Desktop). Дополнительно потребуется текстовый редактор для создания Dockerfile и учебных файлов приложения.

Проверка готовности среды:

docker version
docker info

В качестве учебного приложения используется минимальный веб-сервер на Python. Знание языка Python не требуется: приложение предоставляется в готовом виде и служит объектом упаковки.

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

Задание

Работа состоит из пяти последовательных частей.

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

Порядок выполнения:

  1. Создайте каталог проекта и подготовьте файлы учебного приложения:
    mkdir -p ~/lab04/app
    cd ~/lab04/app
    
  2. Создайте файл app.py со следующим содержимым:
    from http.server import HTTPServer, SimpleHTTPRequestHandler
    import os
    
    port = int(os.environ.get("PORT", 8000))
    
    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"Учебное приложение, порт {port}\n".encode())
    
    HTTPServer(("0.0.0.0", port), Handler).serve_forever()
    
  3. Создайте файл requirements.txt:
    # Зависимости отсутствуют: приложение использует только стандартную библиотеку.
    # Файл сохранён для демонстрации типовой структуры проекта.
    
  4. Создайте Dockerfile в каталоге ~/lab04/app:
    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . .
    
    ENV PORT=8000
    EXPOSE 8000
    
    CMD ["python", "app.py"]
    
  5. Соберите образ и запустите контейнер:
    docker build -t lab04-app:v1 .
    docker run -d --name lab04-test -p 8080:8000 lab04-app:v1
    
  6. Проверьте работоспособность приложения:
    curl http://localhost:8080
    

    Зафиксируйте вывод. Остановите и удалите контейнер:
    docker stop lab04-test && docker rm lab04-test
    

Результат: работающий образ lab04-app:v1, зафиксированный вывод приложения, пояснение назначения каждой инструкции Dockerfile.

Часть 2. Анализ слоёв образа и контекста сборки

Порядок выполнения:

  1. Исследуйте слоистую структуру собранного образа:
    docker history lab04-app:v1
    

    Зафиксируйте количество слоёв, их размер и соответствие инструкциям Dockerfile. Определите, какие инструкции создают новые слои, а какие добавляют только метаданные.
  2. Исследуйте влияние контекста сборки. Создайте в каталоге проекта несколько лишних файлов:
    dd if=/dev/zero of=dummy-large-file.bin bs=1M count=50
    mkdir -p test-data
    echo "тестовые данные" > test-data/notes.txt
    
  3. Выполните повторную сборку и обратите внимание на размер контекста, передаваемого демону Docker:
    docker build -t lab04-app:v1-no-ignore .
    

    Зафиксируйте строку Sending build context to Docker daemon из вывода сборки.
  4. Создайте файл .dockerignore:
    dummy-large-file.bin
    test-data/
    .dockerignore
    Dockerfile
    
  5. Выполните сборку повторно:
    docker build -t lab04-app:v1-with-ignore .
    

    Зафиксируйте размер контекста после применения .dockerignore. Сравните размеры образов:
    docker images lab04-app
    

    Объясните, каким образом .dockerignore влияет на состав образа и скорость сборки.
  6. Удалите лишние файлы:
    rm dummy-large-file.bin
    rm -rf test-data
    

Результат: зафиксированная структура слоёв, сравнение размеров контекста с .dockerignore и без него, объяснение роли контекста сборки.

Часть 3. Кэширование и порядок инструкций

Порядок выполнения:

  1. Внесите изменение в файл app.py, например замените строку ответа:
    self.wfile.write(f"Учебное приложение v2, порт {port}\n".encode())
    
  2. Выполните повторную сборку и наблюдайте за использованием кэша:
    docker build -t lab04-app:v2 .
    

    Зафиксируйте, какие шаги использовали кэш (отмечены CACHED), а какие были выполнены заново. Объясните, почему шаг RUN pip install был взят из кэша, хотя исходный код приложения изменился.
  3. Теперь создайте альтернативный Dockerfile.bad с нерациональным порядком инструкций:
    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY . .
    
    RUN pip install --no-cache-dir -r requirements.txt
    
    ENV PORT=8000
    EXPOSE 8000
    
    CMD ["python", "app.py"]
    
  4. Соберите образ из этого Dockerfile:
    docker build -f Dockerfile.bad -t lab04-app:v2-bad .
    
  5. Внесите ещё одно изменение в app.py (например, измените номер порта по умолчанию на 9000) и выполните сборку обоих вариантов:
    docker build -t lab04-app:v3 .
    docker build -f Dockerfile.bad -t lab04-app:v3-bad .
    

    Зафиксируйте, в каком случае шаг установки зависимостей использовал кэш, а в каком — был выполнен заново. Объясните связь между порядком инструкций COPY и эффективностью кэширования.
  6. Удалите промежуточные образы:
    rm Dockerfile.bad
    docker image rm lab04-app:v2-bad lab04-app:v3-bad
    

Результат: зафиксированные сборки с кэшированием и без, объяснение влияния порядка инструкций на повторную сборку.

Часть 4. Многоэтапная сборка

Цель части — продемонстрировать разделение стадии сборки и стадии исполнения средствами многоэтапного Dockerfile на примере Python-приложения с зависимостями, требующими компиляции.

Порядок выполнения:

  1. Создайте в каталоге ~/lab04 подкаталог для приложения с нативными зависимостями:
    mkdir -p ~/lab04/multi-app
    cd ~/lab04/multi-app
    
  2. Создайте файл app.py:
    from http.server import HTTPServer, SimpleHTTPRequestHandler
    import os
    import markupsafe
    
    port = int(os.environ.get("PORT", 8000))
    
    class Handler(SimpleHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200)
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.end_headers()
            msg = markupsafe.escape(f"Многоэтапная сборка, порт {port}")
            self.wfile.write(f"{msg}\n".encode())
    
    HTTPServer(("0.0.0.0", port), Handler).serve_forever()
    
  3. Создайте файл requirements.txt:
    markupsafe==3.0.2
    
  4. Создайте однофазный Dockerfile.single, использующий полный образ с инструментами сборки:
    FROM python:3.12
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . .
    
    ENV PORT=8000
    EXPOSE 8000
    
    CMD ["python", "app.py"]
    
  5. Создайте многоэтапный Dockerfile:
    FROM python:3.12 AS builder
    
    WORKDIR /build
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
    
    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY --from=builder /install /usr/local
    COPY . .
    
    ENV PORT=8000
    EXPOSE 8000
    
    CMD ["python", "app.py"]
    

    Обратите внимание на ключевые элементы: на стадии builder зависимости устанавливаются с флагом --prefix=/install в изолированный каталог; на финальной стадии используется компактный образ python:3.12-slim, а результат установки переносится из стадии сборки инструкцией COPY --from=builder.
  6. Соберите оба варианта и сравните размеры:
    docker build -f Dockerfile.single -t lab04-multi:single .
    docker build -t lab04-multi:multi .
    
    docker images lab04-multi
    

    Зафиксируйте размеры обоих образов. Объясните, за счёт чего многоэтапная сборка уменьшает итоговый размер (какие компоненты присутствуют в python:3.12, но отсутствуют в python:3.12-slim).
  7. Проверьте работоспособность образа, собранного многоэтапной сборкой:
    docker run -d --name lab04-multi-test -p 8081:8000 lab04-multi:multi
    curl http://localhost:8081
    docker stop lab04-multi-test && docker rm lab04-multi-test
    
  8. Исследуйте слои обоих образов:
    docker history lab04-multi:single
    docker history lab04-multi:multi
    

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

Результат: два образа с сопоставлением размеров, объяснение принципа многоэтапной сборки, зафиксированный вывод docker history.

Часть 5. Версионирование и публикация в локальный реестр

Порядок выполнения:

  1. Разверните локальный реестр контейнерных образов:
    docker run -d -p 5000:5000 --name registry registry:2
    

    Убедитесь, что реестр работает:
    curl http://localhost:5000/v2/_catalog
    
  2. Присвойте образу учебного приложения теги для публикации в локальный реестр:
    cd ~/lab04/app
    
    docker build -t lab04-app:v3 .
    
    docker tag lab04-app:v3 localhost:5000/lab04-app:v3
    docker tag lab04-app:v3 localhost:5000/lab04-app:latest
    

    Объясните в отчёте, почему тег latest не рекомендуется использовать как единственный идентификатор версии.
  3. Опубликуйте образ в локальный реестр:
    docker push localhost:5000/lab04-app:v3
    docker push localhost:5000/lab04-app:latest
    
  4. Убедитесь, что образ доступен в реестре:
    curl http://localhost:5000/v2/_catalog
    curl http://localhost:5000/v2/lab04-app/tags/list
    

    Зафиксируйте список доступных тегов.
  5. Удалите локальную копию образа и загрузите его из реестра:
    docker image rm localhost:5000/lab04-app:v3
    docker pull localhost:5000/lab04-app:v3
    docker run -d --name lab04-from-registry -p 8082:8000 localhost:5000/lab04-app:v3
    curl http://localhost:8082
    

    Зафиксируйте, что приложение работает корректно после загрузки из реестра.
  6. Выполните очистку:
    docker stop lab04-from-registry registry
    docker rm lab04-from-registry registry
    docker image prune -f
    

Результат: образ опубликован в локальный реестр, загружен из него и запущен, зафиксированы теги и каталог реестра.

Требования к оформлению отчёта

Отчёт должен содержать:

  • титульный лист с указанием номера лабораторной работы, темы, имени студента и номера группы;
  • краткое описание конфигурации рабочего места: версия операционной системы и Docker Engine;
  • результаты каждой из пяти частей задания: созданные файлы (Dockerfile, .dockerignore, исходный код), выполненные команды и их вывод, наблюдения, ответы на поставленные вопросы;
  • сравнительную таблицу размеров образов из части 4 (однофазная сборка на python:3.12 и многоэтапная сборка на python:3.12-slim);
  • общий вывод по работе (5–10 предложений): какие принципы сборки образов проявились на практике, какие ошибки проектирования Dockerfile были выявлены, в чём состоит практический смысл многоэтапной сборки и версионирования.

Скриншоты прилагаются по необходимости для подтверждения выполненных шагов. Вывод команд допускается вставлять как текст в блоках кода. Файлы Dockerfile и исходного кода прилагаются к отчёту.

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

  1. Какую роль выполняет инструкция FROM в Dockerfile? Что произойдёт, если указать несуществующий базовый образ?
  2. Чем отличаются инструкции COPY и ADD? В каких случаях предпочтительна COPY?
  3. Объясните, почему файл requirements.txt копируется отдельно от остального кода приложения. Как это связано с механизмом кэширования?
  4. Что такое контекст сборки? Каковы последствия включения в контекст лишних файлов?
  5. Каким образом многоэтапная сборка уменьшает размер итогового образа? Почему в финальном образе отсутствуют инструменты сборки, хотя зависимости были скомпилированы на стадии builder?
  6. В чём опасность использования тега latest как единственного идентификатора образа в цепочке поставки?
  7. Почему секреты (пароли, токены) нельзя включать в Dockerfile через инструкции COPY или ENV? Каким образом удалённый на следующем слое файл может быть восстановлен из образа?
  8. Какие преимущества даёт использование реестра образов по сравнению с локальной сборкой на каждом целевом сервере?