2.4Configuration: ENV, ARG and .env files
The essentials in 30 seconds
- ARG builds, ENV runs: ARG lives during the build then disappears, ENV persists in the container — do not mix them up.
- Sane defaults + overriding: the image runs with nothing and each environment overrides at run time — one image, N environments.
- Secrets out of the image: versioned structure (.env.example), sensitive values out of git — never a committed password.
Running mini-case — Idir and the two ports of one image
Idir's versioned image (2.3) runs, but dev listens on 3000 and staging on 8080: the team rebuilt one image per environment. Idir declares ENV PORT=3000 by default in the Dockerfile: the image runs with nothing. Then he overrides at launch (-e PORT=8080): two containers of the same 1.0 image each answer on their own port. The build key (ARG) stays confined to the build. In 2.5, Idir will check no sensitive variable stayed frozen in the image.
Following the 2.3 thread: Idir's same image serves every environment — and continues in 2.5.
1. ENV vs ARG: two moments
ARG is for build time (version to compile, passed via --build-arg), then disappears: unfindable at run time. ENV persists in the living container (port, mode, base URL) and stays overridable at run -e. Rule: ARG builds, ENV runs. Idir passes his build version as ARG and his listen port as ENV.
2. Helpful default values
Declare sane defaults in the Dockerfile (ENV PORT=3000 NODE_ENV=production): the image runs with nothing. Each environment overrides at launch (-e PORT=8080). An image requiring ten obscure variables with no defaults is a badly designed image: explicit names, documented, defaults that run. Idir launches two containers of the same image with two PORTs: each answers on its own.
3. The .env file: versioned config without secrets
In Compose (level 3), an .env file centralizes variables per environment. Principle: the structure is versioned (.env.example with names and non-sensitive defaults), sensitive values stay out of git (local uncommitted file, secrets manager, CI variables). Never a committed password, never a secret frozen as ENV in the image: visible in history. Idir versions the example and keeps sensitive values out of the repository.
🔎 Concrete example — 🔎 Concrete example — Same image, two ports
Idir's image, configurable with no rebuild:
# Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=3000 NODE_ENV=production
EXPOSE 3000
CMD ["sh", "-c", "node server.js --port $PORT"]
docker run -d -p 3000:3000 mon-app:1.0
docker run -d -p 8080:8080 -e PORT=8080 mon-app:1.0
Reading: the same 1.0 image serves two environments: default 3000, 8080 overridden at launch.
Lesson: config travels outside the image — no rebuild to change a port.
Section vocabulary
- ARG
- A BUILD variable, available during docker build only (via --build-arg), gone at run time.
Simple example: Idir passes the version to compile as ARG at build time.
What it is not: a persisted runtime variable.
Reflex: build value = ARG.
- ENV
- A RUNTIME variable, persisted in the living container, with a sane default overridable at run time.
Simple example: Idir declares ENV PORT=3000, overridden as 8080 at run time.
What it is not: a variable vanishing after the build.
Reflex: living setting = ENV.
- -e
- Overriding a variable at docker run: the environment adjusts without rebuilding the image.
Simple example: Idir launches staging with -e PORT=8080 on the same image.
What it is not: a rebuild to change a setting.
Reflex: adjusting = -e at launch.
- Sane default
- An ENV value making the image run with no override at all; the basis of a well-designed image.
Simple example: Idir's image starts on 3000 with no option at all.
What it is not: ten obscure variables required with no defaults.
Reflex: runs with nothing first.
In practice — In practice — Configurable in 10 minutes
- Add ENV PORT=3000 to your Dockerfile and rebuild.
- Launch two containers of the same image with two different PORTs (-e).
- Check each one answers on its own port.
- List the variables seen from inside (exec + env).
Script ready to say — selling config without rebuild (30 s)
"One single 1.0 image for every environment: by default it listens on 3000, staging overrides to 8080 at launch, with no rebuild. Simple settings travel as ENV, the build key stays as ARG, and secrets are never in the image. Same version everywhere, only the config changes."
Say while launching Idir's two containers side by side, same image, two ports.
Copy-paste template — configurable Dockerfile (ARG + ENV)
FROM node:20-slim
ARG APP_VERSION=1.0
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV PORT=3000 NODE_ENV=production APP_VERSION=${APP_VERSION}
EXPOSE 3000
CMD ["sh", "-c", "node server.js --port $PORT"]
Vanished ARG
A version read at run time but passed as ARG: empty on start — Idir copies it to ENV if the living container needs it.
Ten obscure variables
Image requiring ten -e flags with no defaults: Idir adds sane defaults and documents each name.
Secret as frozen ENV
Password as ENV in the Dockerfile, visible in history: Idir removes it and injects it at run time.
Committed .env
.env file with sensitive values pushed to git: Idir versions .env.example and keeps values out of the repository.
✓ Best practices
- Defaults that run with nothing.
- Explicit, documented variable names.
- Secrets out of git, always.
✗ Common pitfalls
- Putting a secret as committed ENV (visible in history).
- Mixing ARG and ENV up (gone at run time).
- Requiring ten obscure variables with no defaults.
Key takeaways
- ARG = build (vanishes), ENV = run (persists and overrides).
- Sane defaults, overriding at launch (-e).
- One image, N environments: no rebuild for a setting.
- Secrets out of the image and out of git (versioned structure, protected values).
48h challenge — apply tomorrow
Make your 1.0 image configurable: default ENV PORT, two containers two ports (-e), exec + env check inside, and an .env.example with no sensitive value at all.
- I can tell ARG (build) from ENV (run) apart and place them correctly.
- I can serve N environments with one image (defaults + -e).
- I keep the structure versioned and secrets out of git and out of the image.
Frequently asked questions
Where do I put a database password?
Neither in the Dockerfile nor in the image: as a variable at run time (-e), in a local uncommitted .env, or as a managed secret (Compose secrets, CI variables, orchestrator). A secret frozen as ENV stays readable in history and leaves on push.
ENV or mounted config file?
ENV for simple settings (ports, modes, URLs); mounted file for rich configs (nginx.conf, certificates). Both combine: one does not exclude the other.
You change the listen port between dev and prod. What do you do?