The core idea
The same image should run in development, staging and production. What changes between them (database address, log level, feature limits) is configuration, and the standard way to hand it to a container is environment variables. Build once, configure at run time.
Setting variables
$ docker run --rm -e GREETING=hi alpine envPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binGREETING=hiHOME=/root...
Four ways in, from least to most specific:
| Where | Example | Notes |
|---|---|---|
ENV in the Dockerfile | ENV NODE_ENV=production | a default baked into the image |
--env-file | --env-file app.env | one KEY=value per line; # lines are comments |
-e KEY=value | -e LOG_LEVEL=debug | wins over ENV and the env file |
-e KEY | -e PORT | copies PORT from your shell; if it isn't set there, nothing is passed |
Run-time values override image defaults:
$ docker run --rm notes-api node -e 'console.log(process.env.NODE_ENV)'production$ docker run --rm -e NODE_ENV=development notes-api node -e 'console.log(process.env.NODE_ENV)'development
Making the notes API configurable
The notes API now reads two settings, with defaults, and refuses to start if one is invalid:
function intFromEnv(name: string, fallback: number) {
const raw = process.env[name];
if (raw === undefined || raw === "") return fallback;
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) {
console.error(`config error: ${name} must be a positive integer, got "${raw}"`);
process.exit(1);
}
return value;
}
const port = intFromEnv("PORT", 3000);
const maxNoteLength = intFromEnv("MAX_NOTE_LENGTH", 280);$ docker run --name bad -e MAX_NOTE_LENGTH=lots notes-apiconfig error: MAX_NOTE_LENGTH must be a positive integer, got "lots"$ echo $?1
Failing fast matters more in containers than anywhere else: a restart policy or orchestrator will notice the exit immediately, while a service that starts with bad config and limps along can go unnoticed for days.
Where environment variables leak
Environment variables are convenient, not secret. Anyone who can talk to the Docker daemon can read them:
$ docker run -d --name envy --env-file app.env alpine sleep 100$ docker inspect -f '{{json .Config.Env}}' envy["DB_HOST=db","DB_PASSWORD=s3cret","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
They also end up in crash reports, debug pages and child processes. And ENV or ARG with a secret in a Dockerfile is
worse: it's saved in the image itself. For real secrets, use mounted secret files and build secrets
(07.02 Secrets without leaks).
The image has ENV LOG_LEVEL=info. You run docker run --env-file prod.env -e LOG_LEVEL=debug, and prod.env contains LOG_LEVEL=warn. What does the app see?
Run the notes API with -e MAX_NOTE_LENGTH=50 and post a 60-character note: you should get a 400. Then put MAX_NOTE_LENGTH=500 in a file and run with --env-file. Finally pass an invalid value and confirm the container exits with code 1.
STUCK? ▸
If the limit doesn't change, check you rebuilt the image after pulling the new server.ts; the startup log line should say (max note length 50).
Recap
Interview questions
How should a containerised app get its configuration?