Что такое Dockerfile
Каждая строчка Dockerfile представляет собой шаг сборки: 4. Объявление сетевых портов (EXPOSE 8080). 5. Указание исполняемой команды запуска контейнера (ENTRYPOINT ["java", "-ja...
🟢 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, чтобы иметь свежие апдейты» — ломает воспроизводимость сборки; необходимо фиксировать конкретный тег.