Environment variables are the standard way to configure containers. They let you use a single image across different environments (dev, staging, prod) without rebuilding.
ENV in Dockerfile: Default Values
ENV sets variables that are available both during the build and at container runtime. These act as defaults.
FROM python:3.12-slim
# Default values
ENV APP_ENV=production
ENV PORT=8000
ENV LOG_LEVEL=info
ENV DEBUG=false
WORKDIR /app
COPY . .
CMD ["python", "main.py"]
In application code:
import os
port = int(os.environ.get("PORT", 8000))
debug = os.environ.get("DEBUG", "false").lower() == "true"
log_level = os.environ.get("LOG_LEVEL", "info")
Values set with ENV in the Dockerfile are defaults and can be overridden at runtime.
The -e Flag: Variables at Runtime
# Single variable
docker run -e DEBUG=true my-app
# Multiple variables
docker run \
-e DEBUG=true \
-e PORT=9000 \
-e DATABASE_URL=postgresql://localhost/mydb \
my-app
# Pass a variable from the host environment
export API_KEY=secret123
docker run -e API_KEY my-app # value is taken from the host
Variables passed with -e override any ENV instructions in the Dockerfile.
The –env-file Flag: Variables from a File
When you have many variables, storing them in a file is more convenient:
# dev.env
DEBUG=true
PORT=8000
DATABASE_URL=postgresql://localhost/devdb
LOG_LEVEL=debug
REDIS_URL=redis://localhost:6379
docker run --env-file dev.env my-app
docker run --env-file prod.env my-app
File format: one variable per line, KEY=VALUE. Lines starting with # are comments.
environment: in docker-compose.yml
services:
web:
build: .
environment:
# Explicit values
DEBUG: "false"
PORT: "8000"
LOG_LEVEL: info
# From the host environment (no value given)
SECRET_KEY:
API_KEY:
worker:
build: .
environment:
- QUEUE_NAME=tasks
- CONCURRENCY=4
Both syntaxes (key: value and - KEY=VALUE) are equivalent.
.env File in Docker Compose
Docker Compose automatically loads a .env file from the same directory as docker-compose.yml. Values from .env are substituted into docker-compose.yml via ${VAR}.
# .env
POSTGRES_PASSWORD=mysecretpass
POSTGRES_USER=myuser
IMAGE_TAG=1.2.3
APP_PORT=8000
services:
web:
image: my-app:${IMAGE_TAG}
ports:
- "${APP_PORT}:8000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/mydb
db:
image: postgres:15
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# Verify variable substitution
docker compose config
You can set a default value with ${VAR:-default}:
image: my-app:${IMAGE_TAG:-latest}
Never Bake Secrets into an Image
A common mistake is hardcoding a secret directly in the Dockerfile:
# BAD: the secret ends up in an image layer and in git
ENV API_KEY=super_secret_key_12345
RUN curl -H "Authorization: ${API_KEY}" https://api.example.com/setup
Even if you unset the variable in a later layer, it stays in the image history and is visible via docker history.
The correct approach:
# GOOD: no secrets in the Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
CMD ["python", "main.py"]
# Secret is passed at runtime and never baked into the image
docker run -e API_KEY=secret my-app
.gitignore and .dockerignore
# .gitignore — do not commit to git
.env
.env.local
.env.production
*.env
# .dockerignore — do not copy into the image
.env
.env.*
Variable Priority (highest to lowest)
1. -e flag / environment: in compose
2. --env-file / env_file: in compose
3. .env file (Docker Compose only)
4. ENV in Dockerfile
Example: Configuration for Different Environments
# .env.dev
DATABASE_URL=postgresql://localhost/devdb
DEBUG=true
LOG_LEVEL=debug
# .env.prod
DATABASE_URL=postgresql://prod-server/mydb
DEBUG=false
LOG_LEVEL=warning
# Start in dev
docker compose --env-file .env.dev up
# Start in prod
docker compose --env-file .env.prod up
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!