Главная/Dockerfile/Урок

5.2. Базовые инструкции

Цели

После этого материала вы сможете:

  • применять по назначению FROM, ARG, ENV, WORKDIR, LABEL, USER, EXPOSE, VOLUME, SHELL, STOPSIGNAL, ONBUILD;
  • объяснить различие ARG и ENV по области видимости и по попаданию в образ;
  • объяснить, почему секрет, переданный через ARG, извлекается из готового образа;
  • назвать для каждой инструкции типичную ошибку и случай, когда её применять не следует;
  • прочитать чужой Dockerfile и найти в нём проблемы уровня базовых инструкций.

Предварительные знания

Ключевые термины

ТерминОбъяснение
build-timeЭтап сборки образа
runtimeЭтап работы container
область видимостиЧасть Dockerfile, где переменная доступна
build stageСтадия multi-stage сборки, начинающаяся с FROM
predefined ARGАргумент, доступный без объявления: TARGETPLATFORM и другие
OCI annotationСтандартизованная метка вида org.opencontainers.image.*

Теория

Две группы инструкций

Инструкции Dockerfile делятся на две категории по результату:

КатегорияИнструкцииРезультат
Создают слойFROM, RUN, COPY, ADDНовый слой файловой системы
Изменяют конфигурациюARG, ENV, WORKDIR, LABEL, USER, EXPOSE, VOLUME, SHELL, STOPSIGNAL, CMD, ENTRYPOINT, ONBUILDЗапись в config образа, размер 0B

Этот урок посвящён второй группе (кроме CMD и ENTRYPOINT — им отведён урок 5.4). Первая группа разбирается в уроке 5.3.

Инструкции второй группы не занимают места, но каждая изменяет Image ID (урок 3.1) и появляется в docker history со значением 0B.

ARG против ENV — главное различие урока

Обе задают переменные, но работают принципиально по-разному.

ARGENV
Доступна на этапе сборкидада
Доступна в работающем containerнетда
Попадает в конфигурацию образанетда
Задаётся снаружи--build-arg-e при запуске
Видна в docker historyда, значениеда
Область видимоститекущая стадияот объявления до конца стадии
Пригодна для секретовнетнет

Последняя строка требует пояснения, потому что противоречит распространённой практике.

ARG не годится для секретов, хотя значение и не попадает в конфигурацию образа. Причина: оно сохраняется в истории сборки. Команда docker history --no-trunc покажет строку ARG DB_PASSWORD=secret123 любому, у кого есть образ.

ENV не годится тем более: значение попадает и в историю, и в конфигурацию, и в окружение каждого процесса container.

Правильный способ — secret mounts BuildKit (урок 5.6).

Область видимости ARG

Три правила, которые нужно знать точно.

Правило 1. ARG до первого FROM доступен только в инструкциях FROM.

dockerfile
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim
RUN echo "версия: ${PYTHON_VERSION}"   # ПУСТО

Переменная, объявленная до FROM, находится вне какой-либо стадии. Она подставляется в саму инструкцию FROM, но внутри стадии недоступна.

Правило 2. Чтобы использовать её внутри стадии, объявите повторно.

dockerfile
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim
ARG PYTHON_VERSION                      # повторное объявление без значения
RUN echo "версия: ${PYTHON_VERSION}"   # 3.13

Повторное объявление без значения наследует значение из внешней области.

Правило 3. ARG действует от объявления до конца стадии.

Использование до объявления даёт пустую строку — без ошибки и без предупреждения. Это делает опечатку в имени переменной труднообнаружимой.

Приоритет ENV над ARG

Если в стадии объявлены обе переменные с одинаковым именем, ENV побеждает — независимо от порядка объявления и от значения, переданного через --build-arg.

Практическое следствие: конструкция

dockerfile
ARG APP_ENV=development
ENV APP_ENV=${APP_ENV}

работает как ожидается — она передаёт значение из build-time в runtime. А вот

dockerfile
ENV APP_ENV=production
ARG APP_ENV
RUN echo ${APP_ENV}

всегда выведет production, что бы вы ни передали в --build-arg APP_ENV=....

Predefined ARG

Несколько аргументов доступны без объявления. Наиболее полезны платформенные:

ПеременнаяЗначение
TARGETPLATFORMПлатформа сборки: linux/amd64
TARGETOSОперационная система: linux
TARGETARCHАрхитектура: amd64, arm64
TARGETVARIANTВариант: v7 для arm/v7
BUILDPLATFORMПлатформа машины, на которой идёт сборка

Они нужны для multi-platform сборок: позволяют скачать бинарный файл под нужную архитектуру. Чтобы использовать их внутри стадии, всё равно требуется объявить ARG TARGETARCH.

Также предопределены прокси-переменные (HTTP_PROXY, NO_PROXY и другие) — они не попадают в docker history, в отличие от обычных ARG.


Внутренний механизм

Куда попадают значения

При сборке BuildKit формирует конфигурацию образа (урок 3.1):

text
   ENV      ──►  config.Env[]        ──► окружение процессов container
   WORKDIR  ──►  config.WorkingDir   ──► рабочий каталог
   USER     ──►  config.User         ──► UID процесса
   EXPOSE   ──►  config.ExposedPorts ──► только метаданные
   LABEL    ──►  config.Labels       ──► метаданные
   VOLUME   ──►  config.Volumes      ──► анонимные volumes при запуске
   STOPSIGNAL ► config.StopSignal    ──► сигнал при docker stop
   SHELL    ──►  config.Shell        ──► обёртка для shell form
   ARG      ──►  никуда              ──► только в history

Проверить любое поле можно через docker image inspect --format '{{.Config.Env}}'.

Почему WORKDIR создаёт каталог

WORKDIR /app при отсутствии /app создаёт его — и делает это от имени текущего USER. Отсюда неочевидное поведение: порядок USER и WORKDIR влияет на владельца каталога.

dockerfile
USER appuser
WORKDIR /app          # каталог принадлежит appuser
dockerfile
WORKDIR /app          # каталог принадлежит root
USER appuser
RUN touch /app/f      # Permission denied

Это одна из самых частых причин ошибок прав доступа в Python-образах — разбирается в разделе 06.


Инструкции

Для каждой: назначение, синтаксис, пример, типичная ошибка, когда применять не следует.

FROM

Назначение. Задаёт базовый образ и начинает новую стадию сборки.

Синтаксис.

text
FROM [--platform=<platform>] <image>[:<tag>|@<digest>] [AS <name>]

Пример.

dockerfile
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251 AS builder

Типичная ошибка. Использование latest или отсутствие тега. Сборка перестаёт быть воспроизводимой: сегодня и через месяц вы получите разные образы при том же коде (урок 3.3).

Когда не следует. Не указывайте --platform в FROM без необходимости: это жёстко привязывает стадию к платформе и ломает multi-platform сборку. Для кросс-компиляции используйте --platform=$BUILDPLATFORM только на стадии сборки, а финальную стадию оставляйте без указания.

ARG

Назначение. Переменная, доступная только на этапе сборки.

Синтаксис.

text
ARG <name>[=<default>]

Пример.

dockerfile
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim
ARG PYTHON_VERSION
RUN echo "собрано на Python ${PYTHON_VERSION}"

Типичная ошибка. Передача секретов через --build-arg. Значение сохраняется в истории образа и извлекается командой docker history --no-trunc.

Когда не следует. Для значений, нужных в работающем container, — они там недоступны. Для секретов — используйте secret mounts. Для часто меняющихся значений — инвалидируют кэш всех последующих слоёв.

ENV

Назначение. Переменная окружения, доступная и при сборке, и в работающем container.

Синтаксис.

text
ENV <key>=<value> [<key>=<value>...]

Форма ENV <key> <value> (без знака равенства) объявлена устаревшей в Docker 20.10 и не должна использоваться: она неоднозначна при нескольких присваиваниях.

Пример.

dockerfile
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PATH="/app/.venv/bin:$PATH"

Типичная ошибка. Секреты в ENV. Они попадают в конфигурацию образа, видны в docker inspect и в окружении каждого процесса.

Вторая ошибка — переопределение PATH без сохранения прежнего значения: ENV PATH="/app/bin" сделает недоступными все системные утилиты.

Когда не следует. Для конфигурации, различающейся между окружениями, — задавайте её при запуске через -e или env_file. Значение в образе означает, что для смены настройки придётся пересобирать образ.

WORKDIR

Назначение. Задаёт рабочий каталог для последующих RUN, CMD, ENTRYPOINT, COPY, ADD.

Синтаксис.

text
WORKDIR /absolute/path

Пример.

dockerfile
WORKDIR /app
COPY . .

Типичная ошибка. Использование RUN cd /app вместо WORKDIR. Каждая инструкция RUN выполняется в новом процессе — смена каталога не сохраняется до следующей инструкции.

Когда не следует. С относительными путями: WORKDIR app создаст каталог относительно предыдущего WORKDIR, и результат зависит от контекста. Всегда используйте абсолютные пути.

USER

Назначение. Задаёт пользователя для последующих RUN, CMD, ENTRYPOINT.

Синтаксис.

text
USER <user>[:<group>]
USER <UID>[:<GID>]

Пример.

dockerfile
RUN useradd --create-home --uid 10001 appuser
USER 10001:10001

Типичная ошибка. Указание имени вместо UID. При монтировании volumes и в Kubernetes (runAsNonRoot) проверяется числовой UID; имя, отсутствующее в /etc/passwd целевой системы, вызывает проблемы.

Вторая ошибка — USER перед установкой пакетов: непривилегированный пользователь не сможет выполнить apt-get install.

Когда не следует. Не переключайтесь на непривилегированного пользователя раньше, чем выполнены операции, требующие прав. Порядок: установка пакетов → создание пользователя → chown нужных каталогов → USER.

EXPOSE

Назначение. Документирует порты, которые слушает приложение.

Синтаксис.

text
EXPOSE <port>[/<protocol>]

Пример.

dockerfile
EXPOSE 8000/tcp

Типичная ошибка. Ожидание, что EXPOSE публикует порт. Это только метаданные: порт не становится доступным с host. Публикация выполняется флагом -p при запуске или ключом ports в Compose (раздел 08).

Когда не следует. Инструкция необязательна и ничего не меняет функционально. Единственный практический эффект: docker run -P публикует все объявленные порты на случайные порты host, и некоторые инструменты используют эти метаданные. Отсутствие EXPOSE не мешает работе.

LABEL

Назначение. Метаданные образа: авторство, версия, источник, лицензия.

Синтаксис.

text
LABEL <key>=<value> [<key>=<value>...]

Пример. Стандартизованные аннотации OCI:

dockerfile
LABEL org.opencontainers.image.source="https://github.com/example/app" \
      org.opencontainers.image.description="API сервиса заказов" \
      org.opencontainers.image.licenses="MIT"

Типичная ошибка. Использование устаревшей инструкции MAINTAINER. Она объявлена deprecated в Docker 1.13; замена — LABEL org.opencontainers.image.authors="...".

Вторая ошибка — по одной инструкции LABEL на метку: каждая создаёт запись в истории. Объединяйте в одну.

Когда не следует. Не помещайте в метки динамические значения, меняющиеся при каждой сборке (например, timestamp), если важен build cache: это меняет Image ID и инвалидирует кэш для тех, кто использует образ как базовый.

VOLUME

Назначение. Объявляет точку монтирования, для которой при запуске создаётся анонимный volume.

Синтаксис.

text
VOLUME ["/path"]

Пример.

dockerfile
VOLUME ["/var/lib/postgresql/data"]

Типичная ошибка. Ожидание, что VOLUME задаёт именованный volume. Создаётся анонимный volume со случайным именем; при docker rm -v он удаляется, при обычном docker rm — остаётся мусором.

Вторая, более коварная: запись в объявленный путь после инструкции VOLUME теряется. Инструкции RUN после VOLUME пишут во временный volume, который отбрасывается по завершении инструкции.

Когда не следует. В большинстве случаев. Инструкция ограничивает пользователя образа: отменить объявленный VOLUME нельзя, а анонимные volumes накапливаются незаметно. Лучше документировать точку монтирования в README и задавать volume явно при запуске. Тема разбирается в разделе 07.

STOPSIGNAL

Назначение. Сигнал, посылаемый при docker stop.

Синтаксис.

text
STOPSIGNAL SIGQUIT

Пример.

dockerfile
STOPSIGNAL SIGQUIT

Типичная ошибка. Не задавать сигнал для приложений, у которых graceful shutdown привязан к нестандартному сигналу. Для nginx SIGTERM означает быстрое завершение, а SIGQUIT — корректное; официальный образ поэтому содержит STOPSIGNAL SIGQUIT.

Когда не следует. Если приложение корректно обрабатывает SIGTERM — значение по умолчанию верное, инструкция не нужна.

SHELL

Назначение. Меняет оболочку, используемую для shell form инструкций RUN, CMD, ENTRYPOINT.

Синтаксис.

text
SHELL ["executable", "parameters"]

Пример. Включение строгого режима для всех последующих RUN:

dockerfile
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

Типичная ошибка. Указание оболочки, отсутствующей в образе. В Alpine нет bash — будет ошибка с exit code 127 (урок 4.6).

Когда не следует. Без конкретной причины. Основная законная причина — pipefail: без него RUN cmd1 | cmd2 возвращает код только последней команды, и падение cmd1 останется незамеченным.

ONBUILD

Назначение. Откладывает выполнение инструкции до момента, когда образ будет использован как базовый.

Синтаксис.

text
ONBUILD <инструкция>

Пример.

dockerfile
ONBUILD COPY requirements.txt /app/
ONBUILD RUN pip install --no-cache-dir -r /app/requirements.txt

Типичная ошибка. Использование в обычных образах. Инструкции срабатывают неявно, и разработчик, использующий такой базовый образ, не понимает, откуда взялись файлы и почему сборка требует определённой структуры каталогов.

Когда не следует. Практически всегда. Механизм считается устаревшим подходом: он делает поведение неявным и плохо сочетается с multi-stage builds. Вместо него используйте общую базовую стадию (урок 5.7).


Команды и примеры

Подготовка

bash
mkdir -p /tmp/base-instr && cd /tmp/base-instr

Область видимости ARG

bash
cat > Dockerfile.scope <<'EOF'
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim

# Без повторного объявления переменная недоступна
RUN echo "БЕЗ объявления: [${PYTHON_VERSION}]"

ARG PYTHON_VERSION
RUN echo "ПОСЛЕ объявления: [${PYTHON_VERSION}]"

CMD ["true"]
EOF

docker build --no-cache -f Dockerfile.scope -t scope:1 . 2>&1 | grep -E '^#[0-9]+ [0-9.]+ .*объявления'
text
#5 0.288 БЕЗ объявления: [3.13.14]
#6 0.384 ПОСЛЕ объявления: [3.13]

Первая строка не пуста, и это хуже, чем если бы была пуста. Ожидание «переменная вне области видимости — значит, пусто» не оправдалось: подставилось 3.13.14, чего в Dockerfile вообще не написано.

Значение пришло из базового образа:

bash
docker run --rm python:3.13-slim env | grep PYTHON_VERSION
text
PYTHON_VERSION=3.13.14

python:3.13-slim задаёт ENV PYTHON_VERSION, и после FROM это окружение унаследовано. Объявленный до FROM ARG в стадию действительно не попал — но имя совпало, и вместо него подставился ENV.

Вторая строка показывает, что происходит после повторного объявления: ARG PYTHON_VERSION без значения возвращает аргумент сборки (3.13), перекрывая унаследованный ENV.

Практический вывод: совпадение имени вашего ARG с переменной базового образа не даёт ни ошибки, ни пустоты — оно даёт чужое правдоподобное значение. Если бы базой был alpine, где такой переменной нет, строка оказалась бы пустой; поведение зависит от базы, и именно поэтому на него нельзя опираться.

Строки вывода отобраны по шаблону ^#N T.T — это строки вывода команды. BuildKit печатает ещё и строку с самой командой (#5 [2/3] RUN echo ...), где подстановка уже выполнена, и без фильтра значение видно дважды. Флаг --no-cache обязателен: при попадании в кэш строки вывода не печатаются вовсе.

Это делает опечатки опасными:

bash
cat > Dockerfile.typo <<'EOF'
FROM alpine:3.21
ARG APP_VERSION=1.0
RUN echo "версия: [${APP_VERSIN}]"
CMD ["true"]
EOF

docker build --no-cache -f Dockerfile.typo -t typo:1 . 2>&1 \
    | grep -E '^#[0-9]+ [0-9.]+ .*версия'
text
#5 0.147 версия: []

Опечатка APP_VERSIN не вызвала ошибки. Сборка прошла успешно с пустым значением.

ARG и ENV: что попадает в образ

bash
cat > Dockerfile.argenv <<'EOF'
FROM alpine:3.21
ARG BUILD_ONLY="виден только при сборке"
ENV RUNTIME_VAR="виден в container"
RUN echo "при сборке ARG: ${BUILD_ONLY}"
RUN echo "при сборке ENV: ${RUNTIME_VAR}"
CMD ["sh", "-c", "echo \"в container ARG: [${BUILD_ONLY}]\"; echo \"в container ENV: [${RUNTIME_VAR}]\""]
EOF

docker build -q -f Dockerfile.argenv -t argenv:1 . > /dev/null
echo "--- при запуске ---"
docker run --rm argenv:1
text
--- при запуске ---
в container ARG: []
в container ENV: [виден в container]

ARG при сборке работал, в container исчез. ENV доступен и там, и там.

Проверим конфигурацию образа:

bash
docker image inspect argenv:1 --format '{{range .Config.Env}}{{println .}}{{end}}'
text
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUNTIME_VAR=виден в container

BUILD_ONLY отсутствует — в конфигурацию не попал.

Почему ARG не годится для секретов

bash
cat > Dockerfile.secret <<'EOF'
FROM alpine:3.21
ARG DB_PASSWORD
RUN echo "подключаюсь с паролем длиной ${#DB_PASSWORD}" > /log.txt
CMD ["cat", "/log.txt"]
EOF

docker build -q -f Dockerfile.secret \
    --build-arg DB_PASSWORD='SuperSecret123!' \
    -t leak:1 . > /dev/null

echo "--- в container переменной нет ---"
docker run --rm leak:1
docker run --rm leak:1 sh -c 'echo "[${DB_PASSWORD}]"' 2>/dev/null || \
    docker run --rm --entrypoint sh leak:1 -c 'echo "[${DB_PASSWORD:-отсутствует}]"'

echo
echo "--- но в истории образа он есть ---"
docker history --no-trunc leak:1 | grep -o 'DB_PASSWORD=[^ ]*' | head -1
text
--- в container переменной нет ---
подключаюсь с паролем длиной 15
[отсутствует]

--- но в истории образа он есть ---
DB_PASSWORD=SuperSecret123!

Пароль извлечён из образа одной командой. Никакого доступа к процессу сборки не потребовалось — достаточно самого образа.

Правильное решение — secret mounts:

bash
cat > Dockerfile.safe <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=secret,id=dbpass \
    echo "подключаюсь с паролем длиной $(wc -c < /run/secrets/dbpass)" > /log.txt
CMD ["cat", "/log.txt"]
EOF

echo -n 'SuperSecret123!' > pass.txt
docker build -q -f Dockerfile.safe --secret id=dbpass,src=pass.txt -t safe:1 . > /dev/null

docker run --rm safe:1
echo "--- поиск секрета в истории ---"
docker history --no-trunc safe:1 | grep -c 'SuperSecret' || echo "не найден"
rm -f pass.txt
text
подключаюсь с паролем длиной 15
--- поиск секрета в истории ---
не найден

Тот же результат сборки, секрет не сохранён.

Приоритет ENV и ARG: решает порядок, а не вид инструкции

Правило «ENV перекрывает ARG того же имени» встречается часто и в таком виде неверно. Проверим тремя сборками:

bash
prio() {   # $1 — тело Dockerfile
    printf '%s\n' "$1" > Dockerfile.prio
    docker build --no-cache -f Dockerfile.prio --build-arg APP_ENV=development \
        -t prio:1 . 2>&1 | grep -o 'значение: [a-z]*' | tail -1
    printf '   в образе: '
    docker run --rm --entrypoint sh prio:1 -c 'echo "${APP_ENV:-(нет)}"'
}

echo "── ENV, потом ARG ──"
prio 'FROM alpine:3.21
ENV APP_ENV=production
ARG APP_ENV
RUN echo "значение: ${APP_ENV}"'

echo "── ARG, потом ENV ──"
prio 'FROM alpine:3.21
ARG APP_ENV
ENV APP_ENV=production
RUN echo "значение: ${APP_ENV}"'

echo "── только ARG ──"
prio 'FROM alpine:3.21
ARG APP_ENV=production
RUN echo "значение: ${APP_ENV}"'
text
── ENV, потом ARG ──
значение: development
   в образе: production
── ARG, потом ENV ──
значение: production
   в образе: production
── только ARG ──
значение: development
   в образе: (нет)

Три вывода, каждый со своим следствием:

ПорядокЧто видит RUNЧто остаётся в образе
ENVARGзначение из --build-argзначение из ENV
ARGENVзначение из ENVзначение из ENV
только ARGзначение из --build-argничего

Побеждает та инструкция, что объявлена последней — до следующего переопределения. ENV при этом всегда попадает в образ, а ARG — никогда.

Отсюда неприятное следствие для второй строки таблицы: RUN при сборке видел production, а --build-arg был передан и молча ни на что не повлиял. Это и есть частая причина недоумения «почему --build-arg не работает» — но происходит она не всегда, а только при таком порядке.

Правильная передача из build-time в runtime — обратный порядок:

bash
cat > Dockerfile.prio2 <<'EOF'
FROM alpine:3.21
ARG APP_ENV=production
ENV APP_ENV=${APP_ENV}
CMD ["sh", "-c", "echo APP_ENV=${APP_ENV}"]
EOF

docker build -q -f Dockerfile.prio2 --build-arg APP_ENV=development -t prio:2 . > /dev/null
docker run --rm prio:2
text
APP_ENV=development

WORKDIR: почему не RUN cd

bash
cat > Dockerfile.wd <<'EOF'
FROM alpine:3.21
RUN mkdir -p /data
RUN cd /data
RUN echo "после RUN cd: $(pwd)"
WORKDIR /data
RUN echo "после WORKDIR: $(pwd)"
CMD ["true"]
EOF

docker build --no-cache -f Dockerfile.wd -t wd:1 . 2>&1 \
    | grep -E '^#[0-9]+ [0-9.]+ .*после'
text
#7 0.156 после RUN cd: /
#9 0.148 после WORKDIR: /data

RUN cd /data не подействовал: каждая инструкция RUN выполняется в отдельном процессе, и смена каталога умирает вместе с ним.

WORKDIR и владелец каталога

bash
cat > Dockerfile.owner <<'EOF'
FROM alpine:3.21
RUN adduser -D -u 10001 appuser

# Вариант A: WORKDIR до USER
WORKDIR /app-a
RUN echo "A: владелец $(stat -c '%U' /app-a)"

# Вариант B: USER до WORKDIR
USER appuser
WORKDIR /app-b
RUN echo "B: владелец $(stat -c '%U' /app-b)"
CMD ["true"]
EOF

docker build -f Dockerfile.owner -t owner:1 . 2>&1 | grep -E '^#[0-9]+ [0-9.]+ [AB]:'
text
#7 0.163 A: владелец root
#10 0.171 B: владелец appuser

Каталог, созданный WORKDIR, принадлежит текущему USER. Последствие варианта A:

bash
cat > Dockerfile.perm <<'EOF'
FROM alpine:3.21
RUN adduser -D -u 10001 appuser
WORKDIR /app
USER appuser
RUN touch /app/test.txt || echo "ОШИБКА: нет прав на запись"
CMD ["true"]
EOF

docker build --no-cache -f Dockerfile.perm -t perm:1 . 2>&1 \
    | grep -E '^#[0-9]+ [0-9.]+ .*(ОШИБКА|touch)'
text
#8 0.158 ОШИБКА: нет прав на запись

Исправление — явный chown:

bash
cat > Dockerfile.perm-fixed <<'EOF'
FROM alpine:3.21
RUN adduser -D -u 10001 appuser
WORKDIR /app
RUN chown appuser:appuser /app
USER appuser
RUN touch /app/test.txt && echo "запись работает"
CMD ["true"]
EOF

docker build --no-cache -f Dockerfile.perm-fixed -t perm:2 . 2>&1 \
    | grep -E '^#[0-9]+ [0-9.]+ .*запись работает'
text
#9 0.161 запись работает

EXPOSE ничего не публикует

bash
cat > Dockerfile.expose <<'EOF'
FROM python:3.13-slim
EXPOSE 8000
CMD ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
EOF

docker build -q -f Dockerfile.expose -t exp:1 . > /dev/null

docker run -d --name exp-test exp:1 > /dev/null
sleep 2

echo "--- порты в docker ps ---"
docker ps --filter name=exp-test --format '{{.Ports}}'

echo "--- доступен ли с host? ---"
curl -s -m 2 -o /dev/null -w '%{http_code}\n' http://localhost:8000/ 2>/dev/null \
    || echo "недоступен"

docker rm -f exp-test > /dev/null
text
--- порты в docker ps ---
8000/tcp
--- доступен ли с host? ---
недоступен

Порт объявлен, но не опубликован. Публикация требует -p:

bash
docker run -d --name exp-pub -p 8080:8000 exp:1 > /dev/null
sleep 2
docker ps --filter name=exp-pub --format '{{.Ports}}'
curl -s -m 2 -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:8080/
docker rm -f exp-pub > /dev/null
text
0.0.0.0:8080->8000/tcp
HTTP 200

Единственный функциональный эффект EXPOSE — работа флага -P:

bash
docker run -d --name exp-P -P exp:1 > /dev/null
sleep 1
docker port exp-P
docker rm -f exp-P > /dev/null
text
8000/tcp -> 0.0.0.0:32768

Docker выбрал случайный свободный порт host. Без EXPOSE флаг -P не нашёл бы, что публиковать.

VOLUME: запись после объявления теряется

bash
cat > Dockerfile.vol <<'EOF'
FROM alpine:3.21
RUN mkdir -p /data
RUN echo "записано ДО VOLUME" > /data/before.txt
VOLUME ["/data"]
RUN echo "записано ПОСЛЕ VOLUME" > /data/after.txt
CMD ["ls", "-1", "/data"]
EOF

docker build -q -f Dockerfile.vol -t vol:1 . > /dev/null
docker run --rm vol:1
text
before.txt

Файл after.txt отсутствует. Инструкция RUN после VOLUME записала его во временный volume, который был отброшен.

Это одна из самых неочевидных ловушек: сборка проходит без ошибок, а файл пропадает.

Проверим создание анонимных volumes:

bash
BEFORE="$(docker volume ls -q | wc -l)"
docker run -d --name vol-test vol:1 sleep 30 > /dev/null
AFTER="$(docker volume ls -q | wc -l)"
echo "volumes до: $BEFORE, после: $AFTER"

docker inspect vol-test --format '{{range .Mounts}}{{.Type}} {{.Name}}{{end}}'
docker rm -f vol-test > /dev/null

echo "после docker rm БЕЗ -v:"
docker volume ls -q | wc -l
text
volumes до: 0, после: 1
volume 8f7e6d5c4b3a2918077665544332211aabbccddeeff00112233445566778899
после docker rm БЕЗ -v:
1

Анонимный volume остался в системе. При регулярном пересоздании containers такие volumes накапливаются — это источник неожиданно занятого диска (урок 3.4).

bash
docker volume prune -f > /dev/null

SHELL и pipefail

bash
cat > Dockerfile.pipe <<'EOF'
FROM debian:trixie-slim
# Без pipefail: код возврата берётся от последней команды конвейера
RUN false | echo "конвейер 'прошёл' несмотря на ошибку"
CMD ["true"]
EOF

docker build -q -f Dockerfile.pipe -t pipe:1 . > /dev/null && \
    echo "сборка УСПЕШНА, хотя первая команда упала"
text
конвейер 'прошёл' несмотря на ошибку
сборка УСПЕШНА, хотя первая команда упала

С pipefail:

bash
cat > Dockerfile.pipe2 <<'EOF'
FROM debian:trixie-slim
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN false | echo "эта сборка должна упасть"
CMD ["true"]
EOF

docker build -q -f Dockerfile.pipe2 -t pipe:2 . > /dev/null 2>&1 \
    && echo "собралось" \
    || echo "сборка упала — ошибка в конвейере обнаружена"
text
сборка упала — ошибка в конвейере обнаружена

Практическая ценность: конструкция RUN curl -fsSL URL | tar -xz без pipefail не заметит, что скачивание не удалось, и продолжит сборку с неполными данными.

В Alpine нет bash. Там либо доустановите его, либо используйте set -o pipefail внутри RUN с явным /bin/sh, если оболочка это поддерживает (BusyBox ash поддерживает).

LABEL и OCI-аннотации

bash
cat > Dockerfile.label <<'EOF'
FROM alpine:3.21
LABEL org.opencontainers.image.title="Пример сервиса" \
      org.opencontainers.image.description="Демонстрация LABEL" \
      org.opencontainers.image.authors="team@example.com" \
      org.opencontainers.image.source="https://github.com/example/app" \
      org.opencontainers.image.licenses="MIT" \
      org.opencontainers.image.version="1.4.2"
CMD ["true"]
EOF

docker build -q -f Dockerfile.label -t label:1 . > /dev/null
docker image inspect label:1 --format '{{range $k, $v := .Config.Labels}}{{printf "%-42s %s\n" $k $v}}{{end}}'
text
org.opencontainers.image.authors           team@example.com
org.opencontainers.image.description       Демонстрация LABEL
org.opencontainers.image.licenses          MIT
org.opencontainers.image.source            https://github.com/example/app
org.opencontainers.image.title             Пример сервиса
org.opencontainers.image.version           1.4.2

Метка org.opencontainers.image.source имеет практический эффект: GitHub Container Registry связывает по ней образ с репозиторием.

Фильтрация по меткам:

bash
docker images --filter 'label=org.opencontainers.image.licenses=MIT' --format '{{.Repository}}:{{.Tag}}'

Predefined ARG для multi-platform

bash
cat > Dockerfile.platform <<'EOF'
FROM alpine:3.21
ARG TARGETPLATFORM
ARG TARGETARCH
ARG BUILDPLATFORM
RUN echo "собрано на: ${BUILDPLATFORM}" \
 && echo "цель:       ${TARGETPLATFORM}" \
 && echo "архитектура: ${TARGETARCH}"
CMD ["true"]
EOF

docker build --no-cache -f Dockerfile.platform -t plat:1 . 2>&1 \
    | grep -E '^#[0-9]+ [0-9.]+ .*(собрано|цель|архитектура)'
text
#5 0.164 собрано на: linux/amd64
#5 0.164 цель:       linux/amd64
#5 0.164 архитектура: amd64

Типичное применение — загрузка бинарного файла под нужную архитектуру:

dockerfile
FROM alpine:3.21
ARG TARGETARCH
RUN apk add --no-cache curl \
 && curl -fsSL "https://example.com/tool-linux-${TARGETARCH}.tar.gz" -o /tmp/tool.tar.gz \
 && tar -xzf /tmp/tool.tar.gz -C /usr/local/bin \
 && rm /tmp/tool.tar.gz
CMD ["tool", "--version"]

Уборка

bash
cd /tmp
docker rmi -f scope:1 typo:1 argenv:1 leak:1 safe:1 prio:1 prio:2 wd:1 \
    owner:1 perm:1 perm:2 exp:1 vol:1 pipe:1 pipe:2 label:1 plat:1 2>/dev/null || true
docker volume prune -f > /dev/null
rm -rf /tmp/base-instr

Практическое упражнение

Задание. Дан Dockerfile с восемью ошибками уровня базовых инструкций. Найдите их все, объясните последствия каждой и напишите исправленную версию.

dockerfile
FROM python:latest

MAINTAINER dev@example.com

ARG DB_PASSWORD
ENV DB_PASSWORD=${DB_PASSWORD}
ENV PATH="/app/bin"

RUN cd /app
WORKDIR /app

USER appuser
RUN apt-get update && apt-get install -y curl

LABEL version="1.0"
LABEL description="API"
LABEL team="backend"

VOLUME ["/app/data"]
RUN mkdir -p /app/data && echo "init" > /app/data/state.txt

EXPOSE 8000
CMD ["python", "app.py"]

Для каждой ошибки укажите: в чём проблема, что произойдёт на практике, как исправить.

Подсказки

Подсказка 1

Три ошибки связаны с порядком инструкций: RUN cd, USER перед установкой пакетов, RUN после VOLUME.

Подсказка 2

Две ошибки связаны с переменными: секрет и переопределение PATH.

Подсказка 3

Проверить попадание секрета в образ: docker history --no-trunc <image> | grep DB_PASSWORD.

Решение

Сначала выполните задание самостоятельно.

Показать решение

Найденные ошибки.

СтрокаПроблемаПоследствие
1FROM python:latestНет фиксации версииСборка невоспроизводима; версия Python меняется без предупреждения
2MAINTAINERУстаревшая инструкцияDeprecated с Docker 1.13
3ENV DB_PASSWORD=${DB_PASSWORD}Секрет в образеВиден в docker inspect, в истории и в окружении процессов
4ENV PATH="/app/bin"PATH перезаписанСистемные утилиты недоступны: python, sh, ls не найдутся
5RUN cd /appСмена каталога не сохраняетсяИнструкция не делает ничего
6USER appuser перед apt-getНет прав на установкуСборка упадёт; к тому же пользователь не создан
7Три отдельных LABELТри записи в историиМелочь, но лишние слои конфигурации
8RUN после VOLUMEЗапись во временный volumeФайл state.txt не попадёт в образ

Дополнительно: пользователь appuser нигде не создаётся — инструкция USER сошлётся на несуществующего пользователя.

Проверка ошибки 4:

bash
mkdir -p /tmp/ex-fix && cd /tmp/ex-fix
cat > Dockerfile.broken <<'EOF'
FROM python:3.13-slim
ENV PATH="/app/bin"
RUN echo "проверка"
CMD ["true"]
EOF
docker build -f Dockerfile.broken -t broken:1 . 2>&1 | tail -3
text
 => ERROR [2/2] RUN echo "проверка"
------
process "/bin/sh -c echo \"проверка\"" did not complete successfully: exit code: 127

Код 127 — оболочка не нашла echo, потому что /bin больше не в PATH.

Проверка ошибки 8:

bash
cat > Dockerfile.vol <<'EOF'
FROM alpine:3.21
VOLUME ["/data"]
RUN mkdir -p /data && echo "init" > /data/state.txt
CMD ["ls", "-1", "/data"]
EOF
docker build -q -f Dockerfile.vol -t volbroken:1 . > /dev/null
docker run --rm volbroken:1
echo "(пусто — файл потерян)"
text

(пусто — файл потерян)

Исправленная версия.

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251

# 2, 7: одна инструкция LABEL со стандартными аннотациями OCI
LABEL org.opencontainers.image.authors="dev@example.com" \
      org.opencontainers.image.version="1.0" \
      org.opencontainers.image.description="API" \
      org.opencontainers.image.vendor="backend"

# 4: PATH дополняется, а не перезаписывается
ENV PATH="/app/bin:${PATH}" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

# 6: пакеты ставятся от root, до переключения пользователя
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

# 6: пользователь создаётся явно, с фиксированным UID
RUN useradd --create-home --uid 10001 appuser

# 5: WORKDIR вместо RUN cd
WORKDIR /app

# 8: данные создаются ДО объявления volume (если объявление вообще нужно)
RUN mkdir -p /app/data \
 && echo "init" > /app/data/state.txt \
 && chown -R appuser:appuser /app

# 3: секрет НЕ попадает в образ — передаётся при запуске.
#    Если он нужен при сборке, используйте:
#    RUN --mount=type=secret,id=dbpass ...

USER 10001:10001

EXPOSE 8000
CMD ["python", "app.py"]

Что изменилось по пунктам.

Ошибка 1. Тег плюс digest: тег для читаемости, digest для гарантии (урок 3.3).

Ошибка 3. Секрет убран полностью. Если он нужен только при сборке — secret mount; если в работе приложения — передаётся при запуске через -e или файл, но не встраивается в образ.

Ошибка 4. ENV PATH="/app/bin:${PATH}" — прежнее значение сохранено. Проверить:

bash
docker run --rm <image> sh -c 'echo $PATH'

Ошибка 8. Инструкция VOLUME вообще удалена. Причина: она создаёт анонимные volumes и ограничивает пользователя образа. Точку монтирования лучше задавать при запуске:

bash
docker run -v appdata:/app/data <image>

Если объявление всё же нужно, оно должно идти после всех записей в каталог.

Проверка исправленного варианта:

bash
cat > Dockerfile.fixed <<'EOF'
FROM python:3.13-slim
LABEL org.opencontainers.image.authors="dev@example.com" \
      org.opencontainers.image.version="1.0"
ENV PATH="/app/bin:${PATH}" \
    PYTHONUNBUFFERED=1
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --uid 10001 appuser
WORKDIR /app
RUN mkdir -p /app/data \
 && echo "init" > /app/data/state.txt \
 && chown -R appuser:appuser /app
USER 10001:10001
EXPOSE 8000
CMD ["python", "-c", "print('работает')"]
EOF

docker build -q -f Dockerfile.fixed -t fixed:1 . > /dev/null

echo "1. Запуск:"
docker run --rm fixed:1

echo "2. Пользователь:"
docker run --rm fixed:1 id

echo "3. Файл state.txt на месте:"
docker run --rm --entrypoint cat fixed:1 /app/data/state.txt

echo "4. PATH не сломан:"
docker run --rm --entrypoint sh fixed:1 -c 'echo $PATH'

echo "5. Секретов в истории нет:"
docker history --no-trunc fixed:1 | grep -ci 'password' || echo "0"
text
1. Запуск:
работает
2. Пользователь:
uid=10001(appuser) gid=10001(appuser) groups=10001(appuser)
3. Файл state.txt на месте:
init
4. PATH не сломан:
/app/bin:/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin
5. Секретов в истории нет:
0

Все пять проверок проходят.

bash
cd /tmp && docker rmi -f broken:1 volbroken:1 fixed:1 2>/dev/null; rm -rf /tmp/ex-fix

Общий принцип, объединяющий шесть из восьми ошибок. Инструкции Dockerfile выполняются последовательно, и порядок — не стилистика, а семантика. USER действует на всё последующее, VOLUME — тоже, WORKDIR создаёт каталог от имени текущего пользователя. Читая чужой Dockerfile, проверяйте не только набор инструкций, но и их взаимное расположение.

Проверка результата

bash
mkdir -p /tmp/verify && cd /tmp/verify
cat > Dockerfile <<'EOF'
FROM alpine:3.21
ARG BUILD_ARG="только сборка"
ENV RUNTIME_VAR="и сборка, и запуск"
CMD ["sh", "-c", "echo ARG=[$BUILD_ARG] ENV=[$RUNTIME_VAR]"]
EOF
docker build -q -t verify:1 . > /dev/null && docker run --rm verify:1
docker rmi -f verify:1 > /dev/null; cd /tmp && rm -rf /tmp/verify

Ожидается ARG=[] ENV=[и сборка, и запуск]. Если вы можете объяснить, почему первое значение пусто, материал усвоен.

Типичные ошибки

ОшибкаПричинаИсправление
Секрет через --build-argКажется, что ARG не попадает в образЗначение остаётся в истории; использовать secret mounts
ENV PATH="/app/bin"Забыто прежнее значениеENV PATH="/app/bin:${PATH}"
RUN cd /app вместо WORKDIRПривычка из shellКаждый RUN — новый процесс; использовать WORKDIR
ARG до FROM используется в стадииКажется глобальнымОбъявить повторно внутри стадии
Опечатка в имени переменнойПустая подстановка без ошибкиПроверять фактическое значение через RUN echo
--build-arg не действуетВ стадии есть ENV с тем же именемENV имеет приоритет; поменять порядок
USER перед apt-get installПорядок кажется неважнымПакеты ставятся от root, USER — в конце
WORKDIR до USER без chownКаталог принадлежит rootДобавить chown или переставить USER
Ожидание, что EXPOSE откроет портНазвание вводит в заблуждениеЭто метаданные; публикация через -p
RUN после VOLUMEНеочевидное поведениеЗапись теряется; писать до объявления
VOLUME в образе приложенияКопирование из образов баз данныхСоздаёт анонимные volumes; лучше задавать при запуске
MAINTAINERСтарые примерыLABEL org.opencontainers.image.authors
Несколько LABEL подрядЧитается лучшеОбъединить в одну инструкцию
SHELL ["/bin/bash", ...] в Alpinebash отсутствуетExit code 127; доустановить или использовать sh

Контрольные вопросы

На понимание:

  1. Почему ARG, объявленный до FROM, недоступен внутри стадии?
  2. Почему секрет, переданный через --build-arg, извлекается из готового образа?
  3. Что произойдёт при ENV PATH="/app/bin" и почему?
  4. Почему RUN cd /app не меняет каталог для следующей инструкции?
  5. Почему RUN, идущий после VOLUME, не сохраняет записанные файлы?

На применение:

  1. Как передать значение из --build-arg в переменную окружения работающего container?
  2. Как обеспечить, чтобы каталог приложения принадлежал непривилегированному пользователю?
  3. Как включить pipefail для всех инструкций RUN в образе на базе Debian?

На диагностику:

  1. docker build --build-arg APP_ENV=dev не влияет на результат — в container всегда production. Причина?
  2. Сборка падает с exit code 127 на первой же инструкции RUN echo. Что проверить?

Краткое резюме

  1. Инструкции делятся на создающие слой (FROM, RUN, COPY, ADD) и изменяющие конфигурацию — вторые имеют размер 0B.
  2. ARG доступен только при сборке, ENV — и при сборке, и в container.
  3. Значение ARG не попадает в конфигурацию образа, но сохраняется в истории — для секретов непригоден.
  4. ARG до FROM доступен только в инструкциях FROM; внутри стадии нужно объявить повторно.
  5. При совпадении имён ENV имеет приоритет над ARG.
  6. WORKDIR создаёт каталог от имени текущего USER — порядок инструкций определяет владельца.
  7. RUN cd не работает: каждая инструкция выполняется в отдельном процессе.
  8. EXPOSE — только метаданные; публикацию выполняет -p или -P.
  9. RUN после VOLUME теряет записанные данные; сама инструкция VOLUME в образах приложений обычно избыточна.
  10. MAINTAINER устарел, ONBUILD практически не применяется — используйте LABEL и общие стадии.

Официальные источники

ИсточникСсылкаЧто подтверждает
Dockerfile referencehttps://docs.docker.com/reference/dockerfile/Синтаксис всех инструкций, области видимости ARG, приоритет ENV, predefined ARG, deprecated MAINTAINER и ENV key value
Dockerfile reference: ARGhttps://docs.docker.com/reference/dockerfile/#argОбласть видимости до и после FROM, попадание в историю
Dockerfile reference: ENVhttps://docs.docker.com/reference/dockerfile/#envФорма key=value, сохранение в конфигурации образа
Dockerfile reference: VOLUMEhttps://docs.docker.com/reference/dockerfile/#volumeПотеря данных при записи после объявления, анонимные volumes
Dockerfile reference: EXPOSEhttps://docs.docker.com/reference/dockerfile/#exposeИнструкция как метаданные, взаимодействие с -P
Dockerfile reference: SHELLhttps://docs.docker.com/reference/dockerfile/#shellЗамена оболочки для shell form
Dockerfile reference: ONBUILDhttps://docs.docker.com/reference/dockerfile/#onbuildОтложенное выполнение и ограничения
Building best practiceshttps://docs.docker.com/build/building/best-practices/Рекомендации по LABEL, USER, WORKDIR, EXPOSE
Build secretshttps://docs.docker.com/build/building/secrets/Почему ARG непригоден для секретов
OCI Image Spec: annotationshttps://github.com/opencontainers/image-spec/blob/main/annotations.mdСтандартные метки org.opencontainers.image.*
Multi-platform buildshttps://docs.docker.com/build/building/multi-platform/Predefined ARG TARGETPLATFORM, TARGETARCH, BUILDPLATFORM

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → COPY, ADD и RUN
Главное оглавление

Markdown на GitHub ↗