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

Какие основные инструкции используются в Dockerfile

Инструкции в Dockerfile представляют собой команды предметно-ориентированного языка (DSL), которые последовательно выполняются сборщиком Docker для создания слоёв и метаданных о...


🟢 Junior Level

30-секундный ответ

Инструкции в Dockerfile представляют собой команды предметно-ориентированного языка (DSL), которые последовательно выполняются сборщиком Docker для создания слоёв и метаданных образа.

Все основные инструкции делятся на 4 логические группы:

  1. Среда и контекст: FROM (базовый образ), WORKDIR (рабочая папка), ENV (переменные рантайма), ARG (переменные сборки).
  2. Файловые операции: COPY (копирование с хоста), ADD (копирование с автораспаковкой tar).
  3. Выполнение команд: RUN (выполняется во время сборки образа), CMD и ENTRYPOINT (выполняются в момент старта контейнера).
  4. Конфигурация и безопасность: USER (непривилегированный пользователь), EXPOSE (документация портов), VOLUME (точка монтирования тома), HEALTHCHECK (проверка жизнеспособности).

Наглядный пример для Java 21

# 1. Задание базового образа
FROM eclipse-temurin:21-jre-jammy

# 2. Рабочая директория (создаётся автоматически, если её нет)
WORKDIR /app

# 3. Переменная окружения для JVM
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"

# 4. Копирование JAR-файла
COPY target/service.jar app.jar

# 5. Создание и переключение на непривилегированного пользователя
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser

# 6. Документирование порта приложения
EXPOSE 8080

# 7. Фиксированная команда запуска
ENTRYPOINT ["java", "-jar", "app.jar"]

🟡 Middle Level

Детальный разбор ключевых инструкций

1. RUN vs CMD vs ENTRYPOINT (Главная путаница начинающих)

  • RUN: выполняется один раз на этапе сборки образа (docker build). Результат выполнения команды фиксируется в новом слое файловой системы (установка пакетов apt-get, компиляция, создание пользователей).
  • ENTRYPOINT: задаёт главный исполняемый бинарник, который стартует при запуске контейнера (docker run). Переопределить его можно только явным флагом --entrypoint.
  • CMD: задаёт аргументы по умолчанию для ENTRYPOINT. Легко переопределяется при передаче аргументов в конце команды docker run.

2. COPY vs ADD

  • COPY: простое, прозрачное и безопасное копирование файлов и папок с хоста в образ. Рекомендуется в 95% случаев.
  • ADD: расширенная версия:
    1. Автоматически распаковывает локальные tar-архивы (.tar, .tar.gz, .tar.bz2) в целевую директорию.
    2. Умеет скачивать файлы по URL (однако в Production для URL принято использовать RUN curl -fSL ..., так как ADD не умеет очищать кэш скачанного архива).

3. ARG vs ENV

| Параметр | ARG (Build Argument) | ENV (Environment Variable) | | :— | :— | :— | | Когда доступна | Только во время сборки (docker build) | И при сборке, и внутри запущенного контейнера | | Переопределение | Флаг --build-arg KEY=value | Флаг -e KEY=value при docker run | | Безопасность | Видна в docker history (не для паролей!) | Видна в docker inspect контейнера |

4. EXPOSE и VOLUME

  • EXPOSE 8080: является исключительно документацией для человека и оркестратора. Инструкция НЕ открывает порт наружу. Чтобы порт стал доступен с хоста, его нужно явно пробросить: docker run -p 8080:8080.
  • VOLUME ["/data"]: объявляет директорию точкой монтирования внешнего хранилища. Данные, записываемые в эту директорию, сохраняются вне слоёв OverlayFS.

🔴 Senior Level

Золотое правило уменьшения слоёв: Chaining в RUN

Поскольку каждый шаг RUN создаёт неизменяемый слой, очистка временных файлов должна происходить в рамках той же самой сессии bash:

# ❌ КАТАСТРОФА: Кэш apt остался в нижнем слое, образ весит на 150 МБ больше!
RUN apt-get update
RUN apt-get install -y curl jq
RUN rm -rf /var/lib/apt/lists/*

# ✅ ПРАВИЛЬНО: Один слой, чистая файловая система
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl jq && \
    rm -rf /var/lib/apt/lists/*

Exec Form vs Shell Form и проблема сигналов PID 1

Любая инструкция запуска (RUN, CMD, ENTRYPOINT) может быть записана в двух формах:

  1. Shell Form (Строка без скобок):
    ENTRYPOINT java -jar app.jar
    

    Docker неявно оборачивает команду в /bin/sh -c "java -jar app.jar". Оболочка sh становится процессом с PID 1, а JVM — её дочерним процессом. Сигнал SIGTERM от Kubernetes не доходит до Java: приложение не успевает закрыть соединения с БД и сбросить кэши, завершаясь аварийно по SIGKILL.

  2. Exec Form (JSON-массив строк):
    ENTRYPOINT ["java", "-jar", "app.jar"]
    

    Команда вызывается ядром напрямую через системный вызов execve(). Процесс JVM становится PID 1, корректно перехватывая SIGTERM и запуская Spring Boot Shutdown Hooks.


Тонкости работы с VOLUME в Dockerfile

Senior-разработчик обязан помнить о ловушке порядка инструкций с VOLUME:

  • Как только в Dockerfile объявлена инструкция VOLUME ["/app/data"], Docker переводит эту папку в режим внешнего анонимного тома для всех последующих инструкций сборки.
  • Если на следующем шаге выполнить RUN chown -R appuser:appgroup /app/data или положить туда начальные файлы, эти изменения не сохранятся в итоговом образе, так как применяются к временному анонимному тому, удаляемому после сборки шага!
  • Правило: создание директории, назначение прав chown и инициализация дефолтных файлов должны выполняться СТРОГО ДО объявления директивы VOLUME.

Инструкция HEALTHCHECK для оркестраторов

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
  • Позволяет Docker Engine отслеживать реальный статус сервиса: статус контейнера меняется с healthy на unhealthy.
  • Опция --start-period=60s даёт Spring Boot приложению время на «холодный старт» и прогрев JIT без ложных перезапусков.

4 Tricky Questions

1. Какая скрытая опасность возникает, если инструкция RUN chown -R appuser /app/data размещена ПОСЛЕ инструкции VOLUME ["/app/data"]?

Ответ: Как только Docker встречает инструкцию VOLUME ["/app/data"], он помечает этот путь как том. На каждом последующем шаге сборки (включая RUN chown ...) Docker монтирует для сборки этого шага новый временный анонимный том, выполняет команду chown, фиксирует слой и… выбрасывает временный том вместе со всеми изменёнными правами доступа! В результате в итоговом образе права доступа на папку /app/data останутся принадлежать пользователю root. При запуске контейнера от USER appuser приложение получит ошибку Permission Denied при попытке записи в /app/data. Любая настройка прав и файлов директории обязана производиться до объявления VOLUME.


2. В чём разница области видимости (Scope) у переменной ARG, объявленной ДО первой инструкции FROM, и ARG, объявленной ПОСЛЕ FROM?

Ответ: Инструкция ARG, объявленная до первой строчки FROM, находится в глобальной области видимости сборки. Она предназначена исключительно для параметризации самих базовых образов (например, ARG JAVA_VER=21 $\to$ FROM eclipse-temurin:${JAVA_VER}):

  • После выполнения инструкции FROM начинается конкретный этап сборки (Build Stage), и глобальный ARG выходит из области видимости.
  • Любые последующие инструкции RUN echo $JAVA_VER внутри этапа выведут пустую строку!
  • Чтобы использовать эту переменную внутри шага сборки, её необходимо повторно объявить внутри этапа без значения:
    ARG JAVA_VER=21
    FROM eclipse-temurin:${JAVA_VER}
    ARG JAVA_VER # Повторное объявление делает переменную доступной внутри этапа!
    RUN echo "Building with Java $JAVA_VER"
    

3. Как взаимодействуют ENTRYPOINT и CMD, если обе инструкции объявлены в Exec Form? Покажите на примере запуска Spring Boot.

Ответ: Если обе инструкции объявлены в Exec Form, Docker объединяет их массивы: аргументы из CMD передаются как завершающие параметры в команду ENTRYPOINT:

ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["--server.port=8080", "--spring.profiles.active=prod"]

При обычном запуске docker run my-app выполнится команда: java -jar app.jar --server.port=8080 --spring.profiles.active=prod. Однако если пользователь запустит контейнер с собственными аргументами: docker run my-app --server.port=9090, то переданный аргумент полностью заместит массив CMD, и выполнится: java -jar app.jar --server.port=9090. Это стандартный паттерн создания гибких микросервисных образов.


4. Почему для скачивания удалённых архивов по HTTP инструкция RUN curl -fSL ... && tar -xz && rm предпочтительнее, чем встроенная возможность ADD http://...?

Ответ:

  1. Инструкция ADD http://example.com/archive.tar.gz /tmp/ не умеет автоматически распаковывать удалённые архивы, скачанные по HTTP (автораспаковка работает только для локальных tar-файлов из Build Context).
  2. Скачанный архив сохраняется в неизменяемом слое Docker-образа. Даже если на следующем шаге вызвать RUN rm /tmp/archive.tar.gz, архив останется внутри слоя образа навсегда, увеличивая его размер.
  3. Инструкция ADD не поддерживает проверку контрольных сумм sha256, аутентификацию и повторные попытки при сбое сети. Связка RUN curl ... | tar -xz в одной строке скачивает, распаковывает и удаляет поток байтов на лету, не создавая мусорных промежуточных слоёв.

🎯 Шпаргалка для интервью

Сводная таблица инструкций Dockerfile

| Инструкция | Стадия | Назначение | Особенности | | :— | :— | :— | :— | | FROM | Build | Базовый образ | Первая обязательная инструкция | | WORKDIR| Build/Run | Рабочая папка | Создаёт путь автоматически, заменяет cd | | RUN | Build | Сборка и установка | Создаёт новый слой; объединять через && \ | | COPY | Build | Копирование файлов | Безопасно и прозрачно; использовать --chown | | ADD | Build | Копирование + распаковка | Автораспаковка .tar.gz; избегать для URL | | ARG | Build | Переменная сборки | Не сохраняется в контейнере; видна в истории | | ENV | Build/Run | Переменная среды | Доступна приложению в runtime | | USER | Run | Пользователь процесса | Защита от root: USER 1000:1000 | | EXPOSE | Мета | Документация порта | Не публикует порт сам по себе | | ENTRYPOINT| Run | Основная команда | Exec-форма ["cmd"] для корректного PID 1 | | CMD | Run | Аргументы по умолчанию | Легко переопределяется при docker run |

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «EXPOSE открывает доступ к порту контейнера из интернета» — порт публикуется только флагом -p 8080:8080.
  • ❌ «CMD и ENTRYPOINT — это одно и то же» — ENTRYPOINT фиксирует исполняемый файл, CMD задаёт параметры по умолчанию.
  • ❌ «Пароли к БД нужно передавать через ARG, чтобы их не было видно в контейнере» — они остаются в манифесте образа docker history.
  • ❌ «Для перехода в папку я пишу RUN cd /app» — каждый RUN выполняется в новом субоболочечном процессе, нужно использовать WORKDIR /app.

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