How to Manage Environment Variables in Docker Containers

Published:

Ever hard-coded a database password into your Dockerfile, only to panic when you realized it’s now visible in every image layer forever? Environment variables are Docker’s answer to configuration management, but most developers only scratch the surface of what’s possible. This guide walks through five practical approaches for managing env vars, from quick docker run flags for local testing to Docker Secrets for production deployments. You’ll learn which method fits each scenario, how Docker’s precedence rules actually work, and how to keep API keys out of your image history.

Overview of Docker Environment Variable Management Approaches

U6uA0AN4S0qNAEYLPStl8g

Docker gives you several ways to manage environment variables, letting you configure containers without rebuilding images. You can inject configuration at different points in the container lifecycle, whether you’re running one container or dozens of microservices.

You’ve got Dockerfile ENV instructions for image defaults, docker run flags for runtime injection, environment files for handling multiple variables at once, and Docker Compose config for multi-container setups. Each method fits different scenarios, from quick local dev work to complex production deployments.

Setting Environment Variables in Dockerfile

PoywP3WQ_iMzVBehu2svw

The ENV instruction sets environment variables that stick around in your built image and stay available when containers run. You can declare single variables with ENV KEY=value or stack multiple variables on separate lines. These values get baked into the image layers and apply to every container you spin up from that image.

ENV NODE_ENV=production
ENV PORT=3000
ENV DATABASE_HOST=localhost

# Or combine on one line
ENV NODE_ENV=production PORT=3000 DATABASE_HOST=localhost

The ARG directive defines build time variables that only exist while you’re constructing the image. You pass ARG values when running docker build using the --build-arg flag. Unlike ENV, these variables don’t persist into the final image or running containers.

ARG BUILD_VERSION=1.0.0
ARG API_URL

RUN echo "Building version ${BUILD_VERSION}"
RUN curl -o config.json ${API_URL}/config
docker build --build-arg BUILD_VERSION=2.1.0 --build-arg API_URL=https://api.example.com -t myapp .

Use ARG for values needed during build like package versions, build flags, or temporary API endpoints for grabbing build assets. Use ENV for variables your application actually needs at runtime, like database connection strings, API keys, or feature flags. If you need a build argument to also be available at runtime, assign an ARG to an ENV with something like ARG APP_VERSION followed by ENV APP_VERSION=${APP_VERSION}.

Docker Compose Environment Variable Configuration

Lfd2ge-YQmG80-Kpz-ovPg

Docker Compose gives you the most flexible and organized way to manage environment variables across multi-container applications. The tool handles variable substitution, file loading, and precedence rules automatically, making it standard for development and testing environments.

Inline Environment Declaration

The environment attribute lets you set variables right in your docker-compose.yml file using key-value pairs. This works well for non-sensitive config or when you want everything visible in one place.

version: '3.8'
services:
  web:
    image: nginx
    environment:
      - NGINX_HOST=example.com
      - NGINX_PORT=80
  app:
    image: myapp
    environment:
      NODE_ENV: production
      API_TIMEOUT: 5000

Using .env Files for Automatic Loading

Docker Compose automatically loads a file named .env from the same directory as your docker-compose.yml. This file contains variable definitions in KEY=value format, one per line. Reference these variables in your compose file using ${VARIABLE_NAME} syntax.

# .env file
MYSQL_ROOT_PASSWORD=supersecret
MYSQL_DATABASE=appdb
API_KEY=abc123xyz
services:
  db:
    image: mysql:8
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}

Explicit env_file Configuration

The env_file attribute loads variables from specific files, letting you maintain separate configurations for different environments. You can specify one or multiple files, and Docker Compose loads them in order.

services:
  app:
    image: myapp
    env_file:
      - ./config/common.env
      - ./config/dev.env
  prod-app:
    image: myapp
    env_file:
      - ./config/common.env
      - ./config/prod.env

Shell Variable Substitution

Docker Compose can grab variables from your shell environment using ${VARIABLE} syntax. Export variables in your terminal, and they become available to your compose configuration. This works great in CI/CD pipelines where the environment’s already set up.

export DATABASE_PASSWORD=secret123
export CACHE_SIZE=512
docker-compose up
services:
  app:
    environment:
      - DB_PASSWORD=${DATABASE_PASSWORD}
      - CACHE_SIZE=${CACHE_SIZE}

Docker Compose follows a specific hierarchy when multiple sources define the same variable. Shell environment variables win, followed by variables defined inline in docker-compose.yml, then values from .env files. Variables from env_file attributes have the lowest priority. This lets you override defaults selectively without touching configuration files.

Runtime Variable Injection with Docker Run

hwX_TVlYT668xd0n6fr7ag

Runtime variable injection gives you maximum flexibility for container configuration, letting you set or override environment variables when starting containers. The docker run command provides several flags for this.

The -e flag (short for --env) sets a single environment variable. You can repeat the flag multiple times to set several variables in one command. Quick and easy for testing or one-off container runs.

docker run -e DATABASE_URL=postgres://localhost/mydb myapp
docker run -e NODE_ENV=production -e PORT=3000 -e DEBUG=true myapp
  • Single variable with -e flag: docker run -e VARIABLE=value image
  • Multiple variables with repeated -e flags: docker run -e VAR1=value1 -e VAR2=value2 image
  • Using –env-file for batch loading: docker run --env-file ./config.env image
  • Passing shell variables: docker run -e API_KEY=$API_KEY image (grabs from your shell)
  • Combining methods in single command: docker run -e OVERRIDE=value --env-file ./base.env image

The --env-file option loads multiple variables from a file, saving you from typing marathon command lines. The file format matches the .env format Docker Compose uses. One KEY=value pair per line.

docker run --env-file ./production.env myapp
docker run --env-file ./base.env -e LOG_LEVEL=debug myapp

Runtime variables override any ENV values set in the Dockerfile. If your Dockerfile sets ENV NODE_ENV=development, running docker run -e NODE_ENV=production myimage replaces that default. This override behavior makes it safe to bake sensible defaults into images while keeping deployment flexibility.

Security Practices for Sensitive Environment Variables

dJU3BuZpRxeQW_SYGU8_9A

Environment variables create security risks when they contain sensitive data like API keys, database passwords, or auth tokens. Variables defined in Dockerfiles become part of image layers and stay visible even if you remove them in later layers. Anyone with image access can extract these values using docker history or by inspecting the layers.

Protect .env files from version control by adding them to .gitignore. Instead, commit a .env.example file with placeholder values that documents required variables without exposing secrets. This template helps teammates set up their local environment without sharing actual credentials.

# .gitignore
.env
.env.local
.env.production
*.env

# .env.example
DATABASE_URL=postgres://user:password@localhost:5432/dbname
API_KEY=your_api_key_here
SECRET_TOKEN=generate_random_token

Docker Swarm provides native secrets management through encrypted storage and controlled access. Secrets mount as files in containers rather than showing up as environment variables, preventing them from appearing in container inspection output or process listings.

echo "mysecretpassword" | docker secret create db_password -
services:
  db:
    image: postgres
    secrets:
      - db_password
secrets:
  db_password:
    external: true

Production environments should integrate with dedicated secret management systems like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. These tools provide encryption at rest, audit logging, automatic rotation, and fine-grained access controls that environment variables alone can’t deliver.

Implement validation scripts in your container entrypoints to check for required variables and fail fast if secrets are missing. Rotate credentials regularly and don’t reuse the same secrets across multiple environments. When debugging production issues, sanitize logs to prevent secrets from appearing in output that might get stored or shared.

Method Security Level Use Case
Plain ENV in Dockerfile Low (visible in image layers) Non-sensitive defaults like ports or hostnames
.env files Medium (protected by filesystem access) Local development with .gitignore protection
Docker Secrets High (encrypted at rest) Production deployments in Swarm mode
External Vault Highest (enterprise controls) Multi-environment production with compliance requirements

Managing Configuration Across Multiple Environments

j2KJQ8P4S3qusMMb_o0pMw

The twelve-factor app methodology recommends strict separation of config from code, treating environment-specific settings as external inputs rather than hardcoded values. This makes applications portable across development, staging, and production without code changes. Environment variables become the contract between your application and its runtime environment.

Name environment-specific files using consistent conventions like .env.development, .env.staging, and .env.production. Store these files in a config directory and reference them explicitly in your deployment scripts or compose configurations. Each file contains values appropriate for that environment. Localhost database connections for development, internal service URLs for staging, and production endpoints for live deployments.

project/
  ├── docker-compose.yml
  ├── config/
  │   ├── .env.development
  │   ├── .env.staging
  │   └── .env.production
  └── app/

Switch between configurations using the env_file attribute with multiple file declarations. List files in order of precedence, with environment-specific files last to override shared defaults.

services:
  app:
    image: myapp
    env_file:
      - ./config/common.env
      - ./config/development.env
# For staging
docker-compose --env-file ./config/staging.env up

# For production
docker-compose --env-file ./config/production.env up

Docker Compose override files provide another approach using the -f flag. Create docker-compose.override.yml for local development and docker-compose.production.yml for production, then specify which files to merge during deployment.

docker-compose -f docker-compose.yml -f docker-compose.production.yml up

Connection strings, API endpoints, debug flags, and feature toggles are prime candidates for environment-specific configuration. Development might use DATABASE_URL=postgres://localhost:5432/dev_db and DEBUG=true, while production uses DATABASE_URL=postgres://prod-cluster.internal:5432/prod_db and DEBUG=false. The same application code works everywhere because it reads configuration from the environment rather than relying on hardcoded assumptions.

Variable Substitution and Default Values

U0xrrntrTiuTGwFH7m_-Xg

Docker Compose uses ${VARIABLE} syntax to substitute environment variables in your compose file. When Docker Compose encounters this pattern, it looks for the variable in your .env file, shell environment, or explicitly loaded env files.

services:
  db:
    image: postgres:${POSTGRES_VERSION}
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}

Four substitution patterns give you control over default values and error handling:

  1. Basic substitution ${VAR}: Replaces with variable value or empty string if undefined. image: postgres:${VERSION} becomes postgres: when VERSION isn’t set.
  2. Default value syntax ${VAR:-default}: Uses default when variable is unset or empty. ${PORT:-3000} becomes 3000 if PORT isn’t defined.
  3. Required variable ${VAR:?error message}: Stops with error if variable is missing. ${API_KEY:?API key required} fails with custom message when API_KEY is undefined.
  4. Alternative value ${VAR:+alternative}: Uses alternative when variable is set. ${FEATURE_ENABLED:+--enable-feature} expands to --enable-feature only when FEATURE_ENABLED has any value.

Special characters in variable values need proper quoting or escaping. Values containing spaces, dollar signs, or other shell metacharacters should be wrapped in quotes within env files. Multi-line values require escaping newlines or using quoted strings.

# .env file
DATABASE_URL="postgres://user:pass@localhost:5432/db?sslmode=require"
ALLOWED_HOSTS="localhost, 127.0.0.1, example.com"
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----"

Debugging and Troubleshooting Docker Environment Variables

aIpX3ZLJSQyNCXdBaRF0XA

Common issues with environment variables include typos in variable names, incorrect file paths for env files, and confusion about when variables get evaluated (at build time versus runtime). Variables might appear to load but contain unexpected values due to precedence conflicts between different sources.

Check what variables actually exist inside a running container with these debugging commands:

  • docker exec container_name env: Lists all environment variables currently set in the container
  • docker inspect container_name: Shows full container configuration including environment variables in JSON format
  • docker-compose config: Renders the final compose file after variable substitution, revealing what values Docker Compose will use
  • printenv inside container: Run docker exec container_name printenv VARIABLE_NAME to check a specific variable
  • echo $VARIABLE in entrypoint scripts: Add temporary echo statements to your entrypoint.sh to see values during startup
  • docker logs for startup errors: Check container logs for error messages about missing or invalid environment variables

Variables set with ENV in Dockerfiles get baked into the image at build time. You can verify these by running docker run --rm imagename env which starts a temporary container and lists its environment. Variables passed at runtime with -e flags or through Docker Compose appear only when inspecting the running container, not the image itself. If a variable shows up in docker inspect but your application can’t access it, check that your application is actually reading from the environment rather than a config file, and verify the variable name matches exactly. Environment variables are case-sensitive.

Best Practices for Maintainable Environment Variable Management

h3nJ6NAsRfmRg2SrQQ7vjA

Consistent practices for environment variable management reduce friction when onboarding new team members and prevent configuration drift between environments. Teams that establish clear conventions early spend less time debugging environment issues and more time shipping features.

Well-documented configuration makes the difference between smooth setup and hours of troubleshooting. A .env.example file serves as both documentation and template, showing new developers exactly what variables they need to define locally.

  • Use .env.example as template for required variables: Include placeholder values and comments explaining each variable’s purpose
  • Keep .env file in same folder as docker-compose.yml: Docker Compose automatically detects .env files in the same directory without requiring explicit configuration
  • Organize variables by service or functional grouping: Group related variables together with comment headers like # Database Configuration or # API Settings
  • Document required variables with comments: Add inline comments explaining expected formats, allowed values, or where to obtain credentials
  • Validate variables at container startup: Check for required variables in your entrypoint script and fail with helpful error messages when they’re missing
  • Use meaningful variable names with consistent naming conventions: Follow patterns like SERVICE_SETTING_NAME (DATABASECONNECTIONPOOL_SIZE) rather than cryptic abbreviations
  • Don’t hardcode values in Dockerfiles: Use ARG and ENV instructions for values that might change, even if you think they’re permanent

The .env.example file should contain every variable your application needs with example values that help developers understand the expected format. Include comments explaining where to get API keys, what database URL format to use, and which values are safe to leave as defaults versus which require customization.

# .env.example
# Database connection (use localhost for local dev)
DATABASE_URL=postgres://user:password@localhost:5432/appdb

# API Configuration (get your API key from https://dashboard.example.com)
API_KEY=your_api_key_here
API_TIMEOUT=30

# Redis cache (leave as default for local development)
REDIS_URL=redis://localhost:6379

# Feature flags
ENABLE_NOTIFICATIONS=true
ENABLE_ANALYTICS=false

Advanced Patterns: Volume Mounts and Dynamic Configuration

Vv_ajotkRFC2ex_kGxCxxQ

Volume mounts provide an alternative to environment variables when you need to inject complete configuration files rather than simple key-value pairs. Mount a config file from your host into the container, and your application reads a fully-formed configuration without parsing environment variables. This works well for complex configurations like nginx.conf, application.yml, or JSON config files that don’t translate cleanly to environment variables.

services:
  nginx:
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
  app:
    image: myapp
    volumes:
      - ./config/app-settings.json:/app/config.json:ro

Entrypoint scripts process and validate environment variables when containers start. Create an entrypoint.sh that checks for required variables, sets defaults for optional ones, and generates configuration files from templates before starting your main application process.

#!/bin/sh
# entrypoint.sh

# Validate required variables
if [ -z "$DATABASE_URL" ]; then
  echo "Error: DATABASE_URL is required"
  exit 1
fi

if [ -z "$API_KEY" ]; then
  echo "Error: API_KEY is required"
  exit 1
fi

# Set optional defaults
export LOG_LEVEL=${LOG_LEVEL:-info}
export WORKER_COUNT=${WORKER_COUNT:-4}

# Start application
exec "$@"

Generate configuration files from templates using environment variables in your entrypoint script. Tools like envsubst replace ${VARIABLE} placeholders in template files with actual environment variable values. This pattern bridges the gap between applications that expect config files and the flexibility of environment variables.

# Replace variables in template and write final config
envsubst < /app/config.template.json > /app/config.json
exec node server.js

Use volumes when you’ve got complex configurations that are easier to maintain as files, when you need to share configs across multiple containers, or when your application doesn’t natively support environment variable configuration. Use environment variables for simple key-value settings, for values that change between environments, or when you want to skip mounting files. Combining both approaches gives you the most flexibility. Use environment variables for high-level settings and volume-mounted configs for detailed configuration files.

Container Orchestration and Environment Variables

Environment variable management gets more complex in orchestrated environments where you’re managing multiple containers across multiple hosts. Docker Swarm and Kubernetes provide their own mechanisms for injecting configuration, though the basic concepts of separating config from code stay the same.

Docker Swarm Configuration

Swarm mode uses configs and secrets as first-class resources separate from environment variables. Configs handle non-sensitive configuration data, while secrets manage sensitive information with encryption. Both mount as files in containers rather than exposing as environment variables.

echo "production" | docker config create app_env -
docker service create --config source=app_env,target=/etc/app/environment myapp
version: '3.8'
services:
  app:
    image: myapp
    configs:
      - source: app_config
        target: /app/config.yml
configs:
  app_config:
    file: ./app-config.yml

Kubernetes ConfigMaps and Secrets

Kubernetes uses ConfigMaps for configuration data and Secrets for sensitive information. You can expose them as environment variables, mount them as files, or both. ConfigMaps work similarly to Docker Compose environment files, while Secrets provide base64 encoding and access controls.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: postgres-service
  LOG_LEVEL: info
---
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    envFrom:
    - configMapRef:
        name: app-config

CI/CD pipelines inject environment variables during automated deployments, replacing values based on the target environment. Most CI systems like Jenkins, GitLab CI, or GitHub Actions provide secure variable storage that gets injected into build and deployment steps. Your deployment scripts reference these variables when calling docker run or kubectl apply, eliminating manual configuration during releases.

# GitLab CI example
docker run -e DATABASE_URL=$PROD_DATABASE_URL -e API_KEY=$PROD_API_KEY myapp:$CI_COMMIT_TAG

Final Words

Docker gives you multiple ways to handle environment variables, each suited for different scenarios.

Use Dockerfiles for image defaults, runtime flags for quick overrides, and Compose files for multi-container setups. Protect sensitive data with secrets management and .gitignore patterns.

Organize configs by environment, validate variables at startup, and document everything in .env.example templates.

When you know how to manage environment variables in Docker, you spend less time debugging config issues and more time shipping features. Pick the method that fits your workflow, keep it consistent across your team, and your deployments will be smoother.

FAQ

What are the main methods for setting Docker environment variables?

The main methods for setting Docker environment variables include the ENV instruction in Dockerfiles, the -e and –env flags with docker run, the –env-file option for file-based loading, and Docker Compose configuration using environment attributes or env_file declarations.

What is the difference between ENV and ARG in Dockerfiles?

ENV and ARG differ in availability timing. ARG is only available during the image build process, while ENV persists at container runtime and sets default values that remain accessible when the container starts.

How does Docker Compose automatically load environment variables?

Docker Compose automatically loads environment variables from a .env file located in the same directory as the docker-compose.yml file, making variables available without explicit configuration or file path specification.

Can I override Dockerfile ENV values at runtime?

You can override Dockerfile ENV values at runtime using the -e flag with docker run or the environment attribute in Docker Compose, with runtime-specified values taking precedence over image defaults.

How do I pass multiple environment variables with docker run?

You pass multiple environment variables with docker run by repeating the -e flag for each variable (docker run -e VAR1=value1 -e VAR2=value2) or using –env-file to load all variables from a file.

Why shouldn’t I commit .env files to version control?

You shouldn’t commit .env files to version control because they often contain sensitive data like API keys and database credentials that would be exposed in your repository history, creating security vulnerabilities.

What is Docker Swarm secrets for environment variables?

Docker Swarm secrets provide encrypted credential storage for sensitive environment variables in production deployments, offering better security than plain environment variables by encrypting data and restricting access to authorized services.

How do I use different configurations for development and production?

You use different configurations for development and production by creating environment-specific files like .env.development and .env.production, then loading the appropriate file using Docker Compose’s env_file attribute or docker-compose override files.

What is the syntax for default values in Docker Compose?

The syntax for default values in Docker Compose is ${VARIABLENAME:-defaultvalue}, which uses the specified default when the variable is undefined, ensuring services always have necessary configuration values.

How do I verify environment variables inside a running container?

You verify environment variables inside a running container using docker exec containername env to display all variables, or docker inspect containername to view the container’s complete configuration including environment settings.

Should I use environment variables or volume mounts for configuration?

You should use environment variables for simple key-value settings like connection strings and feature flags, and volume mounts for complex configuration files, certificate files, or when you need to update configuration without restarting containers.

How do environment variables work in Kubernetes compared to Docker?

Environment variables work in Kubernetes through ConfigMaps for non-sensitive data and Secrets for credentials, translating Docker’s environment variable patterns into Kubernetes-native resources with similar functionality but enhanced orchestration features.

What is the precedence order for Docker Compose environment variables?

The precedence order for Docker Compose environment variables from highest to lowest is: docker-compose run -e command-line flags, environment attribute in docker-compose.yml, env_file attribute, .env file, and finally shell environment variables.

How do I validate required environment variables at container startup?

You validate required environment variables at container startup by creating an entrypoint script that checks for variable existence and exits with an error message if required variables are missing before launching the main application.

What naming convention should I use for environment variables?

You should use uppercase names with underscores for environment variables (DATABASEURL, APIKEY), group related variables with common prefixes (DBHOST, DBPORT, DB_NAME), and document each variable’s purpose in .env.example files.

curtisharmon
Curtis has spent over two decades guiding hunters and anglers through the backcountry of Montana and Wyoming. His expertise in elk hunting and fly fishing has made him a sought-after voice in the outdoor community. Curtis combines traditional woodsmanship with modern techniques to help readers succeed in the field.

Related articles

Recent articles