Environment Variables and Secrets
Configuration via environment variables
Every app has configuration that changes between environments: database URLs, API keys, feature flags, port numbers. These should come from environment variables, not from code.
const PORT = parseInt(process.env.PORT ?? "3000");
const DATABASE_URL = process.env.DATABASE_URL ?? "app.db";
const NODE_ENV = process.env.NODE_ENV ?? "development"; Setting env vars at runtime
Pass environment variables when starting the container:
# Individual variables
docker run -d \
-p 3000:3000 \
-e NODE_ENV=production \
-e DATABASE_URL=/data/app.db \
-e JWT_SECRET=your-secret-here \
myapp
# From a file
docker run -d \
-p 3000:3000 \
--env-file .env.production \
myapp The --env-file flag reads key=value pairs from a file:
# .env.production
NODE_ENV=production
DATABASE_URL=/data/app.db
JWT_SECRET=change-me-in-production
URL_SECRET=another-secret Never bake secrets into images
# NEVER DO THIS
ENV JWT_SECRET=my-super-secret This embeds the secret in the image. Anyone with access to the image can read it:
docker inspect myapp | grep JWT_SECRET
# "JWT_SECRET=my-super-secret" ← exposed! Images are pushed to registries, shared with team members, and stored in CI caches. A secret in the image is a secret shared with everyone.
Instead, pass secrets at runtime with -e or --env-file. The secret exists only in the running container’s environment, not in the image itself.
Build-time vs runtime variables
Build-time (ARG): Available during docker build. Used for build configuration (Node version, build flags). Not available in the running container.
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine docker build --build-arg NODE_VERSION=20 -t myapp . Runtime (ENV): Available in the running container. Used for non-secret configuration (port, environment name). Baked into the image — visible with docker inspect.
ENV NODE_ENV=production
ENV PORT=3000 Neither (-e flag): Passed only at runtime. Not in the image. Used for secrets.
The rule: ARG for build config, ENV for non-secret defaults, -e for secrets.
Docker Compose env files
Docker Compose (covered in the next section) supports .env files natively:
# docker-compose.yml
services:
app:
image: myapp
env_file:
- .env.production Exercises
Exercise 1: Run your container with -e NODE_ENV=production. Exec into it and verify: docker exec myapp sh -c 'echo $NODE_ENV'.
Exercise 2: Create a .env.production file. Run with --env-file .env.production. Verify the variables are set inside the container.
Exercise 3: Try baking a secret with ENV SECRET=test in the Dockerfile. Build. Run docker inspect myapp | grep SECRET. See the problem? Remove it.
Why should secrets never be set with ENV in a Dockerfile?