1. Інструменти спостереження
На минулій лекції ми зафіксували важливу думку: docker build — це ланцюжок кроків, а кеш може заощадити багато часу. Але сам кеш невидимий, доки ви не навчитеся його «бачити». Без цього будь-яка «оптимізація Dockerfile» перетворюється на ворожіння: ви щось змінили, стало «ніби швидше», а через тиждень усе знову гальмує — і ви вже не розумієте чому.
У цій лекції в нас два інструменти спостереження: build output (вивід docker build) і docker image history (історія шарів готового образу). Перший показує, що відбувалося прямо зараз під час збирання: які кроки повторно використовувалися, а які виконувалися заново. Другий показує, з яких кроків і «шматочків» складається вже зібраний образ і який внесок кожен крок зробив у розмір.
Щоб зафіксувати в голові ролі, ось дуже проста схема:
%% Ролі двох інструментів: що бачимо під час збирання і що бачимо після нього flowchart LR A[Dockerfile] --> B[docker build] B --> C[Build output: що відбулося під час цього збирання] B --> D[Docker image] D --> E[docker image history: як влаштований готовий образ]
Важливе правило дня звучить майже нудно, але економить години життя: спочатку спостерігаємо, потім оптимізуємо. Інакше ви будете «лагодити» не те місце, яке насправді гальмує.
2. Build output: читаємо вивід docker build
Спершу вивід docker build виглядає як балакучий робот, який розповідає взагалі все підряд, навіть те, про що ви не питали. Але в цьому шумі є кілька сигналів, які буквально показують: «ось тут був cache hit», «ось тут кеш зламався», «ось тут ми платимо часом», «а ось тут випадково тягнемо в збирання майже весь репозиторій».
У цій секції ми навчимося дивитися на build output не як на «лякаючий потік тексту», а як на діагностичний інструмент. Наша мета — за одним-двома фрагментами виводу розуміти, чому збирання швидке або чому воно раптом знову стало «як уперше».
Щоб не відволікатися на суфікс версії в імені артефакту, виконуваний bootJar далі будемо умовно називати app.jar; у цій лекції нам важливіша логіка збирання і розмір шарів.
Як зробити build output читабельним
Сучасний Docker часто використовує BuildKit і за замовчуванням робить вивід так, щоб усе «виглядало красиво». Краса приємна, але коли ви навчаєтеся, важливіші читабельність і явні маркери кешу. Тому в навчальному режимі зручно вмикати більш «плоский» вивід, де явно видно CACHED і кроки читаються зверху вниз.
Ось команда, якою зручно користуватися на курсі, щоб побачити максимально зрозумілий вивід:
# --progress=plain робить вивід лінійним і добре показує маркери кешу (наприклад, CACHED)
# -t задає тег образу: цей самий тег потім використовуємо в docker image history
# Крапка в кінці — поточна папка, тобто build context
docker build --progress=plain -t docker-java-catalog-service:day5 .
Типовий фрагмент виводу в такому режимі виглядає приблизно так (спрощено):
#1 [internal] load build definition from Dockerfile
#2 [internal] load .dockerignore
#3 [internal] load build context
#3 transferring context: 3.4MB
#4 [1/5] FROM eclipse-temurin:25-jdk
#5 [2/5] WORKDIR /app
#5 CACHED
#6 [3/5] COPY . .
#7 [4/5] RUN ./gradlew bootJar
#8 [5/5] ENTRYPOINT ["java","-jar","app.jar"]
Сенс простий: ви бачите послідовність кроків і, головне, місця, де Docker зміг написати CACHED, а де — ні. Якщо один раз привчити себе запускати збирання з читабельним виводом, далі проблеми ловляться значно швидше — без магії і без «ну воно дивно себе поводить».
Де побачити cache hit / cache miss
Найважливіший маркер у build output — це чесне CACHED. Якщо крок позначений як CACHED, Docker знайшов відповідний результат у кеші й не виконував цей крок заново. Якщо CACHED немає, значить крок реально виконувався, і часом ви платили саме тут. Це вже майже готова діагностика, причому без психологічної травми.
Уявіть, що ви запускаєте docker build двічі поспіль і нічого в проєкті не змінюєте. У другий раз нормальна картина зазвичай така: майже всі кроки мають стати CACHED. Якщо ж важкі кроки виконуються щоразу, це майже завжди означає одне з двох: або ви справді змінюєте вхідні дані, іноді самі того не помічаючи, або Dockerfile влаштований так, що ранній крок постійно робить недійсним кеш нижче по ланцюжку.
У навчанні ми дивимося на це по-інженерному. Якщо після мікроскопічної правки в src/ у вас знову виконується RUN ./gradlew ..., це не «Docker вас особисто не любить», а сигнал: вхідні файли для цього RUN змінилися. Наступне інженерне запитання тут уже напрошується саме: що саме зламало кеш вище по ланцюжку — найчастіше який COPY стоїть перед цим RUN. Але зараз наш фокус простіший — навчитися побачити сам факт.
На що дивитися насамперед
Є типова пастка новачка: відкрити вивід docker build, побачити 200 рядків і чесно спробувати прочитати все. Намір шляхетний, але на практиці він швидко ламає психіку. Для сьогоднішньої мети достатньо звикнути до кількох «маячків», які вихоплюються очима майже автоматично.
Нижче — невелика таблиця «сигнал → про що говорить». Це не чекліст виправлення, а саме переклад виводу в зрозумілий сенс:
| Що ви побачили в build output | Що це зазвичай означає | Чому це важливо саме сьогодні |
|---|---|---|
|
У build context потрапило занадто багато файлів | Кеш тут не допоможе — ви повільно стартуєте кожне збирання |
на ранніх кроках, але важкий не закешований |
Кеш ламається до важкого кроку | Отже, десь вище є крок, який часто змінюється |
займає 40–120 секунд |
Ви платите часом за збирання або залежності | Це те місце, де правильна структура Dockerfile дає величезний ефект |
стоїть перед важким |
Будь-яка зміна в проєкті ламає кеш | Класичне джерело «перезбирається взагалі все» |
Сама звичка «дивитися на кілька рядків» — уже 80% успіху. А далі підключимо другий інструмент: історію шарів, щоб побачити, що саме опинилося всередині образу і де росте розмір.
3. docker image history: структура образу
Build output — це репортаж про те, що відбувалося під час збирання. Але іноді потрібно інше: подивитися на готовий образ і зрозуміти, які кроки та шари в ньому реально присутні. Тут і з’являється docker image history — команда, яка показує «чек» із магазину під назвою Dockerfile: з яких позицій складається образ і скільки кожна з них важить.
Ця команда особливо корисна, коли у вас одночасно є два відчуття. Перше: «збирання повільне». Друге: «і образ якийсь підозріло товстий». Історія шарів допомагає перестати обговорювати відчуття і перейти до спостережуваних фактів: який крок дав найбільший шар і які кроки взагалі не додають даних, а лише метадані.
Як запускати docker image history
Найпростіша команда виглядає так:
# Дивимося історію шарів саме того тегу, який щойно зібрали
docker image history docker-java-catalog-service:day5
Тут важливо, щоб ви вказували той самий тег, який щойно зібрали. Це звучить очевидно, але в реальному житті люди часто дивляться історію «якогось іншого» образу, бо минулого разу тег був :latest, а сьогодні — :day5, і починаються танці з бубном.
Іноді корисно додати --no-trunc, щоб побачити команди повністю, без обрізання:
# --no-trunc корисний, коли Docker обрізав довгі команди в CREATED BY
docker image history --no-trunc docker-java-catalog-service:day5
Ми не будемо перетворювати лекцію на енциклопедію прапорців. Але ось цей прапорець — хороше навчальне підсилення: він допомагає побачити, що саме було в RUN, COPY та інших кроках, особливо коли Docker скорочує рядки.
Як читати колонки history
Вивід docker image history зазвичай показує кілька колонок, серед яких нас найбільше цікавлять CREATED BY і SIZE. У спрощеному вигляді це може виглядати так:
IMAGE CREATED BY SIZE
<id> ENTRYPOINT ["java","-jar","app.jar"] 0B
<id> RUN /bin/sh -c ./gradlew bootJar 260MB
<id> COPY . . 45MB
<id> WORKDIR /app 0B
<id> FROM eclipse-temurin:25-jdk 390MB
Кілька важливих спостережень, які знімають масу плутанини.
Рядки з 0B не означають, що команда «не спрацювала». Це означає, що крок додав метадані, але не додав файлів у файлову систему шару. Типовий приклад — WORKDIR, CMD, ENTRYPOINT, EXPOSE. Вони важливі, але не роздувають образ.
Великі числа в SIZE показують, скільки даних додав конкретний крок. І саме тут часто трапляється «ага-момент». Наприклад, ви можете побачити, що найтовстіший шар — це COPY . ., і раптом виявиться, що ви тягнете в образ щось важке: випадково закомічений архів, локальні звіти або каталог, який треба було ігнорувати через .dockerignore.
Ще один нюанс: базовий образ (FROM ...) теж має розмір, і він часто виглядає «величезним». Сьогодні ми не обговорюємо вибір базового образу (це окрема тема), тому сприймаємо це як даність і фокусуємося на тому, що ми додали зверху.
Щоб зв’язати в голові Dockerfile і history, зручно запам’ятати просту табличку «створює шар чи ні» (без занудства, лише робочий мінімум):
| Інструкція Dockerfile | Зазвичай створює шар із даними? | Як проявляється в docker image history |
|---|---|---|
| RUN ... | Так | Майже завжди помітний SIZE, іноді дуже великий |
| COPY ... | Так | SIZE показує, скільки реально скопіювали |
| ADD ... | Так | Схоже на COPY, але зі своїми особливостями (поки не чіпаємо) |
| WORKDIR | Ні (метадані) | Часто 0B |
ENTRYPOINT, , |
Ні (метадані) | Часто 0B |
Головне: history — це не «довідка заради довідки». Це спосіб швидко відповісти на питання: «який крок робить образ товстим» і «яка частина Dockerfile реально несе дані».
4. Звичка: build output + history
Є ризик вивчити дві команди (docker build і docker image history) та користуватися ними як окремими «магічними заклинаннями». Але нам потрібен інший результат: короткий, повторюваний підхід, який перетворює збирання образу з містики на інженерний процес. Тоді ви будете розбирати Dockerfile так само спокійно, як розбираєте stacktrace в Java: «ага, помилка ось тут, причина ось тут, лікуємо ось це».
Логіка дня така: спочатку ми дивимося на build output, щоб зрозуміти, які кроки виконувалися, а які взялися з кешу. Потім дивимося на history, щоб зрозуміти, які кроки додають багато даних і потенційно роздувають розмір. І лише після цього робимо висновки про те, що саме потрібно перебудувати, — не «на око», а за спостережуваними сигналами.
Можна думати про це як про два питання, які ви ставите собі щоразу.
Перше питання — про час: «Яка команда реально виконувалася заново і скільки вона зайняла?» Відповідь приходить із виводу docker build.
Друге питання — про структуру і розмір: «Який крок додав в образ найбільше даних?» Відповідь приходить із docker image history.
Щоб було простіше втримати це в голові, ось маленька шпаргалка у вигляді схеми:
%% Шпаргалка: спочатку час (build output), потім структура/розмір (history)
flowchart TD
A[Зібрали image] --> B[Дивимося build output]
B --> C{Є важкі кроки без CACHED?}
C -->|Так| D[Шукаємо, який ранній крок ламає кеш]
C -->|Ні| E[Збирання ок, переходимо до size]
A --> F[Дивимося docker image history]
F --> G{Є неочікувано великі шари?}
G -->|Так| H[Шукаємо, що копіюємо або створюємо в цьому кроці]
G -->|Ні| I[Розмір виглядає розумним для цього етапу]
Зверніть увагу: ми поки не лікуємо проблему до кінця. Спочатку важливо навчитися робити головне — не сперечатися з Docker, а дивитися на факти.
5. Міні-розбір: ефект «перше збирання»
Зараз найприємніше: беремо наш навчальний сервіс і використовуємо обидві команди як інструменти розслідування. Це не «лабораторна робота», а просто демонстрація нормального інженерного мислення. У реальній команді саме так і обговорюють Dockerfile: не «у мене повільно», а «ось крок, він не закешований, ось шар, він товстий, отже проблема тут».
Для демонстрації візьмемо навмисно наївний Dockerfile, який часто з’являється як «перша доросла спроба» зібрати Java/Gradle-проєкт прямо всередині docker build:
# Базовий шар: приносить JDK і систему з образу temurin
FROM eclipse-temurin:25-jdk
# Далі всі команди виконуватимуться в /app усередині образу
WORKDIR /app
# Копіюємо весь репозиторій в образ (і це ключова причина проблем із кешем)
COPY . .
# Запускаємо збирання прямо всередині образу (часто тягне кеші та артефакти всередину шарів)
RUN ./gradlew bootJar
Шлях запуску тут навмисно опущено: для розслідування нам потрібен саме широкий COPY перед важким RUN.
Так, він короткий. Так, він зрозумілий. І саме тому він небезпечний: своєю «простотою» він ховає проблему, яка проявляється на повторному збиранні.
Дивимося build output
Збираємо образ:
# Для діагностики використовуємо плоский вивід, щоб одразу бачити, де CACHED, а де ні
docker build --progress=plain -t docker-java-catalog-service:day5 .
Припустімо, ви бачите спрощено таку картину:
#6 [3/4] COPY . .
#6 DONE 0.3s
#7 [4/4] RUN ./gradlew bootJar
#7 DONE 68.4s
На першому збиранні це може виглядати «нормально»: залежності завантажилися, bootJar зібрався, усе чесно.
Тепер робимо маленьку зміну. Наприклад, правимо один лог у src/main/java/... або навіть редагуємо README.md — і це ще підступніше: код не змінювали, а збирання знову довге. Запускаємо docker build вдруге й бачимо:
#6 [3/4] COPY . .
#6 DONE 0.3s
#7 [4/4] RUN ./gradlew bootJar
#7 DONE 67.9s
І ось ключовий момент: жодного CACHED на RUN. Docker чесно говорить: «я знову виконував збирання».
На рівні сьогоднішньої лекції нам важливо не лікувати, а діагностувати. Діагноз простий: крок RUN ./gradlew bootJar щоразу виконується заново, тому що перед ним стоїть COPY . ., який залежить майже від будь-якої зміни в репозиторії. Ми «випадково» зробили збирання залежним від усього проєкту.
Тепер уже добре видно наступне інженерне питання: що саме в COPY так каскадно ламає кеш і як зсунути цю межу нижче.
Дивимося docker image history
Тепер, коли ми зрозуміли картину за часом, перевіримо структуру образу:
# Дивимося історію саме того тегу, який щойно зібрали
docker image history docker-java-catalog-service:day5
І побачимо щось схоже (сильно спрощене):
IMAGE CREATED BY SIZE
<id> RUN /bin/sh -c ./gradlew bootJar 260MB
<id> COPY . . 45MB
<id> WORKDIR /app 0B
<id> FROM eclipse-temurin:25-jdk 390MB
Тут добре видно дві речі.
По-перше, COPY . . додав відчутний шар. Якщо у вас там несподівано сотні мегабайт, це майже завжди означає проблеми з .dockerignore (наприклад, ви копіюєте build/, .git/, .idea/ або щось інше важке). Ми .dockerignore уже знаємо, тому це сигнал: «перевірити, що саме потрапило в context».
По-друге, RUN ./gradlew bootJar додав величезний шар. І навіть якщо ви поки не знаєте всіх причин, це вже факт: після RUN усередині образу зʼявилося багато даних. Наївний Dockerfile часто затягує всередину образу Gradle-кеші й результати збирання, тому що він робить усе в одній файловій системі. Сьогодні ми лише вчимося це бачити. Лікувати будемо поетапно, не перестрибуючи через сходинки.
Отже, на цьому прикладі ми отримали повну картину: build output показав, що перебудова довга через RUN, а history показав, що цей RUN ще й додає багато даних у підсумковий образ. Від цього моменту обговорення вже не про «смаки», а про інженерну діагностику.
6. Типові помилки під час читання build output і docker image history
У фіналі спокійно проговорімо найчастіші граблі. Вони особливо прикрі, тому що майже всі виглядають як «Docker дивний», хоча насправді проблема в тому, що ми або не туди подивилися, або не так прочитали дані. Якщо почнете уникати цих помилок уже з п’ятого дня, далі в курсі буде помітно легше.
Помилка №1: дивитися docker image history не того образу.
Це трапляється постійно: ви зібрали docker-java-catalog-service:day5, а історію дивитеся в docker-java-catalog-service:latest або в старого тегу. У результаті ви «досліджуєте» минуле, а не поточне збирання, і робите хибні висновки. Звичка проста: зібрали образ — одразу скопіювали тег із команди й вставили його в docker image history.
Помилка №2: читати лише розмір і ігнорувати build output.
docker image history показує структуру і розмір, але не показує, де ви втрачаєте час і чому кеш не спрацював. Буває, що шар за розміром невеликий, але крок RUN займає хвилину: наприклад, через завантаження залежностей, прогін тестів або компіляцію. Якщо дивитися лише на SIZE, легко оптимізувати «товсте», але рідко перебудовуване, і не помітити «тонке», але дороге за часом.
Помилка №3: читати лише build output і ігнорувати docker image history.
Зворотна крайність теж трапляється. Ви побачили, що RUN виконується заново, і починаєте «крутити» Dockerfile, але не перевіряєте, що при цьому випадково копіюєте в образ сміття або роздуваєте шар. Підсумок: збирання стало швидшим, але образ виріс, а потім у вас Docker Desktop починає скаржитися на диск. Доросла звичка — дивитися і на час, і на структуру.
Помилка №4: панікувати через шари 0B.
Новачки іноді сприймають 0B як «команда не спрацювала». Насправді WORKDIR, ENTRYPOINT, CMD, EXPOSE найчастіше просто записують метадані й не додають файлів. Це нормально. Якби вони додавали дані, Dockerfile перетворювався б на невеликий генератор сміття.
Помилка №5: намагатися лікувати все прапорцем --no-cache.
Прапорець --no-cache корисний, коли ви хочете примусово перебудувати все, але в навчальній задачі про кеш він частіше заважає зрозуміти, що відбувається. Ви вимикаєте кеш і потім дивуєтеся, що «нічого не кешується». Це приблизно як у Java вимкнути JIT і сказати, що JVM «не вміє оптимізувати». У нашій темі краще навпаки: увімкнути читабельний вивід і навчитися бачити CACHED.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ