Image hygiene

Docker for Spring
24 уровень , 3 лекция
Открыта

1. Image hygiene: смысл

Когда слышишь «гигиена образа», мозг новичка часто рисует одну картинку: мы садимся на диету, выкидываем из образа всё подряд и празднуем минус 200 мегабайт. В реальности image hygiene — это не бодибилдинг, а скорее чистая кухня: чтобы вы могли готовить быстро, повторяемо и не отравиться случайной «приправой», которую кто-то оставил на столе ещё неделю назад.

Самый важный смысл hygiene в нашем курсе — сделать образ предсказуемым. Предсказуемость начинается не с «самый маленький Linux», а с того, что вы понимаете, откуда берутся файлы в сборке, почему образ на вашей машине совпадает с образом у коллеги, и почему сборка не ломается от мелочи вроде «кто-то положил в папку data/exports/ огромный CSV».

Чтобы было проще удерживать картину, полезно думать о hygiene как о трёх уровнях: чистый build context (что отправили на сборку), чистый runtime image (что осталось в финальном слое) и чистые ссылки на базу (какие базовые образы вы используете и насколько они дрейфуют во времени). Всё это дополняется четвертым «ускорителем здравого смысла» — быстрыми проверками, чтобы ловить ошибки раньше.

Небольшая таблица для ориентира:

Слой гигиены Вопрос, который мы задаём Типичный «запах» проблемы
Build context «Что именно Docker увидит при docker build Контекст 500MB, случайно уехал .env, сборка тормозит
Runtime image «Что реально попало в финальный образ?» В runtime лежат исходники/gradle/временные файлы
Base images «Насколько воспроизводима база?» Сегодня работает, через неделю внезапно ломается
Быстрые checks «Можно ли поймать проблему без полного build?» Ошибку видим только после долгой сборки

2. Build context: состав

С build context здесь важно уже не знакомство, а ревизия. На финальном шаблоне именно через него в сборку чаще всего уезжают вещи, которые делают image непредсказуемым. Docker всё равно забирает контекст целиком, поэтому лишние build/, .gradle/, IDE-файлы, экспорты и локальные secret-файлы бьют и по скорости, и по воспроизводимости.

Для Java/Gradle проекта это особенно заметно: контекст распухает быстро, а случайный COPY . . потом запекает внутрь образа то, что вообще не должно было участвовать в build. Поэтому build context на этом этапе смотрят не как на теорию из начала курса, а как на быстрый hygiene-аудит: что реально видит Docker и что мы точно не хотим отправлять в сборку.

В Container-Ready Catalog Service контекстом остаётся корень репозитория, и этого достаточно. Но это работает только вместе со строгим .dockerignore.

3. .dockerignore: фильтрация контекста

На финальном шаблоне .dockerignore работает как последний фильтр перед build. Он не обязан быть огромным, но обязан отсекать build-мусор, runtime-данные и локальные секреты, которые не должны даже появляться в контексте.

# Кэш Gradle и артефакты сборки — в контекст не тащим
.gradle/
build/

# Настройки IDE — не нужны для сборки
.idea/
*.iml

# Git-метаданные и мусор ОС
.git/
.DS_Store

# Секреты и локальные runtime-конфиги
.env
config/application-secrets*.yml
secrets/

# Runtime-экспорты
data/exports/*
!data/exports/.gitkeep

Заметьте, что мы исключаем реальные secret-файлы и runtime-данные, а не их шаблоны. .env.example и application-secrets.yml.example можно держать в репозитории, а .env, локальные application-secrets*.yml и каталог secrets/ в build context не нужны вообще.

И наоборот: gradlew, папка gradle/, build scripts и исходники трогать нельзя. .dockerignore полезен ровно тогда, когда он режет лишнее, а не ломает builder stage.

4. Базовый образ: tag vs digest

Когда мы говорим «образ на базе eclipse-temurin:25-jre-jammy», кажется, что это конкретная вещь. На практике тег (tag) — это скорее указатель, а не железобетонный объект. Сегодня он может указывать на один набор слоёв, через месяц — на другой (например, после security‑апдейта или пересборки базового образа). И это нормально для индустрии. Ненормально — если вы хотите воспроизводимость, а используете только плавающие указатели и потом удивляетесь, почему в четверг всё работало, а в понедельник «вдруг нет».

Digest — это уже «точный адрес». Он выглядит как @sha256:... и обозначает конкретный immutable-артефакт. Если вы фиксируете базу через digest, вы получаете сборку «как по ГОСТу»: тот же вход — тот же результат. Это очень полезно для учебного шаблона и для командной разработки, где «у меня работает» не должно зависеть от дня недели.

Для контекста (без углубления в supply-chain трек) можно запомнить простое правило. Тег — удобен, когда вы читаете Dockerfile глазами. Digest — полезен, когда вы хотите, чтобы Dockerfile не превращался в лотерею. В реальной команде часто делают компромисс: настраивают политику «обновляем digest осознанно», а не «он дрейфует сам».

Мини‑пример Dockerfile‑фрагмента, где база runtime stage закреплена digest’ом, выглядит так:

# Закрепляем базу не только тегом, но и digest'ом для воспроизводимости
FROM eclipse-temurin:25-jre-jammy@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

# Рабочая директория внутри контейнера
WORKDIR /app

# Копируем только собранный артефакт приложения
COPY application.jar application.jar

# Запускаем сервис как java -jar
ENTRYPOINT ["java", "-jar", "application.jar"]

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

Тот же принцип относится и к Compose-окружению: PostgreSQL, Redis, RabbitMQ тоже живут в виде образов, и если вы оставляете слишком плавающие версии, то «случайно» меняете инфраструктуру под ногами. Но мы здесь удерживаем границу курса: нам достаточно понимать логику tag vs digest, а не строить полноценную supply-chain систему.

Как получить digest

Обычно первый вопрос после разговора про digest очень приземлённый: «Окей, а где его взять? Я же не буду придумывать sha256 на глаз». Не будете, и это правильно: digest берут из Docker, а не из вдохновения.

Самый простой Junior-friendly путь — сначала явно скачать образ по тегу, а потом посмотреть, какой digest у него в локальном кеше. Выглядит это примерно так:

# Скачиваем образ по тегу (чтобы он появился в локальном кеше)
docker pull eclipse-temurin:25-jre-jammy

# Смотрим, какой immutable digest оказался у скачанного образа
docker image inspect eclipse-temurin:25-jre-jammy \
  --format='{{index .RepoDigests 0}}'

# Вывод будет вида: eclipse-temurin@sha256:7f3c... (пример)

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

Ещё один практический нюанс, который иногда всплывает у студентов со смешанными платформами (например, кто-то на amd64, кто-то на arm64). Если вы зафиксировали digest, который относится к конкретной архитектуре, на другой архитектуре образ может не подтянуться так, как вы ожидаете. Поэтому digest — это инструмент дисциплины, но пользоваться им нужно осознанно: если ваша команда реально mixed-platform, стоит проверять, что выбранная фиксация работает в обоих мирах. Мы не уходим в multi-platform pipeline, но «проверить, что не отстрелили себе ногу» — это уже часть гигиены.

5. Dockerfile check: buildx --check

В какой-то момент взрослой разработки приходит мудрость: лучше получить маленький красный флажок за 10 секунд, чем большой красный пожар через 10 минут. docker buildx build --check — это именно про такую профилактику. Он не заменяет реальный build и запуск контейнера, но помогает быстро понять, что Dockerfile хотя бы валиден и не содержит очевидных проблем.

Проще всего воспринимать это как быстрый линтер/валидатор Dockerfile. Он проверяет синтаксис, структуру, корректность некоторых инструкций и может выдавать предупреждения по части распространённых anti-pattern’ов. Точный набор проверок может отличаться между версиями Docker/buildx (инструменты развиваются), поэтому к нему стоит относиться как к «быстрому сигналу», а не как к судебной экспертизе.

Команды выглядят максимально просто, и это хорошо:

# Быстро проверяем Dockerfile на валидность (без реальной сборки и без запуска)
docker buildx build --check .

# То же самое, но для конкретного stage (например, runtime)
docker buildx build --check --target runtime .

Вторая команда особенно полезна, если у вас в Dockerfile много stages (builder, development, runtime, layered и т.д.), и вы хотите проверить конкретный путь. Это аккуратно ложится на наш общий стиль курса: один Dockerfile, несколько stages, и вы управляете ими явно.

Очень важно не перепутать смысл. --check не докажет, что приложение стартует, подключается к БД, пишет в export directory и отвечает на HTTP. Это всё проверяется запуском контейнера и нашими smoke‑сценариями. Но --check отлично ловит «глупые» ошибки раньше: опечатки, некорректные ссылки на stage, странные места, где Dockerfile не парсится или не собирается логически. И именно на финальном дне курса такой быстрый «санитарный осмотр» очень экономит время.

Небольшая схема, как этот check вписывается в общий pipeline, выглядит так:

flowchart TD
    %% Быстрый sanity-check Dockerfile перед тяжёлой сборкой и запуском
    A[Правим Dockerfile / .dockerignore] --> B[docker buildx build --check]
    B -->|OK| C[docker build ...]
    C --> D[docker run / docker compose up]
    D --> E[smoke-check: logs + health + API]
    B -->|ошибка| F[правим раньше, чем начнём долго собирать]

Тут логика простая: сначала быстро проверяем, что «документ не сломан», потом уже делаем тяжёлую работу, потом запускаем и проверяем поведение.

6. Мини-гигиена финального образа

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

В практическом смысле мини-гигиена финального образа — это когда вы убираете всё, что делает сборку случайной: лишний мусор в context, плавающие базы, dev-настройки в runtime-path и бесконечные Dockerfile-варианты. Вместо этого вы делаете один понятный путь, который читается глазами: вот контекст, вот сборка, вот база, вот проверка.

Очень здорово, что мы уже раньше приняли multi-stage как канон: builder stage может быть хоть «грязным» (там Gradle, исходники, кэш), а runtime stage остаётся чистым. Это само по себе гигиенично. Но именно сегодня мы доводим дисциплину до состояния «шаблон можно копировать в новый проект без стыда».

И ещё один момент, который часто недооценивают: гигиена — это не только про Dockerfile, но и про “окружение вокруг”. Если вы держите в репозитории .env.example, а .env не коммитите и не отправляете в build context, вы не просто «соблюдаете правила», вы делаете проект удобным для команды. Если вы фиксируете базовые образы осознанно, вы снижаете вероятность того, что в один день у вас «вдруг» изменится поведение JVM внутри контейнера из-за обновления базы. И это уже не теория: такие вещи регулярно происходят в реальных проектах, просто не всегда очевидно, что причина была в базе, а не в коде.

7. Типичные ошибки при наведении image hygiene

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

Ошибка №1: в .dockerignore случайно выкинули то, что нужно builder stage.
Это классика: человек увидел папку gradle/ или файл gradlew и подумал «ну это же какой-то мусор для сборки, давайте уберём». А потом builder stage не может запустить ./gradlew bootJar. Симптом обычно простой: сборка падает на раннем шаге с ошибкой “file not found” или “permission denied”. Лечится не магией, а проверкой: что реально нужно копировать в builder stage и что вы исключили из контекста.

Ошибка №2: .env или локальный конфиг попал в build context «ну просто так получилось».
Даже если вы не делаете COPY . ., сам факт, что секретный файл присутствует в контексте, — плохая привычка. Сегодня вы его не копируете, завтра кто-то сделает рефакторинг и случайно утащит внутрь образа. Это типичная «медленная авария»: она не ломает сборку сразу, но создаёт риск baked secret, который потом очень сложно “вытащить обратно” из истории образов.

Ошибка №3: базовый образ задан через плавающий тег, а вы ожидаете железобетонной воспроизводимости.
Если вы используете условный :latest или слишком общий тег и при этом хотите, чтобы «всегда собиралось одинаково», вы играете в лотерею. Иногда это проявляется как «на моём ноутбуке работает, на CI сломалось», иногда — как «в понедельник начали падать тесты, хотя код не трогали». Причина может быть в тихом обновлении базы.

Ошибка №4: digest зафиксировали, но не подумали о смешанных платформах.
На одной машине всё хорошо, на другой вдруг образ не подтягивается или подтягивается не так, как ожидается. Часто это всплывает у студентов с Mac на arm64, когда шаблон тестировался на amd64. Мы не делаем из курса multi-platform build track, но правило простое: если ваша команда реально mixed-platform, проверяйте, что фиксация базы не стала «только для одной архитектуры».

Ошибка №5: docker buildx build --check воспринимают как замену реальному запуску.
Иногда после успешного --check хочется поставить галочку «готово» и бежать дальше. Но check не проверяет, что приложение стартует, что профили читаются, что порт проброшен, что health endpoint жив, что export directory writable. Он проверяет Dockerfile как документ, а не сервис как систему. Поэтому --check — это профилактика, а не медкомиссия перед Олимпиадой.

1
Задача
Docker for Spring, 24 уровень, 3 лекция
Недоступна
Runtime base image, закреплённый по digest
Runtime base image, закреплённый по digest
1
Задача
Docker for Spring, 24 уровень, 3 лекция
Недоступна
Аудит build context через `.dockerignore`
Аудит build context через `.dockerignore`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ