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
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 устанавливает переменные окружения. Они видны:
- В последующих RUN-инструкциях (можно использовать
$VAR) - В CMD/ENTRYPOINT
- В запущенном контейнере (это главное отличие от 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
НИКОГДА не зашивай 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) .
Приоритеты 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