sehaj/learn
Docker/03 Running containers

03.03 · with code · 10 min

Environment variables and config

Build one image and run it anywhere by moving configuration into environment variables, and learn where those variables leak.

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

❯terminal
$ docker run --rm -e GREETING=hi alpine env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GREETING=hi
HOME=/root
...

Four ways in, from least to most specific:

WhereExampleNotes
ENV in the DockerfileENV NODE_ENV=productiona default baked into the image
--env-file--env-file app.envone KEY=value per line; # lines are comments
-e KEY=value-e LOG_LEVEL=debugwins over ENV and the env file
-e KEY-e PORTcopies PORT from your shell; if it isn't set there, nothing is passed

Run-time values override image defaults:

❯terminal
$ 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:

src/server.ts
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);
❯terminal
$ docker run --name bad -e MAX_NOTE_LENGTH=lots notes-api
config 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:

❯terminal
$ 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).

Predict first

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?

Commit to an answer before you read on.
CheckpointConfigure without rebuilding

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

MidfreqMust-knowDesign

How should a containerised app get its configuration?

Report a mistakeEdit this page