Docker containers have no access to your host’s environment variables by default. That means your database URL, API keys, and feature flags don’t just appear inside a container — you have to pass them in explicitly. There are several ways to do this, each with different tradeoffs between convenience and security.
:::note[TL;DR]
- Inline in
docker run:-e KEY=value(fine for development, avoid in production) - From host shell:
-e KEYwith no value (container inherits from host environment) - From a file:
--env-file .env(good for local dev) - In Compose:
environment:block orenv_file:key - For production secrets: Docker Secrets or external secret managers (AWS SSM, Vault) — never bake secrets into images :::
Prerequisites
- Docker installed and running
- Basic familiarity with
docker runanddocker-compose.yml
How do you pass environment variables to a single container?
Inline on the command line:
docker run -e DATABASE_URL="postgres://user:pass@localhost:5432/mydb" myimage
Multiple variables:
docker run \
-e DATABASE_URL="postgres://user:pass@localhost:5432/mydb" \
-e NODE_ENV=production \
-e PORT=3000 \
myimage
Inherit from the host shell (useful for secrets already in your shell environment):
# Export in your shell first
export DATABASE_PASSWORD="supersecret"
# Pass it by name — Docker reads the current value
docker run -e DATABASE_PASSWORD myimage
From a file:
docker run --env-file .env myimage
The .env file format is one KEY=VALUE per line. Lines starting with # are comments. No quotes needed around values:
# .env
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
NODE_ENV=development
API_KEY=abc123
How do you use environment variables in Docker Compose?
There are three approaches and they can be combined.
Inline in docker-compose.yml:
services:
api:
image: myimage
environment:
NODE_ENV: production
PORT: 3000
Reference host environment variables:
services:
api:
environment:
DATABASE_URL: ${DATABASE_URL} # Read from host shell or .env file
API_KEY: ${API_KEY}
Compose automatically loads a .env file from the project directory and makes its contents available for ${VAR} substitution in the compose file itself.
Use a separate env file for the container:
services:
api:
env_file:
- .env
- .env.local # Optional override file
The difference: environment: ${VAR} pulls variables into the compose file for substitution. env_file: injects the file’s contents directly into the container’s environment at runtime.
What is the variable precedence order in Docker Compose?
Highest to lowest priority:
- Variables set with
--env-fileon thedocker composeCLI command - Shell environment variables in the terminal running
docker compose .envfile in the project directoryenvironment:block values indocker-compose.yml
In practice: if NODE_ENV=production is set in your CI/CD environment’s shell, it overrides whatever’s in your .env file. This is usually what you want — CI environments often inject production secrets through environment variables, and those should take precedence.
How do you see what variables are set inside a running container?
# Print all environment variables in a running container
docker exec mycontainer env
# Or filter for a specific one
docker exec mycontainer env | grep DATABASE
# Using docker inspect
docker inspect mycontainer | grep -A 20 '"Env"'
:::warning
docker inspect shows all environment variables including secrets in plaintext. Anyone with Docker socket access can read them. This is one of the reasons you should not pass long-lived production secrets as plain environment variables — use Docker Secrets or a secret manager instead.
:::
How do you handle secrets in production?
Plain environment variables are convenient but not secure for sensitive values:
- They appear in
docker inspectoutput - They’re visible to any process in the container
- They can leak into logs if your app crashes or logs its config
Option 1: Docker Secrets (Swarm mode only)
Docker Secrets store encrypted values that are mounted as files inside the container at /run/secrets/:
# Create a secret
echo "supersecret" | docker secret create db_password -
# Use in compose (Swarm mode)
services:
api:
image: myimage
secrets:
- db_password
secrets:
db_password:
external: true
Inside the container, your app reads the secret from a file:
const dbPassword = fs.readFileSync('/run/secrets/db_password', 'utf8').trim();
Option 2: Mount secrets as files at runtime
For non-Swarm deployments, the same pattern works with bind mounts:
services:
api:
volumes:
- /run/secrets/db_password:/run/secrets/db_password:ro
Option 3: External secret managers
This is the production standard at most organizations. The container fetches secrets at startup from AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager:
# Example: fetch from AWS Secrets Manager in your entrypoint
export DB_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id myapp/db_password \
--query SecretString --output text)
exec "$@"
The container needs IAM permissions (instance profile or Kubernetes service account) to access the secret store — no credentials need to be baked into the image or passed as environment variables.
What should you never do?
Don’t bake secrets into your image:
# Bad — this is in the image layer forever, even if you delete it later
ENV API_KEY=supersecret123
Anyone with access to your image can read this with docker inspect. It also gets committed to your image history. Remove sensitive values from ENV instructions entirely.
Don’t commit .env files with real secrets:
# .gitignore
.env
.env.local
.env.*.local
Commit a .env.example with placeholder values instead. Real values go in .env locally and are injected by your CI/CD platform in production.
Don’t log environment variables on startup:
// Very common, very bad
console.log('Starting with config:', process.env);
This dumps all secrets to stdout, which often ends up in a log aggregator where it’s searchable and persistent.
How do you validate required variables at startup?
If your app requires certain environment variables, check for them at startup and fail fast with a clear error:
// Node.js example
const required = ['DATABASE_URL', 'JWT_SECRET', 'PORT'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('Missing required environment variables:', missing.join(', '));
process.exit(1);
}
# Python example
import os
import sys
required = ['DATABASE_URL', 'SECRET_KEY', 'PORT']
missing = [k for k in required if not os.environ.get(k)]
if missing:
print(f"Missing required environment variables: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
Failing fast at startup is much better than a cryptic error halfway through a request when the app tries to use an undefined variable.
Summary
-e KEY=valuefor quick one-offs;--env-file .envfor local dev- In Compose, use
env_file:to inject a file’s contents, orenvironment: ${VAR}to pull from the host environment - Shell variables >
.envfile >environment:block — later entries override earlier ones - Never bake secrets into images or commit
.envwith real values - For production: Docker Secrets (Swarm), file mounts, or external secret managers
FAQ
What’s the difference between ARG and ENV in a Dockerfile?
ARG is build-time only — it’s available during docker build but not in the running container. ENV sets a variable that persists in the image and is available at runtime. Use ARG for build configuration (platform, version numbers). Use ENV for runtime defaults. Never use either for secrets — ARG values appear in build history, and ENV values appear in docker inspect.
My container can’t see a variable I set in .env. What’s wrong?
Check a few things. First, Compose’s .env substitution applies to the compose file itself (for ${VAR} references), not automatically to containers unless you have an env_file: key or explicit environment: entries. Second, confirm the .env file is in the same directory as your docker-compose.yml. Third, check for typos — variable names are case-sensitive.
Can I change environment variables without rebuilding the container?
Yes — docker run and docker compose up inject environment at container start, not at build time. Stop and restart the container with new values. No rebuild needed. The image itself doesn’t contain the values (unless you used ENV in the Dockerfile).
What to read next
- Docker Compose Cheat Sheet — complete env_file and environment reference
- How to Set Up a Reverse Proxy with Nginx — production server config that pairs with Docker deployments
- SSH Hardening Guide — securing access to servers running Docker