Що таке Dockerfile
Кожен рядок Dockerfile є кроком збірки: 4. Оголошення мережевих портів (EXPOSE 8080). 5. Вказівка виконуваної команди запуску контейнера (ENTRYPOINT ["java", "-jar", "app.jar"]).
🟢 Junior Level
30-секундна відповідь
Dockerfile — це текстовий конфігураційний файл, що містить послідовність декларативних інструкцій, за якими рушій Docker автоматично збирає незмінний образ контейнера (Docker Image).
Кожен рядок Dockerfile є кроком збірки:
- Вибір базової операційної системи та середовища виконання (
FROM eclipse-temurin:21-jre-jammy). - Визначення робочої директорії (
WORKDIR /app). - Копіювання зібраного артефакту (
COPY target/app.jar app.jar). - Оголошення мережевих портів (
EXPOSE 8080). - Вказівка виконуваної команди запуску контейнера (
ENTRYPOINT ["java", "-jar", "app.jar"]).
Наочна аналогія з життя
Dockerfile — це суворий кулінарний рецепт:
FROM— вибір базового напівфабрикату (готове тісто).COPY— додавання начинки (соус, сир).RUN— процес випікання в печі.ENTRYPOINT— інструкція офіціантові, як подавати готову страву гостю. За одним рецептом (Dockerfile) можна спекти тисячі абсолютно однакових піц (образів та контейнерів).
Базовий приклад для Java 21 / Spring Boot 3
# 1. Базовий офіційний образ JRE 21 на базі Ubuntu
FROM eclipse-temurin:21-jre-jammy
# 2. Створення робочої директорії всередині образу
WORKDIR /app
# 3. Копіювання скомпільованого JAR-файлу
COPY target/payment-service-1.0.jar app.jar
# 4. Створення непривілейованого користувача для безпеки
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup
USER appuser
# 5. Декларація мережевого порту
EXPOSE 8080
# 6. Команда запуску в Exec-формі
ENTRYPOINT ["java", "-jar", "app.jar"]
# Команда збірки образу (-t — тег/ім'я, крапка — поточний Build Context):
docker build -t payment-service:1.0 .
# Команда запуску контейнера:
docker run -d -p 8080:8080 --name payment-app payment-service:1.0
🟡 Middle Level
Механіка шарів та інвалідація кешу (Layer Caching)
Docker-образ збирається у вигляді листкового пирога з незмінних шарів файлової системи:
- Інструкції
RUN,COPYтаADDстворюють новий фізичний шар на диску. - Інструкції
WORKDIR,ENV,EXPOSE,USER,ENTRYPOINTдодають метадані в маніфест образу.
Правило інвалідації кешу:
Docker перевіряє кроки зверху вниз:
- Для інструкції
RUN: перевіряється збіг тексту самої команди зі шаром із локального кешу. - Для інструкцій
COPYтаADD: Docker обчислює контрольну суму (Checksum/Hash) кожного копійованого файлу. - Ланцюгова інвалідація: щойно на одному з кроків кеш визнано недійсним (Cache Miss), усі наступні шари до кінця Dockerfile примусово перезбираються з нуля, ігноруючи кеш!
Оптимізація порядку шарів: Залежності vs Вихідний код
# ❌ ПОГАНО: при будь-якій зміні 1 рядка коду наново завантажуються 200 МБ бібліотек!
COPY . .
RUN ./mvnw clean package
# ✅ ПРАВИЛЬНО: роздільне кешування Maven-залежностей
WORKDIR /build
# Крок 1: Копіюємо ТІЛЬКИ pom.xml та wrapper (змінюються рідко)
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline -B # Цей важкий шар закешується!
# Крок 2: Копіюємо вихідний код (змінюється постійно)
COPY src ./src
RUN ./mvnw package -DskipTests -B
Контекст збірки (Build Context) та файл .dockerignore
Коли виконується docker build .:
- CLI-клієнт пакує всю поточну директорію в tar-архів і передає її демону Docker.
- Якщо в директорії лежать папки
.git,target/, тестові дампи або локальні IDE-файли, відправка може зайняти хвилини й передати гігабайти сміття. - Файл
.dockerignoreвиключає непотрібні директорії:.git .idea target/ build/ *.log Dockerfile
🔴 Senior Level
Spring Boot 3 Layered JAR Extraction (Production Gold Standard)
У Spring Boot JAR-файл влаштований так, що залежності бібліотек (BOOT-INF/lib) займають 95% ваги, але змінюються рідко, а код застосунку (BOOT-INF/classes) важить 200 КБ, але змінюється при кожному коміті.
Починаючи зі Spring Boot 2.3 / 3.x, фреймворк підтримує нативне розділення на шари (Layered Tools):
# ==========================================
# Етап 1: Розпакування шарів Spring Boot JAR
# ==========================================
FROM eclipse-temurin:21-jre-jammy AS extractor
WORKDIR /extracted
COPY target/application.jar app.jar
# Витягнення шарів: dependencies, spring-boot-loader, snapshot-dependencies, application
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted/
# ==========================================
# Етап 2: Фінальний легковагий Runtime-образ
# ==========================================
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
# Створення безпечного non-root користувача
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 --ingroup appgroup appuser
USER appuser:appgroup
# Копіювання шарів від найстабільніших до найбільш мінливих:
COPY --from=extractor /extracted/dependencies/ ./
COPY --from=extractor /extracted/spring-boot-loader/ ./
COPY --from=extractor /extracted/snapshot-dependencies/ ./
COPY --from=extractor /extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "org.springframework.boot.loader.launch.JarLauncher"]
Результат: збірка та пуш нового релізу в Docker Registry займають менше 2 секунд, оскільки пушиться лише шар застосунку вагою в кілька сотень кілобайт!
Безпечна збірка з Docker BuildKit (Секрети та Cache Mounts)
У сучасних версіях Docker (BuildKit) передавати паролі від приватних Maven-репозиторіїв (Nexus/Artifactory) через ARG або ENV суворо заборонено, оскільки вони назавжди залишаються в метаданих шарів (docker history).
Правильний підхід із монтуванням секретів та кешу:
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
# Кешування папки ~/.m2 на хості між збірками та безпечна передача пароля:
RUN --mount=type=cache,target=/root/.m2 \
--mount=type=secret,id=maven_settings,target=/root/.m2/settings.xml \
mvn clean package -DskipTests
Секрет монтується лише в оперативну пам’ять на час виконання інструкції RUN і гарантовано не потрапляє в готовий образ.
4 Tricky Questions
1. Чому передача API-токенів або паролів до БД через інструкцію ARG у Dockerfile є грубою вразливістю, і як правильно передавати секрети?
Відповідь:
Змінні, оголошені через ARG, зберігаються в маніфесті збірки та історії шарів образу. Будь-який користувач, який завантажив образ із реєстру, може виконати команду docker history --no-trunc <image> або інспектувати образ через docker inspect і побачити передані паролі та ключі у відкритому вигляді.
Інструкція ENV ще небезпечніша: змінна зберігається як доступна змінна оточення всередині запущеного контейнера.
Правильне рішення: використовувати BuildKit Secret Mounts:
RUN --mount=type=secret,id=my_secret cat /run/secrets/my_secret .... Секрет монтується через тимчасову файлову систему в оперативній пам’яті (tmpfs), не залишає слідів в історії шарів і недоступний у підсумковому образі.
2. Чому видалення великого файлу командою RUN rm /tmp/big_file.iso у другому рядку Dockerfile НЕ зменшує розмір підсумкового образу?
Відповідь: Шари Docker-образу є строго незмінними (Read-Only) і працюють за принципом каскадного накладення (OverlayFS):
- Перший крок
RUN wget ... /tmp/big_file.isoстворює Шар 1, куди записується файл розміром 1 ГБ. - Другий крок
RUN rm /tmp/big_file.isoстворює Шар 2, у який записується лише спеціальний системний маркер приховування файлу (Whiteout file). - При запуску контейнера файлова система робить файл невидимим, але у фізичному образі Шар 1 вагою 1 ГБ зберігається назавжди.
Рішення: об’єднувати завантаження, розпакування та видалення в одну інструкцію
RUN:RUN wget http://.../file.tar.gz && tar -xzf file.tar.gz && rm file.tar.gz(або використовувати Multi-stage збірку).
3. У чому небезпека використання базових образів Alpine Linux (eclipse-temurin:21-alpine) для високонавантажених Java-застосунків?
Відповідь:
Alpine Linux використовує власну реалізацію стандартної бібліотеки мови C — musl libc замість стандартної GNU C Library (glibc), прийнятої в Debian/Ubuntu:
- Несумісність із JNI/C++: високопродуктивні нативні бібліотеки (Netty Epoll транспорт, Snappy компресія для Kafka, RocksDB, компілятор GraalVM) часто скомпільовані під
glibcі або падають при старті зUnsatisfiedLinkError, або працюють через неефективну емуляціюgcompat. - Алокатор пам’яті: системний алокатор пам’яті в
muslоптимізований під мінімальний обсяг коду, але сильно поступаєтьсяptmalloc(з glibc) абоjemallocу сценаріях із сотнями паралельних потоків, викликаючи деградацію продуктивності за алокаціями пам’яті. Для Java у Production рекомендується використовувати*-slimобрази на базі Debian/Ubuntu.
4. Чим відрізняється поведінка інструкцій COPY src/ /dest/ та COPY src /dest/?
Відповідь: Це класична пастка синтаксису Dockerfile:
- Якщо джерелом є директорія, Docker копіює лише вміст директорії, а не саму директорію!
- Інструкція
COPY src/ /dest/скопіює всі файли з локальної папкиsrcвсередину контейнерної папки/dest/. - Щоб зберегти саме ім’я директорії, необхідно явно вказувати цільовий шлях:
COPY src /dest/src/. - Крім того, інструкція
COPYза замовчуванням присвоює всім скопійованим файлам власникаroot:root. Якщо застосунок запускається від непривілейованого користувача, необхідно явно вказувати прапорець зміни власника:COPY --chown=appuser:appgroup app.jar ..
🎯 Шпаргалка для інтерв’ю
Головне про Dockerfile
- Суть: декларативний рецепт створення незмінного образу.
- Шари:
RUN,COPY,ADDстворюють фізичні незмінні шари OverlayFS. - Оптимізація кешу: розташовувати стабільні шари (залежності, JRE) на початку, часто змінювані (код) — у кінці.
- Build Context: весь вміст поточної папки передається демону; відсікати зайве через
.dockerignore. - Spring Boot 3: використовувати розпакування шарів JAR (
JarLauncher) для 99% попадання в кеш. - Безпека: запускати від
USER nonroot, не передавати секрети черезARG/ENV, використовувати Exec-форму вENTRYPOINT.
Порівняння базових образів для Java
| Образ | Розмір | C-бібліотека | Плюси | Мінуси |
| :— | :— | :— | :— | :— |
| temurin:21-jre-jammy | $\approx 250$ МБ | glibc | 100% сумісність, стабільність Netty/Kafka | Більша вага |
| temurin:21-alpine | $\approx 100$ МБ | musl | Мінімальний розмір | Проблеми з JNI, алокатором пам’яті |
| distroless/java21 | $\approx 150$ МБ | glibc | Немає shell/пакетів, максимальна безпека | Складніше налагоджувати |
Червоні прапорці на співбесіді (Чого говорити не можна)
- ❌ «Для зменшення розміру образу я просто видаляю файли окремим рядком RUN rm» — в OverlayFS це не зменшує розмір, а додає новий шар приховування.
- ❌ «Секрети для збірки безпечно передавати через ARG» — вони залишаються у відкритому доступі в
docker history. - ❌ «Базовий образ краще завжди брати :latest, щоб мати свіжі апдейти» — ламає відтворюваність збірки; необхідно фіксувати конкретний тег.