Learning Platform
Глоссарий Troubleshooting
Урок 08.04 · 22 мин
Начальный
dockerdockerfileworkdirenvarg

WORKDIR, ENV, ARG: настройка контейнера

В прошлых уроках мы строили образ из FROM, COPY, RUN, CMD. В этом разберём три инструкции, которые отвечают за «настройку» контейнера: где идёт работа (WORKDIR), какие переменные окружения видны во время сборки и запуска (ENV), и какие параметры передаются только на build (ARG).

Особо важно понять разницу между ENV и ARG — это classic source of confusion.


FHS: структура файловой системы Linux

WORKDIR: рабочая директория

WORKDIR устанавливает текущую директорию для всех последующих инструкций (RUN, COPY, ADD, CMD, ENTRYPOINT), а также для процесса при docker run.

FROM python:3.13-slim
WORKDIR /app
COPY . .              # копирует в /app (потому что WORKDIR /app)
RUN pip install ...    # выполнено в /app
CMD ["python", "main.py"]   # запущено в /app

Без WORKDIR cwd внутри контейнера = / (корень) или то, что задано в базовом образе. Это создаёт ад для путей: COPY . . копирует в /, RUN-команды работают в /, CMD читает файлы из /. Лучше явно WORKDIR /app.

Особенности:

1. Создаёт директорию если её нет.

WORKDIR /opt/myapp/data    # создаст /opt, /opt/myapp, /opt/myapp/data если их нет

Эквивалент RUN mkdir -p /opt/myapp/data && cd /opt/myapp/data, но в одну строку.

2. Сохраняется между инструкциями.

WORKDIR /app
RUN pwd          # /app
WORKDIR src
RUN pwd          # /app/src  -- relative path добавляется к предыдущему
WORKDIR /usr/local/bin
RUN pwd          # /usr/local/bin  -- absolute path переопределяет

3. Применяется к CMD/ENTRYPOINT.

WORKDIR /app
CMD ["python", "main.py"]
# При docker run: python запускается с cwd=/app, читает main.py относительно /app

Правило: всегда WORKDIR с абсолютным путём ближе к началу Dockerfile. Не использовать cd в RUN — между RUN’ами cwd сбрасывается (каждый RUN — это новый процесс).

# Плохо: cd в RUN не работает между RUN'ами
RUN cd /app && pip install -r requirements.txt
RUN pwd   # / -- cd в предыдущем RUN не сохранился

# Хорошо: WORKDIR -- персистентно
WORKDIR /app
RUN pip install -r requirements.txt
RUN pwd   # /app
TIP

WORKDIR в Docker Desktop / OrbStack создаётся с UID:GID 0:0 (root). Если потом USER appuser и приложение пытается писать в WORKDIR — будет permission denied. Лечится через RUN chown appuser:appgroup /app перед USER (см. урок 07.03 user-and-security).


ENV: переменные окружения runtime

ENV устанавливает переменные окружения. Они видны:

  1. В последующих RUN-инструкциях (можно использовать $VAR)
  2. В CMD/ENTRYPOINT
  3. В запущенном контейнере (это главное отличие от ARG)
FROM python:3.13-slim

ENV APP_HOME=/app \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR $APP_HOME

COPY . .
RUN pip install -r requirements.txt

CMD ["python", "main.py"]

В этом примере:

  • APP_HOME=/app использован в WORKDIR (через $APP_HOME)
  • PYTHONUNBUFFERED=1 отключает буферизацию Python stdout — критично для логов в docker logs (иначе print() буферизуется до закрытия stdin)
  • PIP_NO_CACHE_DIR=1 отключает pip cache во всех RUN с pip install

При docker run все эти переменные доступны процессу:

docker run --rm myimage env | grep -E '^(APP_HOME|PYTHON)'
# APP_HOME=/app
# PYTHONUNBUFFERED=1
# ...

Переопределение в runtime через -e:

docker run -e LOG_LEVEL=DEBUG -e APP_HOME=/custom myimage

Или через --env-file:

# .env
LOG_LEVEL=DEBUG
DB_URL=postgresql://localhost/dev

docker run --env-file .env myimage

Хорошие практики для ENV в Dockerfile:

# Group в одну инструкцию (один слой)
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Не зашивать secrets!
# ENV DB_PASSWORD=secret   <-- НИКОГДА: попадёт в каждый слой образа
# Лучше: передавать DB_PASSWORD через docker run -e или secret mount

# Зашить только публичные defaults
ENV LOG_LEVEL=INFO \
    HTTP_PORT=8080
DANGER

НИКОГДА не зашивай secrets (passwords, API keys, tokens) в ENV в Dockerfile. Они попадают в каждый слой и в docker inspect — любой, у кого есть pull-доступ к образу, увидит секреты. Для secrets используй runtime injection через docker run -e или secret mounts (модуль 15).


ARG: build-time аргументы

ARG это переменные, доступные только во время build. В отличие от ENV, они не сохраняются в образе и не видны процессу при docker run.

ARG PYTHON_VERSION=3.13
ARG APP_VERSION

FROM python:${PYTHON_VERSION}-slim

LABEL version=$APP_VERSION

WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

CMD ["python", "main.py"]

Передача через docker build --build-arg:

docker build \
    --build-arg PYTHON_VERSION=3.12 \
    --build-arg APP_VERSION=v1.2.3 \
    -t myapp:v1.2.3 .

Особенности ARG:

1. ARG до FROM viден только в FROM.

ARG PYTHON_VERSION=3.13       # видна только в FROM ниже
FROM python:${PYTHON_VERSION}-slim

ARG APP_VERSION               # эта работает после FROM
LABEL version=$APP_VERSION

Это особенность Docker: ARG имеет scope, и нужно повторять её ниже FROM, если хочешь использовать в RUN/COPY/LABEL.

2. ARG может быть без default-значения.

ARG APP_VERSION   # без default -- обязателен через --build-arg
LABEL version=$APP_VERSION

Если ARG без default и не передан в build-arg, его значение будет пустым строкой (не error).

3. ARG не виден в runtime.

docker run myimage env | grep APP_VERSION
# (пусто -- ARG не доступен в runtime)

Если нужно сохранить значение в runtime, нужно скопировать ARG в ENV:

ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION   # копирование в ENV для runtime

4. ARG в кэше build.

При изменении значения ARG слой, использующий $ARG, перестраивается (cache miss). Это полезно для cache busting:

ARG CACHE_BUSTER=1
RUN apt-get update && apt-get install -y curl
# Если хочешь принудительно пере-сборку apt-get update: docker build --build-arg CACHE_BUSTER=$(date +%s) .
ENV vs ARG: scope и видимость
ENVbuild + runtimeВидна в build-инструкциях (RUN, COPY, CMD) И в runtime-процессе. Сохраняется в image config, видна в docker inspect.
ARGbuild onlyВидна только в build-инструкциях. НЕ сохраняется в image config, НЕ доступна в runtime. Чтобы сохранить -- скопировать ARG в ENV.
ENV: full lifecycleENV: и в build (RUN $VAR), и в run (process env).
ARG: build-onlyARG: только build. Аналог CMake -D VAR=value.

Приоритеты ARG vs ENV

Когда в Dockerfile определены оба с одним именем, ENV выигрывает в build-инструкциях:

ARG VERSION=arg-default
ENV VERSION=env-default

RUN echo $VERSION   # выведет env-default (ENV перекрывает ARG)

Если ARG не имеет default, а ENV имеет — будет use ENV value:

ARG VERSION
ENV VERSION=env-default

RUN echo $VERSION   # env-default

При docker build --build-arg VERSION=arg-from-cli:

ARG VERSION       # получит "arg-from-cli"
ENV VERSION=env-default
RUN echo $VERSION   # env-default (ENV всё ещё перекрывает)

Это запутывает. Правило: не используй одинаковые имена для ARG и ENV в одном Dockerfile.


Типичные ENV для Python-DE

В DE-практике эти ENV кладут почти в каждый Python-Dockerfile:

ENV \
    # Python
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    \
    # pip
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_DEFAULT_TIMEOUT=100 \
    \
    # locale (для UTF-8)
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    \
    # app
    APP_HOME=/app

Что это даёт:

  • PYTHONUNBUFFERED=1 — stdout/stderr не буферизуются. Логи появляются в docker logs сразу, а не после flush’а.
  • PYTHONDONTWRITEBYTECODE=1 — Python не создаёт __pycache__ директории. Меньше I/O, образ меньше.
  • PIP_NO_CACHE_DIR=1 — pip не сохраняет downloaded wheels в ~/.cache/pip. Экономит 50-200 MB.
  • PIP_DISABLE_PIP_VERSION_CHECK=1 — pip не делает HTTP-запрос к pypi.org для проверки своей версии (ускоряет каждый pip install).
  • LANG=C.UTF-8 — locale. Без неё некоторые библиотеки (pandas с UTF-8 CSV) могут вести себя странно.

Попробуй сам

Поэкспериментируй с ENV и ARG:

mkdir env-arg && cd env-arg

cat > Dockerfile <<'EOF'
ARG BASE_VERSION=3.13

FROM python:${BASE_VERSION}-slim

ARG APP_NAME=default-app
ENV APP_NAME=$APP_NAME
ENV ENV_ONLY=set-in-env

WORKDIR /app

RUN echo "Build: APP_NAME=$APP_NAME, ENV_ONLY=$ENV_ONLY"

CMD ["sh", "-c", "echo Runtime: APP_NAME=$APP_NAME, ENV_ONLY=$ENV_ONLY, ARG_ONLY=${ARG_ONLY:-not-set}"]
EOF

# Build с дефолтными ARG
docker build -t test:default .

# Запуск
docker run --rm test:default
# Runtime: APP_NAME=default-app, ENV_ONLY=set-in-env, ARG_ONLY=not-set
# Заметь: ARG_ONLY действительно not-set в runtime, потому что ARG не передаётся

# Build с custom ARG
docker build --build-arg APP_NAME=my-etl -t test:custom .
docker run --rm test:custom
# Runtime: APP_NAME=my-etl, ENV_ONLY=set-in-env

# Override ENV в runtime через -e
docker run --rm -e ENV_ONLY=runtime-override test:custom
# Runtime: APP_NAME=my-etl, ENV_ONLY=runtime-override

# Inspect показывает только ENV, не ARG
docker inspect test:custom --format '{{.Config.Env}}'
# [APP_NAME=my-etl ENV_ONLY=set-in-env PATH=...]

cd .. && rm -rf env-arg
docker rmi test:default test:custom

Проверка знанийKnowledge check
Команда добавила в Dockerfile ENV DB_PASSWORD=supersecret для упрощения локальной разработки. Образ push'нут в private GHCR. Какие риски и как сделать правильно?
ОтветAnswer
ENV сохраняется в image config и виден через docker inspect <image>. Любой, у кого есть pull-доступ к образу (включая бывших сотрудников, скомпрометированных PATs, CI-логи), увидит DB_PASSWORD. Даже если позже сделать ENV DB_PASSWORD='' -- старое значение остаётся в слое из истории. Image history immutable. Правильно: 1) Не хранить secrets в образе. 2) Передавать через docker run -e DB_PASSWORD=... (из vault / k8s secret / .env), либо 3) Использовать build-time secrets через RUN --mount=type=secret (модуль 8.02). Если секрет уже утёк -- ротировать (пересоздать) пароль в БД и пересобрать образ с обновлённой версией; старый образ удалить из registry.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём ключевое отличие ENV от ARG в Dockerfile?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5