🐳 Розділ 14 · Питання #3

Що таке Dockerfile

Кожен рядок Dockerfile є кроком збірки: 4. Оголошення мережевих портів (EXPOSE 8080). 5. Вказівка виконуваної команди запуску контейнера (ENTRYPOINT ["java", "-jar", "app.jar"]).


🟢 Junior Level

30-секундна відповідь

Dockerfile — це текстовий конфігураційний файл, що містить послідовність декларативних інструкцій, за якими рушій Docker автоматично збирає незмінний образ контейнера (Docker Image).

Кожен рядок Dockerfile є кроком збірки:

  1. Вибір базової операційної системи та середовища виконання (FROM eclipse-temurin:21-jre-jammy).
  2. Визначення робочої директорії (WORKDIR /app).
  3. Копіювання зібраного артефакту (COPY target/app.jar app.jar).
  4. Оголошення мережевих портів (EXPOSE 8080).
  5. Вказівка виконуваної команди запуску контейнера (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 перевіряє кроки зверху вниз:

  1. Для інструкції RUN: перевіряється збіг тексту самої команди зі шаром із локального кешу.
  2. Для інструкцій COPY та ADD: Docker обчислює контрольну суму (Checksum/Hash) кожного копійованого файлу.
  3. Ланцюгова інвалідація: щойно на одному з кроків кеш визнано недійсним (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:

  1. Несумісність із JNI/C++: високопродуктивні нативні бібліотеки (Netty Epoll транспорт, Snappy компресія для Kafka, RocksDB, компілятор GraalVM) часто скомпільовані під glibc і або падають при старті з UnsatisfiedLinkError, або працюють через неефективну емуляцію gcompat.
  2. Алокатор пам’яті: системний алокатор пам’яті в 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, щоб мати свіжі апдейти» — ламає відтворюваність збірки; необхідно фіксувати конкретний тег.

Пов’язані питання