Як використовувати змінні середовища в Docker?
Environment-змінні це як Docker передає runtime-конфігурацію застосунку всередині container. Чотири місця, де їх ставити, кожне з різним scope і persistence. Знання різниць тримає secret поза image-history.
Теорія
TL;DR
ENVу Dockerfile = запечено у image. Видно у image-history назавжди. Для non-secret дефолтів.-e KEY=valueнаdocker run= per-container, не в image.--env-file file.env= bulk env з файлу. Той самий scope, що й-e.- Compose
environment:іenv_file:= runtime, project-scoped. ARGу Dockerfile ≠ env var. Build-time only, зникає у runtime.- Ніколи не використовуй env для secret у проді. Вони з'являються у
docker inspect,ps aux, image-history і багатьох логах.
ENV у Dockerfile
FROM node:22-alpine
ENV NODE_ENV=production
ENV PORT=3000
# АБО multi-line
ENV NODE_ENV=production \
PORT=3000 \
LOG_LEVEL=infoЗначення це частина image. Кожен, хто запускає image, отримує їх за замовчуванням. Override під час запуску:
docker run -e PORT=8080 myapp # PORT тепер 8080, NODE_ENV ще productionВажливо: ENV-значення з'являються у docker history і docker inspect. Ніколи не запікай secret тут.
-e і --env-file на docker run
# Одна змінна
docker run -e DATABASE_URL=postgres://... myapp
# Кілька
docker run -e DB_HOST=db -e DB_PORT=5432 -e DB_USER=postgres myapp
# Прокинути з shell (без value = читай host env)
export API_KEY=secret123
docker run -e API_KEY myapp
# Bulk з файлу
docker run --env-file .env myappФормат .env-файлу:
# Рядки з # це коментарі
DB_HOST=db
DB_PORT=5432
NODE_ENV=productionБез лапок, без shell-розгортання, один KEY=VALUE на рядок.
Compose: environment: і env_file:
services:
api:
image: myapp
environment:
NODE_ENV: production # map-форма
DATABASE_URL: postgres://...
DEBUG: "" # порожнє значення (різне від відсутнього)
env_file:
- .env # file-форма
- .env.local # пізніший override раніший
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD} # list-форма, з shell-style інтерполяцією
- POSTGRES_DB=${DB_NAME:-app} # default, якщо DB_NAME unsetCompose-інтерполяція: ${VAR} і ${VAR:-default} посилаються на змінні з твого shell або з файлу .env поряд з compose.yaml. Цей .env для самого Compose, використовується під час YAML-парсингу. Інакше, ніж env_file:, що передається у container.
ARG vs ENV
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine
ARG BUILD_VERSION # передано через --build-arg, доступно ПІД ЧАС білду тільки
RUN echo "Building $BUILD_VERSION" > /app/version.txt
ENV APP_VERSION=$BUILD_VERSION # щоб було доступно у runtime, копіюй ARG у ENVdocker build --build-arg BUILD_VERSION=1.2.3 -t myapp .ARG= build-time only. Зникає після завершенняdocker build.ENV= runtime. Живе у image і running container.- Поширений патерн: отримай значення як
ARG(щоб білд міг використати), скопіюй уENV(щоб runtime міг бачити).
Чому secret НЕ належить у env
docker run -e DB_PASSWORD=hunter2 myappВитікає:
docker inspect <container>показує env у plain textps auxeна host може показати env PID 1 container- Власний
/proc/1/environcontainer читається зсередини - Багато app-фреймворків логують повний env при старті (особливо у dev)
- Дочірні процеси успадковують env
Краще роби так:
- BuildKit secret-mount для build-time secret:
dockerfile
RUN cp /run/secrets/npmrc ~/.npmrc && npm ci - Docker Swarm secret або K8s secret, змонтовані як файли у runtime
- External secret-manager (AWS Secrets Manager, HashiCorp Vault), читаються при старті
- Для локального dev:
.env.localпоза git нормально
Типові помилки
Класти паролі у ENV Dockerfile
# НЕПРАВИЛЬНО: назавжди у image-history
ENV DB_PASSWORD=hunter2Пароль у image-manifest, видно усім, хто pull'ить image. Ніколи не запікай secret.
Лапки у .env-файлах
# НЕПРАВИЛЬНО: літеральні лапки стають частиною значення
VAR="value with spaces" # значення буквально `"value with spaces"`
# ПРАВИЛЬНО (Docker трактує значення як plain strings)
VAR=value with spacesDocker .env це не shell. Лапки не потрібні. Якщо ставиш лапки, вони стають частиною значення.
Забути, що .env для Compose-інтерполяції, env_file: для container
services:
api:
environment:
DB_PASSWORD: ${DB_PASSWORD} # ← читається з host-shell або .env при YAML-парсингу
env_file: .env.app # ← передається container як env-змінніДва різні файли, два різні scope. .env (дефолтний lookup Compose) для YAML; env_file: для container.
Спроба інтерполювати у runtime у ENV
ENV PATH=$PATH:/app/bin
# Працює: $PATH тут це попереднє ENV-значення
ENV DEBUG=$LOG_LEVEL
# Може НЕ працювати: залежить від ARG/ENV scoping під час білдуDockerfile-інтерполяція у момент ENV використовує build-time контекст, не runtime. Для runtime-композиції бери shell-wrapper або entrypoint-скрипт.
Реальне застосування
- Twelve-factor застосунки: уся config через env, усі зміни поведінки через env, без config-файлів у image. Compose / K8s передають env при деплої.
- CI/CD: secret інжектяться як env через CI secret-store (
GITHUB_TOKEN,DOCKERHUB_TOKEN). - Локальний dev:
.env.local(gitignored) тримає dev-override;compose.yamlпосилається через${VAR}. - Прод: secret через mounted-файли (Swarm secret, K8s secret, Vault sidecar), не сирий env. Non-secret config все одно через env.
Питання для поглиблення
Q: Який пріоритет, коли та сама змінна задана у кількох місцях?
A: Для Compose: env_file < environment у service < host shell-env. Пізніше виграє. Для docker run: флаг -e виграє над Dockerfile ENV.
Q: Як побачити effective env container?
A: docker exec <name> env. Показує runtime-env, включно з ENV з image і -e під час запуску.
Q: Чи можу зняти успадковану env-змінну?
A: Постав її порожнім: -e DEBUG=. Нота: порожнє не те саме, що unset; process.env.DEBUG буде "", не undefined. Щоб реально unset, збирай новий image без ENV DEBUG=..., Dockerfile ENV можна прибрати лише через відсутність у Dockerfile, не unset командою.
Q: Яка різниця між --env-file і Compose env_file:?
A: Та сама ідея. --env-file для docker run. Compose env_file: той самий механізм всередині YAML.
Q: (Senior) Як безпечно інжектити secret у Compose-прод-деплой?
A: Бери Docker Swarm secret (file-mounted, ніколи в env), або external secret-manager, що застосунок читає при старті, або sealed-secrets-підходи з init-container, що fetch і пишуть у tmpfs, який застосунок читає. Уникай plain env_file: з secret у plaintext. Лінія, яку не перетинаєш: secret у env або у image-шарах.
Приклади
Реалістичний Compose з правильно шарованим env
# compose.yaml
services:
api:
image: myapp:${TAG:-latest}
environment:
NODE_ENV: production
LOG_LEVEL: info
DATABASE_URL: postgres://api:${DB_PASSWORD}@db:5432/app
env_file:
- .env.app # додаткова non-secret config
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app# .env (Compose-інтерполяція)
TAG=v1.2.3
DB_PASSWORD=hunter2
# .env.app (передається api-container як env)
FEATURE_X=enabled
FEATURE_Y=disabled$ docker compose up -d
# api отримає:
# NODE_ENV=production (з `environment:`)
# LOG_LEVEL=info (з `environment:`)
# DATABASE_URL=postgres://api:hunter2@db:5432/app (інтерпольовано)
# FEATURE_X=enabled, FEATURE_Y=disabled (з .env.app через env_file:)Build-time arg + runtime env
ARG BUILD_VERSION
FROM alpine:3.21
ARG BUILD_VERSION
ENV APP_VERSION=$BUILD_VERSION
RUN echo $APP_VERSION > /version.txt
CMD ["sh", "-c", "echo Running version $APP_VERSION; cat /version.txt; sleep 100"]$ docker build --build-arg BUILD_VERSION=1.2.3 -t demo .
$ docker run --rm demo
Running version 1.2.3
1.2.3ARG передає значення у білд; копіювання у ENV робить його доступним у runtime.
BuildKit secret для npmrc
# syntax=docker/dockerfile:1.7
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN \
npm ci
COPY . .$ docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp ..npmrc доступний для RUN-кроку, але ніколи не приземляється у жоден image-шар. Жодного ENV NPM_TOKEN=... ніде.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів