JavaRush /Курси /Docker for Spring /Build context і .dockerig...

Build context і .dockerignore у Java

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

1. Build context: вхідні файли збірки

Docker не бачить увесь ваш диск — він працює лише з тим, що ви явно передали як build context. Коли ви вперше пишете docker build, легко може здатися, що Docker «просто читає Dockerfile» і далі магічно сам усе знаходить на компʼютері. Це відчуття швидко змінюється здивуванням, а іноді й нервовим сміхом, тому що Docker не має права і не зобовʼязаний бачити весь ваш компʼютер. Він працює лише з тим, що ви йому передали, і саме це називається build context — «контекст збірки».

Уявіть збірку образу як посилку для Docker. Усередину ви кладете файли, які він може використати: Dockerfile, jar, конфіги — що завгодно, але тільки те, що ви справді поклали. Docker не повинен ходити по вашій файловій системі «куди заманеться». Це і про безпеку, щоб випадково не забрати зайве, і про відтворюваність, щоб збірка була однаковою на різних машинах, і просто про практичність: контекст фізично передається в механізм збірки.

Є ще один важливий нюанс, особливо на Docker Desktop під Windows/macOS: Docker-движок часто живе не «прямо у вашій ОС», а в окремому середовищі. Отже, контекст реально передається туди як набір файлів. Навіть якщо ви не копіюєте половину цих файлів в образ, час на їх надсилання та обробку все одно витрачається. Тому контекст — це не філософія, а швидкість вашої роботи.

Наглядно це можна уявити так:

flowchart TD
    A["Папка проєкту на вашій машині"] -->|docker build <context>| B["Build context — набір файлів"]
    B --> C["Docker build engine (daemon / BuildKit)"]
    C --> D["Зібраний Docker image"]

Ключова думка тут така: Dockerfile — це рецепт, але продукти для нього беруться лише з build context.

2. Крапка в docker build: вибір контексту

Крапка в кінці команди docker build -t ... . — не пунктуація для краси. Це шлях: . — папка, яка стане build context. І це одна з причин, чому в Docker так часто кажуть «збирай із кореня проєкту»: якщо ви помилилися з контекстом, Docker не побачить потрібні файли, а ви будете сперечатися з реальністю.

Подивімося на мінімальний «правильний» варіант для нашого навчального репозиторію docker-java-catalog-service, у корені якого лежать і Dockerfile, і .dockerignore, і build/, і src/:

# Збираємо образ із поточного каталогу (крапка = build context)
docker build -t docker-java-catalog-service:day3 .

По-людськи ця команда читається так: «Docker, ось папка проєкту, візьми з неї все потрібне за правилами .dockerignore і зберіть image за Dockerfile».

А тепер типовий сценарій «майже те саме, але ні». Припустімо, ви випадково перейшли в src/ і запустили збірку звідти:

# Переходимо в підпапку і тим самим змінюємо build context
cd src

# Намагаємося зібрати образ, але контекст тепер = src/
docker build -t docker-java-catalog-service:day3 .
# Dockerfile не знайдено або jar не знайдено — і починається розслідування

Docker чесно бере контекст із src/. А в src/, сюрприз, немає build/libs/...jar і, найімовірніше, немає Dockerfile. Не тому, що Docker «поганий», а тому, що він робить рівно те, що ви попросили.

Іноді люди намагаються «обдурити» ситуацію: вказують Dockerfile окремо через -f, але залишають маленький контекст. Технічно так можна, але для сьогоднішнього дня нам важливіше закріпити базову дисципліну: контекст = корінь проєкту, доки ви не навчитеся впевнено керувати цією механікою.

3. Як працює COPY: шляхи рахуються відносно контексту

Коли в Dockerfile зʼявляється COPY, новачку дуже хочеться думати так: «Ну я ж бачу файл на своєму диску, отже Docker його теж бачить». Але COPY не працює з «вашим диском взагалі». Він працює лише з файлами всередині build context. Тому шляхи в COPY рахуються відносно кореня контексту, а не відносно вашого настрою, робочого столу чи сусідньої папки.

Ось мінімальний фрагмент Dockerfile, де це видно найкраще:

# Фрагмент Dockerfile: показуємо, що COPY працює лише всередині контексту

# Робочий каталог усередині образу
WORKDIR /app

# Копіюємо jar із build context (шлях відносно кореня контексту)
COPY build/libs/docker-java-catalog-service-0.0.1-SNAPSHOT.jar app.jar

Якщо build context — корінь проєкту, шлях build/libs/... існує, і Docker зможе його скопіювати. Якщо build context — інша папка, Docker чесно скаже: «Файл не знайдено». І в цей момент важливо не впадати в містицизм, а згадати правило: Docker бачить лише контекст.

Особливо показова помилка — спроба «сходити назовні» з контексту:

# Так не можна: спроба вийти за межі build context
COPY ../somewhere/app.jar app.jar

Це майже завжди закінчується однаково: Docker не дасть вам так зробити. І це правильно. Інакше можна було б випадково скопіювати в образ будь-що з машини, включно із секретами, а збірка перестала б бути переносною.

Щоб закріпити ідею, ось маленька схема:

Що ви хочете скопіювати Всередині build context? Можна COPY? Коментар
build/libs/app.jar Так Так Наш сценарій на сьогодні
src/main/java/... Так Так Але сьогодні ми вихідні файли не копіюємо
../app.jar Ні Ні Docker не виходить за межі контексту
/Users/.../secret.txt Ні (якщо контекст інший) Ні І слава всім богам безпеки

Поки ви тримаєте в голові правило «COPY працює лише всередині контексту», половина ранніх Docker-помилок перестає бути загадкою. А в першому Dockerfile це зведеться до дуже буденної рядка COPY build/libs/... app.jar: без магії, просто перенесення файлу з контексту всередину образу.

4. Java/Gradle і «брудний» контекст

На маленькому проєкті рівня «hello world» build context зазвичай крихітний, і проблему можна взагалі не помітити. А ось Java/Gradle-проєкти вміють розростатися так, що один docker build раптом починає «думати» хвилину ще до того, як реально збирати образ. І це не магія: ви просто надіслали Dockerʼу забагато файлів.

Що зазвичай потрапляє в контекст «за замовчуванням», якщо ви нічого не налаштували:

  • .git/ — історія репозиторію, обʼєкти, іноді дуже багато даних;
  • .gradle/ — локальні кеші Gradle, які можуть бути величезними;
  • .idea/ або інші файли IDE — корисні вам, але не корисні контейнеру;
  • тимчасові файли ОС, звіти, логи та інша дрібнота;
  • .env з локальними секретами (паролями, токенами) — а це вже не просто «шум», а потенційна проблема безпеки.

Docker під час збірки зазвичай показує, що саме він передає як контекст. У старому виведенні це виглядає так:

Sending build context to Docker daemon  84.12MB

У сучаснішому виведенні, з BuildKit, це часто виглядає приблизно так:

#1 [internal] load build context
#1 transferring context: 84.12MB 2.3s done

І ось тут починається математика реального життя. 84MB можна пережити один раз. Але якщо ви збираєте образ часто, десятки й сотні мегабайт перетворюються на постійне «мито за неуважність». Плюс це просто неприємно: вам здається, що ви збираєте образ з одного JAR, а насправді надсилаєте в збірку половину своєї робочої станції.

Є ще один момент, який часто недооцінюють: навіть якщо ви не копіюєте ці файли в підсумковий образ, а берете лише JAR, вони все одно потрапляють у контекст, а отже впливають на сприйняття збірки Dockerʼом і на час, який він витрачає.

Звідси простий висновок: нам потрібен фільтр, який скаже Dockerʼу «ось ці файли в посилку не клади». Таким фільтром і є .dockerignore.

5. .dockerignore: фільтр контексту

.dockerignore — це файл у корені build context, який описує, які файли й папки не потрібно включати в контекст. За духом він схожий на .gitignore, але працює в інший момент: не під час коміту в репозиторій, а під час збірки Docker image.

Одразу приберемо дві типові ілюзії.

Перша ілюзія: «.dockerignore видаляє файли з проєкту». Ні, він нічого не видаляє. Він лише каже Dockerʼу: «Не бери це в збірку».

Друга ілюзія: «Якщо я не роблю COPY . ., то .dockerignore не потрібен». Потрібен. Навіть якщо ви копіюєте лише один JAR, Docker усе одно спочатку отримує build context. .dockerignore зменшує цей контекст і прискорює шлях до реальної збірки.

Синтаксис у базовій формі дуже простий: кожний рядок — це патерн. Наприклад:

# Не тягнемо в build context те, що не потрібно для збірки образу
.git
.idea
.gradle

# Локальні секрети тримаємо поза Docker-збіркою
.env

Патерни читаються відносно build context. Тобто .gradle означає папку .gradle в корені контексту. Якщо потрібно ігнорувати щось глибше, є більш загальні форми, але на старті нам достатньо найочевидніших.

І ще одне важливе правило саме для нашого дня: сьогодні ми копіюємо JAR із build/libs/. Отже, не можна просто взяти й написати «ігноруй build/ цілком», інакше Docker не побачить потрібний файл. Це не заборона на ігнорування build/ взагалі, а прямий наслідок вибраного сьогодні шляху: JAR збирається на хост-машині, а потім копіюється в образ.

6. Приклад .dockerignore для сервісу

Зараз не потрібен «ідеальний» .dockerignore на всі випадки життя. Це все одно що намагатися побудувати космічний корабель, коли нам потрібен велосипед до магазину. Важливіше отримати зрозумілий файл, який прибирає очевидне сміття, не ламає копіювання JAR і не створює загадкових ефектів.

Уявімо, що корінь проєкту виглядає приблизно так:

docker-java-catalog-service/
|-- Dockerfile
|-- .dockerignore
|-- build/
|   `-- libs/
|       `-- docker-java-catalog-service-0.0.1-SNAPSHOT.jar
|-- .gradle/
|-- .git/
|-- .idea/
|-- .env
|-- .env.example
|-- src/
`-- requests/

Наше завдання — прибрати з контексту все, що не потрібно для збірки образу, і особливо все чутливе. Мінімальний варіант .dockerignore, який зазвичай добре працює на старті, виглядає так:

# Історія репозиторію не потрібна для збірки образу
.git

# Файли IDE не мають стосунку до контейнера
.idea
*.iml

# Локальні кеші Gradle — дуже «важкі»
.gradle

# Секрети не повинні потрапити в build context
.env

Зверніть увагу: ми ігноруємо .env, але не ігноруємо .env.example. Це хороший патерн: приклад конфігурації можна зберігати в репозиторії, а реальні секрети — ні. Навіть якщо в цьому курсі ми поки не запускаємо складні середовища, звичка «.env не має летіти в Docker build» дуже здорова.

Якщо хочеться трохи сильніше підчистити шум, можна додати типовий «смітник» ОС та редакторів, але без фанатизму:

# Історія репозиторію та IDE
.git
.idea

# Локальні кеші Gradle
.gradle

# Локальні секрети
.env

# Сміття ОС
.DS_Store
Thumbs.db

Щоб це не перетворювалося на смакування, корисно тримати в голові просту таблицю: що ми ігноруємо і чому.

Що ігноруємо Чому це не потрібно Dockerʼу під час збірки першого образу Чим погано, якщо залишити
.git/ Docker не збирає образ за історією комітів Контекст більший, збірка повільніша
.gradle/ Це локальний кеш Gradle, а не частина застосунку Дуже швидко роздуває контекст
.idea/ Налаштування IDE Сміття в контексті, іноді мегабайти
.env Часто містить паролі/токени Ризик випадково забрати секрети
Thumbs.db «Слід» ОС Просто шум
.DS_Store «Слід» ОС Просто шум

Тепер важливий момент, прямо повʼязаний із першою лекцією: build/libs/*.jar нам сьогодні потрібен. Отже, файл .dockerignore має бути таким, щоб JAR не зник із контексту.

Якщо ви випадково додасте рядок:

# Обережно: це виключить УВЕСЬ build/ із контексту (включно з jar)
build

то наступний docker build майже напевно завершиться тим, що COPY build/libs/... не знайде файл. І Docker буде правий: ви самі попросили його не включати build/ у контекст.

Є й «дорослий» механізм винятків через !, який дає змогу ігнорувати папку build/ майже цілком, але залишити build/libs/*.jar. Це робочий варіант, але для першого знайомства він може сприйматися як мініголоволомка. Якщо вам хочеться побачити саму ідею без вимоги негайно її застосовувати, це виглядає так:

# Ігноруємо все в build/
build/

# ...але повертаємо назад те, що справді потрібно для COPY у Dockerfile
!build/libs/
!build/libs/*.jar

Читається це так: «ігноруй усе в build/, але поверни назад build/libs/ і JAR-файли всередині». Якщо використовуєте такий варіант, обовʼязково перевіряйте, що JAR справді потрапляє в контекст, інакше отримаєте помилку на COPY.

7. Перевірка контексту: швидкі сигнали й діагностика

Docker у більшості випадків дуже чесно підказує, де проблема, якщо ви вмієте читати перші рядки виводу збірки та помилки COPY. Не потрібно ставати експертом з усіх прапорців — достатньо кількох спостережуваних сигналів.

Перший сигнал — розмір контексту. Запускаєте збірку і дивитеся, що Docker пише про передавання контексту:

docker build -t docker-java-catalog-service:day3 .

Якщо ви бачите щось на кшталт «передаю 400MB», це майже завжди означає, що .dockerignore або відсутній, або занадто слабкий, або ви справді зберігаєте в проєкті щось важке. Для першого образу з одного JAR це вже підозріло: JAR зазвичай важить десятки мегабайт, а не сотні.

Другий сигнал — помилка на кроці COPY. Класична ситуація: ви написали .dockerignore, виключили build/, а потім Dockerfile намагається копіювати JAR. Тоді на етапі збірки ви побачите помилку, схожу на цю (формулювання можуть відрізнятися, але сенс один):

COPY failed: file not found in build context or excluded by .dockerignore

Це чудова помилка. Вона не каже абстрактно «збірка зламалася», а дає конкретну підказку: або файла немає в контексті, або ви самі його виключили.

Третій сигнал — «Dockerfile не знайдено». Якщо ви запускаєте docker build не з кореня проєкту, Docker може просто не знайти Dockerfile. Помилка буде прямою:

failed to read dockerfile: open Dockerfile: no such file or directory

У цей момент не потрібно переписувати проєкт. Потрібно просто повернутися в корінь репозиторію, де лежать Dockerfile і .dockerignore, і запустити команду ще раз.

І ще одна корисна звичка, особливо коли ви щось змінили й сумніваєтеся, що відбувається: дивіться на проєкт очима «списку файлів», а не очима IDE. У лекції 1 ми вже робили ls build/libs. Точно так само іноді корисно зробити ls -la у корені й переконатися, що .dockerignore справді лежить там, де ви думаєте, і називається саме .dockerignore, а не, наприклад, dockerignore.txt (так, це трапляється частіше, ніж ви думаєте).

8. Типові помилки під час роботи з build context і .dockerignore

Помилка №1: запуск docker build не з кореня проєкту.
Найпопулярніший сценарій: ви перебуваєте в src/ або scripts/, запускаєте docker build ... ., і Docker «раптово» не бачить ні Dockerfile, ні JAR. Насправді він усе бачить правильно — просто ви передали йому неправильний контекст. Лікується це не «танцями з прапорцями», а дисципліною: збірку робимо в корені репозиторію, там, де лежать Dockerfile і .dockerignore.

Помилка №2: сприймати крапку в команді як «просто кінець команди».
Крапка — це шлях до контексту. Якщо про це забути, починається лікування симптомів: переписування шляхів у COPY, пошуки «де Docker зберігає файли», сварка на Windows/macOS. Коли ви памʼятаєте правило «контекст — це те, що стоїть після docker build», усе стає спокійніше й передбачуваніше.

Помилка №3: додати в .dockerignore build/ і тим самим «видалити» JAR із контексту.
Для нашого сьогоднішнього сценарію JAR лежить у build/libs/, і саме його ми копіюватимемо. Якщо ви виключили build/, Docker більше не зможе взяти цей файл. Помилка проявиться на COPY, і це нормально: ви самі дали Dockerʼу інструкцію «не брати build». Рішення просте: або не ігнорувати build/ на цьому етапі, або використовувати винятки !build/libs/*.jar.

Помилка №4: думати, що .dockerignore щось видаляє на диску.
Іноді після додавання .dockerignore люди лякаються: «А Docker зараз видалить .env?» Ні. .dockerignore — це фільтр пакування контексту, а не команда прибирання файлів. Ваші файли залишаються на місці, просто Docker більше не бере їх у збірку.

Помилка №5: зберігати секрети у файлах, які потрапляють у контекст, особливо в .env.
Навіть якщо сьогодні ви не робите COPY . ., завтра хтось може додати це «за звичкою» — і раптом секрети опиняться всередині image. Тому ігнорувати .env і не тягнути його в збірку — хороше правило з першого дня. Наша мета — щоб образ збирався однаково в усіх і не тягнув за собою зайве, особливо чутливе.

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