1. Гігієна образів: суть
Коли чуєш «гігієна образу», у новачка в голові часто виникає одна картина: ми сідаємо на дієту, викидаємо з образу все підряд і радіємо мінус 200 МБ. Насправді image hygiene — це не бодибілдинг, а радше чиста кухня: ви готуєте швидко, відтворювано й без ризику «отруїтися» випадковою приправою, яку хтось залишив на столі ще тиждень тому.
Найважливіше в гігієні образів у нашому курсі — зробити образ передбачуваним. Передбачуваність починається не з «найменший Linux», а з того, що ви розумієте, звідки беруться файли під час збирання, чому образ на вашій машині збігається з образом у колеги й чому збирання не ламається через дрібницю на кшталт «хтось поклав у папку data/exports/ величезний CSV».
Щоб було простіше втримувати цілісну картину, корисно думати про hygiene як про три рівні: чистий build context (що відправили на збирання), чистий runtime image (що залишилося у фінальному шарі) і чисті посилання на базу (які базові образи ви використовуєте та наскільки вони дрейфують із часом). Усе це доповнює четвертий «прискорювач здорового глузду» — швидкі перевірки, які допомагають ловити помилки раніше.
Невелика таблиця для орієнтиру:
| Шар гігієни | Питання, яке ми ставимо | Типовий «запах» проблеми |
|---|---|---|
| Build context | «Що саме Docker побачить під час docker build?» | Контекст 500 МБ, випадково потрапив .env, збирання гальмує |
| Runtime image | «Що насправді потрапило до фінального образу?» | У runtime-образі лежать вихідний код, Gradle і тимчасові файли |
| Base images | «Наскільки відтворюваною є база?» | Сьогодні працює, а через тиждень раптово ламається |
| Швидкі checks | «Чи можна зловити проблему без повного build?» | Помилку бачимо лише після тривалого збирання |
2. Build context: склад і ревізія
З build context тут важлива вже не ознайомленість, а ревізія. На фінальному шаблоні саме через нього до збирання найчастіше потрапляють речі, які роблять образ непередбачуваним. Docker усе одно забирає контекст цілком, тому зайві build/, .gradle/, файли IDE, експорти й локальні файли-секрети б’ють і по швидкості, і по відтворюваності.
Для Java/Gradle-проєкту це особливо помітно: контекст швидко розростається, а випадковий COPY . . потім запікає всередині образу те, що взагалі не мало брати участі у build. Тому build context на цьому етапі розглядають не як теорію з початку курсу, а як швидкий аудит гігієни: що насправді бачить 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/, скрипти збирання та вихідний код чіпати не можна. .dockerignore корисний рівно тоді, коли він відрізає зайве, а не ламає builder stage.
4. Базовий образ: tag vs digest
Коли ми кажемо «образ на базі eclipse-temurin:25-jre-jammy», здається, ніби це цілком конкретна річ. Насправді тег (tag) — це радше вказівник, а не залізобетонний об’єкт. Сьогодні він може вести до одного набору шарів, а через місяць — до іншого, наприклад після security-оновлення або перескладання базового образу. І це нормально для індустрії. Ненормально — якщо ви хочете відтворюваності, але користуєтеся лише плаваючими вказівниками, а потім дивуєтеся, чому в четвер усе працювало, а в понеділок «раптом ні».
Digest — це вже «точна адреса». Він має вигляд @sha256:... і позначає конкретний immutable-артефакт. Якщо ви фіксуєте базу за допомогою digest, то отримуєте збирання з однаковим результатом за однакових вхідних даних. Це дуже корисно для навчального шаблону і для командної розробки, де «у мене працює» не має залежати від дня тижня.
Для контексту, без заглиблення в ланцюг постачання ПЗ, можна запам’ятати просте правило. Тег — зручний, коли ви читаєте 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, а не будувати повноцінний конвеєр ланцюга постачання.
Як отримати digest
Зазвичай перше питання після розмови про digest дуже приземлене: «Гаразд, а де його взяти? Я ж не буду вигадувати sha256 на око». Не будете, і це правильно: digest беруть із Docker, а не з натхнення.
Найпростіший, зрозумілий для джуна шлях — спочатку явно завантажити образ за тегом, а потім подивитися, який digest опинився в локальному кеші. Виглядає це так:
# Завантажуємо образ за тегом, щоб він з’явився в локальному кеші
docker pull eclipse-temurin:25-jre-jammy
# Дивимося, який незмінний digest опинився в завантаженому образі
docker image inspect eclipse-temurin:25-jre-jammy \
--format='{{index .RepoDigests 0}}'
# Вивід матиме вигляд: eclipse-temurin@sha256:7f3c... (приклад)
Це не єдиний спосіб, але він зрозумілий: ви спочатку фіксуєте, що саме завантажили, а потім бачите незмінний ідентифікатор. Якщо RepoDigests покаже кілька значень, це зазвичай пов’язано з тим, що образ доступний під різними іменами або має кілька посилань. У навчальному проєкті достатньо вибрати перше, а в команді — домовитися про правила вибору й оновлення.
Ще один практичний нюанс, який іноді спливає у студентів зі змішаними платформами (наприклад, хтось на amd64, хтось на arm64). Якщо ви зафіксували digest, який стосується конкретної архітектури, на іншій архітектурі образ може не підтягнутися так, як ви очікуєте. Тому digest — це інструмент дисципліни, але користуватися ним потрібно свідомо: якщо ваша команда справді mixed-platform, варто перевірити, що вибрана фіксація працює в обох середовищах. Ми не йдемо в повноцінний багатоплатформний конвеєр, але перевірити, що ви не нашкодили собі, — це вже частина гігієни.
5. Dockerfile check: buildx --check
У якийсь момент дорослої розробки приходить мудрість: краще отримати маленький червоний прапорець за 10 секунд, ніж великий червоний спалах через 10 хвилин. docker buildx build --check — саме про таку профілактику. Він не замінює реальне збирання та запуск контейнера, але допомагає швидко зрозуміти, що Dockerfile принаймні валідний і не містить очевидних проблем.
Найпростіше сприймати це як швидкий лінтер або валідатор Dockerfile. Він перевіряє синтаксис, структуру, коректність деяких інструкцій і може видавати попередження щодо поширених антипатернів. Точний набір перевірок може відрізнятися між версіями Docker/buildx, тому ставитися до нього слід як до «швидкого сигналу», а не як до судової експертизи.
Команди виглядають максимально просто, і це добре:
# Швидко перевіряємо Dockerfile на валідність — без реального збирання і без запуску
docker buildx build --check .
# Те саме, але для конкретного stage (наприклад, runtime)
docker buildx build --check --target runtime .
Друга команда особливо корисна, якщо у вас у Dockerfile багато етапів (builder, development, runtime, layered тощо), і ви хочете перевірити конкретний шлях. Це акуратно лягає на наш загальний стиль курсу: один Dockerfile, кілька етапів, і ви керуєте ними явно.
Дуже важливо не переплутати призначення. --check не доведе, що застосунок стартує, підключається до БД, пише в export directory і відповідає на HTTP. Усе це перевіряється запуском контейнера та нашими smoke-сценаріями. Але --check чудово ловить дурні помилки раніше: описки, некоректні посилання на stage, дивні місця, де Dockerfile не парситься або не складається логічно. І саме в кінці курсу такий швидкий санітарний огляд дуже заощаджує час.
Невелика схема, як цей check вписується в загальний pipeline, має такий вигляд:
flowchart TD
%% Швидка перевірка здорового глузду для 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 відрізняється від «одного разу запустилося» саме тим, що він виживає після того, як ви вже не пам’ятаєте, чому тут узагалі так написано.
У практичному сенсі міні-гігієна фінального образу — це коли ви прибираєте все, що робить збирання випадковим: зайве сміття в контексті, плаваючі бази, dev-налаштування в середовищі виконання та нескінченні варіанти 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. Ми не робимо з курсу повноцінний багатоплатформний конвеєр, але правило просте: якщо ваша команда справді mixed-platform, перевіряйте, що фіксація бази не стала «лише для однієї архітектури».
Помилка № 5: docker buildx build --check сприймають як заміну реального запуску.
Іноді після успішного --check хочеться поставити галочку «готово» і бігти далі. Але check не перевіряє, що застосунок стартує, що профілі читаються, що порт проброшено, що health endpoint живий, що export directory доступний для запису. Він перевіряє Dockerfile як документ, а не сервіс як систему. Тому --check — це профілактика, а не медкомісія перед Олімпіадою.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ