Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як використовувати змінні середовища в Docker?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**У Docker чотири способи поставити env-змінні:** `ENV` у Dockerfile, `-e KEY=value` на `docker run`, `--env-file file.env` і `environment:` у Compose. ```bash # Dockerfile ENV NODE_ENV=production # docker run docker run -e DATABASE_URL=postgres://... -e LOG_LEVEL=debug myapp docker run --env-file .env myapp # Compose services: api: environment: - NODE_ENV=production env_file: .env ``` **Головне:** ніколи не клади secret у env у проді, вони витікають у `docker inspect`, image-history і process-list. Бери BuildKit secret-mount або external secret-manager.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 ```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 під час запуску: ```bash docker run -e PORT=8080 myapp # PORT тепер 8080, NODE_ENV ще production ``` **Важливо:** `ENV`-значення з'являються у `docker history` і `docker inspect`. Ніколи не запікай secret тут. ### `-e` і `--env-file` на `docker run` ```bash # Одна змінна 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:` ```yaml 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 unset ``` **Compose-інтерполяція:** `${VAR}` і `${VAR:-default}` посилаються на змінні з твого shell або з файлу `.env` поряд з `compose.yaml`. Цей `.env` для **самого Compose**, використовується під час YAML-парсингу. Інакше, ніж `env_file:`, що передається у container. ### `ARG` vs `ENV` ```dockerfile 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 у ENV ``` ```bash docker 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 ```bash docker run -e DB_PASSWORD=hunter2 myapp ``` Витікає: - `docker inspect <container>` показує env у plain text - `ps auxe` на host може показати env PID 1 container - Власний `/proc/1/environ` container читається зсередини - Багато app-фреймворків логують повний env при старті (особливо у dev) - Дочірні процеси успадковують env **Краще роби так:** - **BuildKit secret-mount** для build-time secret: ```dockerfile RUN --mount=type=secret,id=npmrc 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** ```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 spaces ``` Docker `.env` це не shell. Лапки не потрібні. Якщо ставиш лапки, вони стають частиною значення. **Забути, що `.env` для Compose-інтерполяції, `env_file:` для container** ```yaml 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`** ```dockerfile 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 ```yaml # 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 ``` ```bash # .env (Compose-інтерполяція) TAG=v1.2.3 DB_PASSWORD=hunter2 # .env.app (передається api-container як env) FEATURE_X=enabled FEATURE_Y=disabled ``` ```bash $ 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 ```dockerfile 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"] ``` ```bash $ docker build --build-arg BUILD_VERSION=1.2.3 -t demo . $ docker run --rm demo Running version 1.2.3 1.2.3 ``` ARG передає значення у білд; копіювання у ENV робить його доступним у runtime. ### BuildKit secret для npmrc ```dockerfile # syntax=docker/dockerfile:1.7 FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci COPY . . ``` ```bash $ docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp . ``` `.npmrc` доступний для `RUN`-кроку, але ніколи не приземляється у жоден image-шар. Жодного `ENV NPM_TOKEN=...` ніде.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.