Сборка Docker-образов в CI/CD-пайплайнах
У вас есть Dockerfile, который отлично работает на локальной машине. Вы запускаете docker build, образ собирается, приложение запускается. Но когда вы пушите тот же Dockerfile в пайплайн, всё начинает работать иначе.
Сборка, которая на вашей машине занимала две минуты, теперь длится пятнадцать. Образ, работавший вчера, сегодня падает без очевидной причины. А когда нужно пересобрать старую версию для отката, полученный образ ведёт себя иначе, чем оригинал.
Эти проблемы не случайны. Они возникают из-за разницы между сборкой образов на машине разработчика и в автоматизированном пайплайне. Понимание этой разницы — первый шаг к её устранению.
Что меняется при переходе в пайплайн
Когда вы собираете образ в пайплайне, принципиально меняются три вещи.
Во-первых, сборка выполняется на чужой машине. У неё могут быть другие ресурсы, сетевое окружение и файловая система. Ваш Dockerfile должен работать независимо от места выполнения.
Во-вторых, пайплайн должен пересобирать образ при каждом изменении кода. Это не опционально. Если пропускать пересборки, в деплой попадёт устаревший код. Но пересборка каждого коммита запускает полный процесс сборки, и этот процесс должен быть достаточно быстрым, чтобы не тормозить разработку.
В-третьих, сборка должна быть воспроизводимой. Один и тот же исходный код должен давать один и тот же образ независимо от времени и места сборки. Без воспроизводимости вы не можете доверять откату на старый коммит — он может не восстановить точное поведение приложения.
Диаграмма ниже показывает типичные этапы пайплайна сборки Docker: от исходного кода до пуша в registry.
Контролируйте контекст сборки
Контекст сборки — это папка, которую вы отправляете Docker-демону при выполнении docker build. На локальной машине это обычно папка проекта. В пайплайне происходит то же самое, но последствия большого контекста хуже.
Каждый файл в контексте сборки передаётся Docker-демону. Если в репозитории лежат node_modules, виртуальные окружения Python или скомпилированные бинарники, эти файлы передаются, даже если они не нужны для сборки. Это замедляет каждый запуск пайплайна.
Решение — файл .dockerignore. Он работает как .gitignore, но для Docker-сборок. Перечислите всё, что не нужно для образа: папки зависимостей, кэш, историю .git, тестовые фикстуры и документацию. Компактный контекст сборки означает более быстрые сборки и меньший сетевой трафик.
Используйте кэширование с умом
Docker собирает образы послойно. Каждая инструкция в Dockerfile создаёт один слой. При пересборке Docker проверяет, изменился ли слой. Если слой не изменился, Docker использует кэшированный результат предыдущей сборки.
Этот механизм кэширования — ваш главный союзник для быстрых сборок. Но он работает только если кэш сохраняется между запусками пайплайна.
В локальной разработке кэш живёт на вашей машине. В пайплайне кэш исчезает после завершения раннера, если вы явно его не сохраните. Некоторые CI-системы предоставляют встроенное кэширование слоёв Docker. Если ваша система этого не делает, нужно настроить кэширование вручную или смириться с тем, что каждая сборка начинается с нуля.
Даже при работающем кэшировании порядок инструкций в Dockerfile определяет, сколько кэша вы реально используете. Золотое правило: копируйте то, что меняется реже, в первую очередь.
Для Node.js-приложения это означает копирование package.json и package-lock.json до остального исходного кода. Запускайте npm install сразу после копирования этих файлов. Затем копируйте код приложения. При таком порядке установка зависимостей будет запускаться заново только когда зависимости действительно меняются, а не при изменении одной строки в коде приложения.
Тот же принцип применим к любому языку. Python-проекты должны копировать requirements.txt или pyproject.toml первыми. Go-проекты — go.mod и go.sum. Шаблон универсален: отделите стабильные зависимости от изменяющегося кода приложения.
Используйте аргументы сборки для гибкости
Ваш Dockerfile не должен содержать захардкоженные значения, которые меняются между окружениями. Версия базового образа, имя окружения или токен доступа к приватному registry должны приходить извне.
Docker предоставляет ARG для этих целей. Вы определяете плейсхолдер в Dockerfile, а пайплайн подставляет реальное значение во время сборки.
ARG BASE_IMAGE_VERSION=20.04
FROM ubuntu:${BASE_IMAGE_VERSION}
В пайплайне передаёте актуальное значение:
docker build --build-arg BASE_IMAGE_VERSION=22.04 .
Это сохраняет ваш Dockerfile обобщённым и переиспользуемым. Один Dockerfile может обслуживать сборки для разработки, стейджинга и продакшена без дублирования.
Обеспечьте воспроизводимость сборок
Воспроизводимость означает, что сборка одного и того же исходного кода в разное время даёт один и тот же образ. Без этого вы не можете доверять стратегии отката, аудиту или сканированию безопасности.
Три вещи чаще всего нарушают воспроизводимость.
Во-первых, использование тегов latest для базовых образов. Тег latest меняется со временем. Сегодняшний latest — это не завтрашний latest. Фиксируйте базовый образ на конкретной версии, например ubuntu:22.04 или node:20-alpine.
Во-вторых, отсутствие фиксации версий зависимостей. Ваш package.json может указывать диапазон версий, например ^4.0.0. Этот диапазон со временем разрешается в разные версии. Используйте lock-файлы, такие как package-lock.json или yarn.lock, чтобы заморозить точные версии.
В-третьих, встраивание временных меток сборки или метаданных в образ. Если процесс сборки записывает текущую дату в файл внутри образа, этот файл будет отличаться между сборками, даже если исходный код идентичен. Избегайте этого, если нет конкретной операционной причины.
Сохраняйте образ в registry
После того как пайплайн собрал образ, ему нужно место, откуда серверы и кластеры Kubernetes смогут его забрать. Это место — контейнерный registry.
Ваш пайплайн должен тегировать образ осмысленными идентификаторами. Распространённый паттерн — использовать SHA коммита как основной тег, с дополнительными тегами для веток или семантических версий.
docker tag myapp:${COMMIT_SHA} registry.example.com/myapp:${COMMIT_SHA}
docker push registry.example.com/myapp:${COMMIT_SHA}
Это даёт вам постоянную ссылку на каждый когда-либо собранный образ. Вы всегда можете вернуться к любому коммиту и забрать точный образ, который был из него собран.
Практический чек-лист
Перед тем как запустить сборку Docker в пайплайне, проверьте:
.dockerignoreисключает папки зависимостей, кэш и.git- Dockerfile копирует файлы зависимостей до кода приложения
- Теги базовых образов зафиксированы на конкретных версиях, а не
latest - Версии зависимостей зафиксированы с помощью lock-файлов
- Для значений, зависящих от окружения, используются аргументы сборки
- Пайплайн сохраняет кэш слоёв Docker между запусками
- Образы тегируются SHA коммита и пушатся в registry
Что это значит для вашего пайплайна
Сборка образов в пайплайне — это не просто запуск docker build на другой машине. Это проектирование Dockerfile и пайплайна так, чтобы они работали вместе. Хорошо структурированный Dockerfile, учитывающий порядок слоёв и контекст сборки, будет собираться быстрее. Пайплайн, который сохраняет кэш и правильно тегирует образы, даст вам надёжные, воспроизводимые артефакты.
Образ, собранный сегодня, должен быть тем же самым образом, который вы сможете пересобрать через шесть месяцев из того же коммита. Такая согласованность делает деплои предсказуемыми, а откаты — безопасными.