5.2. Базовые инструкции
Цели
После этого материала вы сможете:
- применять по назначению
FROM,ARG,ENV,WORKDIR,LABEL,USER,EXPOSE,VOLUME,SHELL,STOPSIGNAL,ONBUILD; - объяснить различие
ARGиENVпо области видимости и по попаданию в образ; - объяснить, почему секрет, переданный через
ARG, извлекается из готового образа; - назвать для каждой инструкции типичную ошибку и случай, когда её применять не следует;
- прочитать чужой
Dockerfileи найти в нём проблемы уровня базовых инструкций.
Предварительные знания
- 5.1. Build context и
.dockerignore; - 3.1. Архитектура image — конфигурация образа;
- 4.4. Сигналы и graceful shutdown — для
STOPSIGNAL.
Ключевые термины
| Термин | Объяснение |
|---|---|
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 — главное различие урока
Обе задают переменные, но работают принципиально по-разному.
ARG | ENV | |
|---|---|---|
| Доступна на этапе сборки | да | да |
| Доступна в работающем 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.
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-slim
RUN echo "версия: ${PYTHON_VERSION}" # ПУСТО
Переменная, объявленная до FROM, находится вне какой-либо стадии. Она подставляется в саму инструкцию FROM, но внутри стадии недоступна.
Правило 2. Чтобы использовать её внутри стадии, объявите повторно.
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.
Практическое следствие: конструкция
ARG APP_ENV=development
ENV APP_ENV=${APP_ENV}
работает как ожидается — она передаёт значение из build-time в runtime. А вот
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):
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 влияет на владельца каталога.
USER appuser
WORKDIR /app # каталог принадлежит appuser
WORKDIR /app # каталог принадлежит root
USER appuser
RUN touch /app/f # Permission denied
Это одна из самых частых причин ошибок прав доступа в Python-образах — разбирается в разделе 06.
Инструкции
Для каждой: назначение, синтаксис, пример, типичная ошибка, когда применять не следует.
FROM
Назначение. Задаёт базовый образ и начинает новую стадию сборки.
Синтаксис.
FROM [--platform=<platform>] <image>[:<tag>|@<digest>] [AS <name>]
Пример.
FROM python:3.13-slim@sha256:bf503bb2243c5aad0aa951544dd60d165f992646441d35dea90893703fc26251 AS builder
Типичная ошибка. Использование latest или отсутствие тега. Сборка перестаёт быть воспроизводимой: сегодня и через месяц вы получите разные образы при том же коде (урок 3.3).
Когда не следует. Не указывайте --platform в FROM без необходимости: это жёстко привязывает стадию к платформе и ломает multi-platform сборку. Для кросс-компиляции используйте --platform=$BUILDPLATFORM только на стадии сборки, а финальную стадию оставляйте без указания.
ARG
Назначение. Переменная, доступная только на этапе сборки.
Синтаксис.
ARG <name>[=<default>]
Пример.
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.
Синтаксис.
ENV <key>=<value> [<key>=<value>...]
Форма ENV <key> <value> (без знака равенства) объявлена устаревшей в Docker 20.10 и не должна использоваться: она неоднозначна при нескольких присваиваниях.
Пример.
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.
Синтаксис.
WORKDIR /absolute/path
Пример.
WORKDIR /app
COPY . .
Типичная ошибка. Использование RUN cd /app вместо WORKDIR. Каждая инструкция RUN выполняется в новом процессе — смена каталога не сохраняется до следующей инструкции.
Когда не следует. С относительными путями: WORKDIR app создаст каталог относительно предыдущего WORKDIR, и результат зависит от контекста. Всегда используйте абсолютные пути.
USER
Назначение. Задаёт пользователя для последующих RUN, CMD, ENTRYPOINT.
Синтаксис.
USER <user>[:<group>]
USER <UID>[:<GID>]
Пример.
RUN useradd --create-home --uid 10001 appuser
USER 10001:10001
Типичная ошибка. Указание имени вместо UID. При монтировании volumes и в Kubernetes (runAsNonRoot) проверяется числовой UID; имя, отсутствующее в /etc/passwd целевой системы, вызывает проблемы.
Вторая ошибка — USER перед установкой пакетов: непривилегированный пользователь не сможет выполнить apt-get install.
Когда не следует. Не переключайтесь на непривилегированного пользователя раньше, чем выполнены операции, требующие прав. Порядок: установка пакетов → создание пользователя → chown нужных каталогов → USER.
EXPOSE
Назначение. Документирует порты, которые слушает приложение.
Синтаксис.
EXPOSE <port>[/<protocol>]
Пример.
EXPOSE 8000/tcp
Типичная ошибка. Ожидание, что EXPOSE публикует порт. Это только метаданные: порт не становится доступным с host. Публикация выполняется флагом -p при запуске или ключом ports в Compose (раздел 08).
Когда не следует. Инструкция необязательна и ничего не меняет функционально. Единственный практический эффект: docker run -P публикует все объявленные порты на случайные порты host, и некоторые инструменты используют эти метаданные. Отсутствие EXPOSE не мешает работе.
LABEL
Назначение. Метаданные образа: авторство, версия, источник, лицензия.
Синтаксис.
LABEL <key>=<value> [<key>=<value>...]
Пример. Стандартизованные аннотации OCI:
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.
Синтаксис.
VOLUME ["/path"]
Пример.
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.
Синтаксис.
STOPSIGNAL SIGQUIT
Пример.
STOPSIGNAL SIGQUIT
Типичная ошибка. Не задавать сигнал для приложений, у которых graceful shutdown привязан к нестандартному сигналу. Для nginx SIGTERM означает быстрое завершение, а SIGQUIT — корректное; официальный образ поэтому содержит STOPSIGNAL SIGQUIT.
Когда не следует. Если приложение корректно обрабатывает SIGTERM — значение по умолчанию верное, инструкция не нужна.
SHELL
Назначение. Меняет оболочку, используемую для shell form инструкций RUN, CMD, ENTRYPOINT.
Синтаксис.
SHELL ["executable", "parameters"]
Пример. Включение строгого режима для всех последующих RUN:
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
Типичная ошибка. Указание оболочки, отсутствующей в образе. В Alpine нет bash — будет ошибка с exit code 127 (урок 4.6).
Когда не следует. Без конкретной причины. Основная законная причина — pipefail: без него RUN cmd1 | cmd2 возвращает код только последней команды, и падение cmd1 останется незамеченным.
ONBUILD
Назначение. Откладывает выполнение инструкции до момента, когда образ будет использован как базовый.
Синтаксис.
ONBUILD <инструкция>
Пример.
ONBUILD COPY requirements.txt /app/
ONBUILD RUN pip install --no-cache-dir -r /app/requirements.txt
Типичная ошибка. Использование в обычных образах. Инструкции срабатывают неявно, и разработчик, использующий такой базовый образ, не понимает, откуда взялись файлы и почему сборка требует определённой структуры каталогов.
Когда не следует. Практически всегда. Механизм считается устаревшим подходом: он делает поведение неявным и плохо сочетается с multi-stage builds. Вместо него используйте общую базовую стадию (урок 5.7).
Команды и примеры
Подготовка
mkdir -p /tmp/base-instr && cd /tmp/base-instr
Область видимости ARG
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.]+ .*объявления'
#5 0.288 БЕЗ объявления: [3.13.14]
#6 0.384 ПОСЛЕ объявления: [3.13]
Первая строка не пуста, и это хуже, чем если бы была пуста. Ожидание «переменная вне области видимости — значит, пусто» не оправдалось: подставилось 3.13.14, чего в Dockerfile вообще не написано.
Значение пришло из базового образа:
docker run --rm python:3.13-slim env | grep PYTHON_VERSION
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обязателен: при попадании в кэш строки вывода не печатаются вовсе.
Это делает опечатки опасными:
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.]+ .*версия'
#5 0.147 версия: []
Опечатка APP_VERSIN не вызвала ошибки. Сборка прошла успешно с пустым значением.
ARG и ENV: что попадает в образ
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
--- при запуске ---
в container ARG: []
в container ENV: [виден в container]
ARG при сборке работал, в container исчез. ENV доступен и там, и там.
Проверим конфигурацию образа:
docker image inspect argenv:1 --format '{{range .Config.Env}}{{println .}}{{end}}'
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUNTIME_VAR=виден в container
BUILD_ONLY отсутствует — в конфигурацию не попал.
Почему ARG не годится для секретов
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
--- в container переменной нет ---
подключаюсь с паролем длиной 15
[отсутствует]
--- но в истории образа он есть ---
DB_PASSWORD=SuperSecret123!
Пароль извлечён из образа одной командой. Никакого доступа к процессу сборки не потребовалось — достаточно самого образа.
Правильное решение — secret mounts:
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
подключаюсь с паролем длиной 15
--- поиск секрета в истории ---
не найден
Тот же результат сборки, секрет не сохранён.
Приоритет ENV и ARG: решает порядок, а не вид инструкции
Правило «ENV перекрывает ARG того же имени» встречается часто и в таком виде неверно. Проверим тремя сборками:
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}"'
── ENV, потом ARG ──
значение: development
в образе: production
── ARG, потом ENV ──
значение: production
в образе: production
── только ARG ──
значение: development
в образе: (нет)
Три вывода, каждый со своим следствием:
| Порядок | Что видит RUN | Что остаётся в образе |
|---|---|---|
ENV → ARG | значение из --build-arg | значение из ENV |
ARG → ENV | значение из ENV | значение из ENV |
только ARG | значение из --build-arg | ничего |
Побеждает та инструкция, что объявлена последней — до следующего переопределения. ENV при этом всегда попадает в образ, а ARG — никогда.
Отсюда неприятное следствие для второй строки таблицы: RUN при сборке видел production, а --build-arg был передан и молча ни на что не повлиял. Это и есть частая причина недоумения «почему --build-arg не работает» — но происходит она не всегда, а только при таком порядке.
Правильная передача из build-time в runtime — обратный порядок:
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
APP_ENV=development
WORKDIR: почему не RUN cd
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.]+ .*после'
#7 0.156 после RUN cd: /
#9 0.148 после WORKDIR: /data
RUN cd /data не подействовал: каждая инструкция RUN выполняется в отдельном процессе, и смена каталога умирает вместе с ним.
WORKDIR и владелец каталога
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]:'
#7 0.163 A: владелец root
#10 0.171 B: владелец appuser
Каталог, созданный WORKDIR, принадлежит текущему USER. Последствие варианта A:
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)'
#8 0.158 ОШИБКА: нет прав на запись
Исправление — явный chown:
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.]+ .*запись работает'
#9 0.161 запись работает
EXPOSE ничего не публикует
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
--- порты в docker ps ---
8000/tcp
--- доступен ли с host? ---
недоступен
Порт объявлен, но не опубликован. Публикация требует -p:
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
0.0.0.0:8080->8000/tcp
HTTP 200
Единственный функциональный эффект EXPOSE — работа флага -P:
docker run -d --name exp-P -P exp:1 > /dev/null
sleep 1
docker port exp-P
docker rm -f exp-P > /dev/null
8000/tcp -> 0.0.0.0:32768
Docker выбрал случайный свободный порт host. Без EXPOSE флаг -P не нашёл бы, что публиковать.
VOLUME: запись после объявления теряется
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
before.txt
Файл after.txt отсутствует. Инструкция RUN после VOLUME записала его во временный volume, который был отброшен.
Это одна из самых неочевидных ловушек: сборка проходит без ошибок, а файл пропадает.
Проверим создание анонимных volumes:
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
volumes до: 0, после: 1
volume 8f7e6d5c4b3a2918077665544332211aabbccddeeff00112233445566778899
после docker rm БЕЗ -v:
1
Анонимный volume остался в системе. При регулярном пересоздании containers такие volumes накапливаются — это источник неожиданно занятого диска (урок 3.4).
docker volume prune -f > /dev/null
SHELL и pipefail
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 "сборка УСПЕШНА, хотя первая команда упала"
конвейер 'прошёл' несмотря на ошибку
сборка УСПЕШНА, хотя первая команда упала
С pipefail:
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 "сборка упала — ошибка в конвейере обнаружена"
сборка упала — ошибка в конвейере обнаружена
Практическая ценность: конструкция RUN curl -fsSL URL | tar -xz без pipefail не заметит, что скачивание не удалось, и продолжит сборку с неполными данными.
В Alpine нет
bash. Там либо доустановите его, либо используйтеset -o pipefailвнутриRUNс явным/bin/sh, если оболочка это поддерживает (BusyBoxashподдерживает).
LABEL и OCI-аннотации
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}}'
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 связывает по ней образ с репозиторием.
Фильтрация по меткам:
docker images --filter 'label=org.opencontainers.image.licenses=MIT' --format '{{.Repository}}:{{.Tag}}'
Predefined ARG для multi-platform
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.]+ .*(собрано|цель|архитектура)'
#5 0.164 собрано на: linux/amd64
#5 0.164 цель: linux/amd64
#5 0.164 архитектура: amd64
Типичное применение — загрузка бинарного файла под нужную архитектуру:
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"]
Уборка
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 с восемью ошибками уровня базовых инструкций. Найдите их все, объясните последствия каждой и напишите исправленную версию.
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.
Решение
Сначала выполните задание самостоятельно.
Показать решение
Найденные ошибки.
| № | Строка | Проблема | Последствие |
|---|---|---|---|
| 1 | FROM python:latest | Нет фиксации версии | Сборка невоспроизводима; версия Python меняется без предупреждения |
| 2 | MAINTAINER | Устаревшая инструкция | Deprecated с Docker 1.13 |
| 3 | ENV DB_PASSWORD=${DB_PASSWORD} | Секрет в образе | Виден в docker inspect, в истории и в окружении процессов |
| 4 | ENV PATH="/app/bin" | PATH перезаписан | Системные утилиты недоступны: python, sh, ls не найдутся |
| 5 | RUN cd /app | Смена каталога не сохраняется | Инструкция не делает ничего |
| 6 | USER appuser перед apt-get | Нет прав на установку | Сборка упадёт; к тому же пользователь не создан |
| 7 | Три отдельных LABEL | Три записи в истории | Мелочь, но лишние слои конфигурации |
| 8 | RUN после VOLUME | Запись во временный volume | Файл state.txt не попадёт в образ |
Дополнительно: пользователь appuser нигде не создаётся — инструкция USER сошлётся на несуществующего пользователя.
Проверка ошибки 4:
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
=> ERROR [2/2] RUN echo "проверка"
------
process "/bin/sh -c echo \"проверка\"" did not complete successfully: exit code: 127
Код 127 — оболочка не нашла echo, потому что /bin больше не в PATH.
Проверка ошибки 8:
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 "(пусто — файл потерян)"
(пусто — файл потерян)
Исправленная версия.
# 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}" — прежнее значение сохранено. Проверить:
docker run --rm <image> sh -c 'echo $PATH'
Ошибка 8. Инструкция VOLUME вообще удалена. Причина: она создаёт анонимные volumes и ограничивает пользователя образа. Точку монтирования лучше задавать при запуске:
docker run -v appdata:/app/data <image>
Если объявление всё же нужно, оно должно идти после всех записей в каталог.
Проверка исправленного варианта:
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"
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
Все пять проверок проходят.
cd /tmp && docker rmi -f broken:1 volbroken:1 fixed:1 2>/dev/null; rm -rf /tmp/ex-fix
Общий принцип, объединяющий шесть из восьми ошибок. Инструкции Dockerfile выполняются последовательно, и порядок — не стилистика, а семантика. USER действует на всё последующее, VOLUME — тоже, WORKDIR создаёт каталог от имени текущего пользователя. Читая чужой Dockerfile, проверяйте не только набор инструкций, но и их взаимное расположение.
Проверка результата
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", ...] в Alpine | bash отсутствует | Exit code 127; доустановить или использовать sh |
Контрольные вопросы
На понимание:
- Почему
ARG, объявленный доFROM, недоступен внутри стадии? - Почему секрет, переданный через
--build-arg, извлекается из готового образа? - Что произойдёт при
ENV PATH="/app/bin"и почему? - Почему
RUN cd /appне меняет каталог для следующей инструкции? - Почему
RUN, идущий послеVOLUME, не сохраняет записанные файлы?
На применение:
- Как передать значение из
--build-argв переменную окружения работающего container? - Как обеспечить, чтобы каталог приложения принадлежал непривилегированному пользователю?
- Как включить
pipefailдля всех инструкцийRUNв образе на базе Debian?
На диагностику:
docker build --build-arg APP_ENV=devне влияет на результат — в container всегдаproduction. Причина?- Сборка падает с exit code
127на первой же инструкцииRUN echo. Что проверить?
Краткое резюме
- Инструкции делятся на создающие слой (
FROM,RUN,COPY,ADD) и изменяющие конфигурацию — вторые имеют размер0B. ARGдоступен только при сборке,ENV— и при сборке, и в container.- Значение
ARGне попадает в конфигурацию образа, но сохраняется в истории — для секретов непригоден. ARGдоFROMдоступен только в инструкцияхFROM; внутри стадии нужно объявить повторно.- При совпадении имён
ENVимеет приоритет надARG. WORKDIRсоздаёт каталог от имени текущегоUSER— порядок инструкций определяет владельца.RUN cdне работает: каждая инструкция выполняется в отдельном процессе.EXPOSE— только метаданные; публикацию выполняет-pили-P.RUNпослеVOLUMEтеряет записанные данные; сама инструкцияVOLUMEв образах приложений обычно избыточна.MAINTAINERустарел,ONBUILDпрактически не применяется — используйтеLABELи общие стадии.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Dockerfile reference | https://docs.docker.com/reference/dockerfile/ | Синтаксис всех инструкций, области видимости ARG, приоритет ENV, predefined ARG, deprecated MAINTAINER и ENV key value |
| Dockerfile reference: ARG | https://docs.docker.com/reference/dockerfile/#arg | Область видимости до и после FROM, попадание в историю |
| Dockerfile reference: ENV | https://docs.docker.com/reference/dockerfile/#env | Форма key=value, сохранение в конфигурации образа |
| Dockerfile reference: VOLUME | https://docs.docker.com/reference/dockerfile/#volume | Потеря данных при записи после объявления, анонимные volumes |
| Dockerfile reference: EXPOSE | https://docs.docker.com/reference/dockerfile/#expose | Инструкция как метаданные, взаимодействие с -P |
| Dockerfile reference: SHELL | https://docs.docker.com/reference/dockerfile/#shell | Замена оболочки для shell form |
| Dockerfile reference: ONBUILD | https://docs.docker.com/reference/dockerfile/#onbuild | Отложенное выполнение и ограничения |
| Building best practices | https://docs.docker.com/build/building/best-practices/ | Рекомендации по LABEL, USER, WORKDIR, EXPOSE |
| Build secrets | https://docs.docker.com/build/building/secrets/ | Почему ARG непригоден для секретов |
| OCI Image Spec: annotations | https://github.com/opencontainers/image-spec/blob/main/annotations.md | Стандартные метки org.opencontainers.image.* |
| Multi-platform builds | https://docs.docker.com/build/building/multi-platform/ | Predefined ARG TARGETPLATFORM, TARGETARCH, BUILDPLATFORM |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → COPY, ADD и RUN
Главное оглавление