JavaRush /Курси /Docker for Spring /Читання шарів: build output і history

Читання шарів: build output і history

Docker for Spring
Рівень 5 , Лекція 1
Відкрита

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 Що це зазвичай означає Чому це важливо саме сьогодні
transferring context: 800MB
У build context потрапило занадто багато файлів Кеш тут не допоможе — ви повільно стартуєте кожне збирання
CACHED
на ранніх кроках, але важкий
RUN
не закешований
Кеш ламається до важкого кроку Отже, десь вище є крок, який часто змінюється
RUN ./gradlew ...
займає 40–120 секунд
Ви платите часом за збирання або залежності Це те місце, де правильна структура Dockerfile дає величезний ефект
COPY . .
стоїть перед важким
RUN
Будь-яка зміна в проєкті ламає кеш Класичне джерело «перезбирається взагалі все»

Сама звичка «дивитися на кілька рядків» — уже 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,
CMD
,
EXPOSE
Ні (метадані) Часто 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.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ