🐳 Раздел 14 · Вопрос #3

Что такое Dockerfile

Каждая строчка Dockerfile представляет собой шаг сборки: 4. Объявление сетевых портов (EXPOSE 8080). 5. Указание исполняемой команды запуска контейнера (ENTRYPOINT ["java", "-ja...


🟢 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, чтобы иметь свежие апдейты» — ломает воспроизводимость сборки; необходимо фиксировать конкретный тег.

Связанные вопросы