Skip to content

Fix: Docker Compose Environment Variables Not Loading from .env File

FixDevs ·

Quick Answer

How to fix Docker Compose not loading environment variables from .env files — why variables are empty or undefined inside containers, the difference between env_file and variable substitution, and how to debug env var issues.

The Error

Your docker-compose.yml references environment variables, but inside the container they are empty or undefined:

docker compose exec app printenv DATABASE_URL
# (empty output — variable not set)

Or the compose file itself uses variable substitution and warns:

WARN[0000] The "DATABASE_URL" variable is not set. Defaulting to a blank string.

Or the application crashes because it cannot read a required environment variable that you are sure is in .env:

Error: DATABASE_URL environment variable is required

Or after changing .env, the containers still use old values:

docker compose up  # Still shows old DATABASE_URL value

Why This Happens

Docker Compose has two distinct ways to use .env files, and confusing them is the root cause of most variable issues:

  1. Variable substitution in docker-compose.yml — Compose automatically reads .env (in the same directory as the compose file) to substitute ${VAR} placeholders in the compose file itself. This happens on the host, before containers start.

  2. Environment variables inside containers — Variables are only passed into containers if you explicitly configure them via environment: or env_file: in the service definition. Reading .env for substitution does not automatically inject those variables into the container environment.

Other common causes:

  • Wrong .env file location.env must be in the same directory as docker-compose.yml, not in a subdirectory.
  • Stale containers — containers started with docker compose up without --force-recreate after changing .env keep the old environment.
  • Variable names with export prefixexport DATABASE_URL=value in .env does not work; Compose expects bare KEY=value format.
  • Quoting issuesDATABASE_URL="postgres://..." with quotes includes the quotes in the value.

Fix 1: Understand the Two env Variable Mechanisms

Mechanism A — Compose file substitution (host-side):

# docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: ${DB_NAME}       # Substituted from .env on the HOST
      POSTGRES_PASSWORD: ${DB_PASS} # before the container starts
# .env (same directory as docker-compose.yml)
DB_NAME=myapp
DB_PASS=secret123

This sets POSTGRES_DB=myapp in the container — but only because it is listed under environment:. The .env file is used to resolve ${DB_NAME}, not to inject variables directly.

Mechanism B — env_file injects variables directly into the container:

# docker-compose.yml
services:
  app:
    image: myapp:latest
    env_file:
      - .env           # All KEY=VALUE pairs in this file are injected into the container
      - .env.local     # Multiple files are merged (later files override earlier)

With env_file, every KEY=VALUE line in .env becomes an environment variable inside the container — no need to list them individually under environment:.

Combine both mechanisms:

services:
  app:
    image: myapp:latest
    env_file:
      - .env            # Injects all vars into container
    environment:
      NODE_ENV: production         # Override specific vars
      API_URL: ${EXTERNAL_API_URL} # Also use substitution for compose-level vars

Fix 2: Pass Variables Explicitly with environment:

If you want explicit control over exactly which variables reach the container:

# docker-compose.yml
services:
  app:
    build: .
    environment:
      # Pass a hardcoded value
      NODE_ENV: production

      # Pass from the host shell environment
      DATABASE_URL: ${DATABASE_URL}

      # Pass from host with a default if not set
      LOG_LEVEL: ${LOG_LEVEL:-info}

      # Pass a host variable with the same name (shorthand)
      SECRET_KEY:  # No value — inherits from host environment or .env substitution

Verify the variable is set on the host before running compose:

# Check host environment
echo $DATABASE_URL

# Or check what compose resolves to (dry run)
docker compose config
# Shows the fully resolved docker-compose.yml with all substitutions applied

Fix 3: Fix .env File Location and Format

Check the file location:

# .env must be alongside docker-compose.yml
ls -la
# Should show:
# .env
# docker-compose.yml

# If you have a different filename, specify it explicitly
docker compose --env-file ./config/.env.production up

Check the file format — no export, no surrounding quotes:

# Correct format
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
API_KEY=abc123

# Wrong — 'export' prefix is a shell syntax, not supported by Compose
export DATABASE_URL=postgres://user:pass@localhost:5432/mydb

# Wrong — surrounding quotes are included in the value
DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
# The container receives: "postgres://user:pass@localhost:5432/mydb" (with quotes)

# Correct for values with spaces or special characters — use no quotes
DATABASE_URL=postgres://user:pass@localhost:5432/mydb

Note: Double quotes in .env are stripped by some tools (like dotenv libraries) but kept literally by Docker Compose. Always use unquoted values in .env files used by Docker Compose, or test with docker compose config to verify the resolved value.

Validate your .env file:

# Show all variables Compose would resolve
docker compose config | grep -A5 environment

# Print a specific variable's resolved value
docker compose run --rm app printenv DATABASE_URL

Fix 4: Recreate Containers After Changing .env

Changing .env does not automatically update running containers. You must recreate them:

# Stop and remove existing containers, then start fresh
docker compose down
docker compose up -d

# Or force-recreate without removing volumes
docker compose up -d --force-recreate

# Verify the new value is live
docker compose exec app printenv DATABASE_URL

Common Mistake: Running docker compose restart after changing .env. restart does not recreate containers — it only stops and starts the existing container with its original environment. Use down + up or --force-recreate.

Fix 5: Use Multiple .env Files for Different Environments

# docker-compose.yml — base config
services:
  app:
    image: myapp
    env_file:
      - .env

# docker-compose.override.yml — development overrides (loaded automatically)
services:
  app:
    env_file:
      - .env
      - .env.development
# Development (loads docker-compose.yml + docker-compose.override.yml automatically)
docker compose up

# Production (only use the base file)
docker compose -f docker-compose.yml up

# Staging (explicit env file)
docker compose --env-file .env.staging up

Structure for multiple environments:

.env              # Default values (committed to git — no secrets)
.env.development  # Dev-specific overrides (not committed)
.env.production   # Production values (not committed — use secrets manager)
.env.example      # Template with all keys, no values (committed to git)

Fix 6: Debug Environment Variables Inside a Running Container

Print all environment variables in a running container:

# All variables
docker compose exec app env

# Specific variable
docker compose exec app printenv DATABASE_URL

# From outside, without exec
docker inspect app-container-name | grep -A5 '"Env"'

Run a temporary container with the same environment to debug:

# One-shot container with your env_file — does not start the app
docker compose run --rm app env | sort

# Or start a shell
docker compose run --rm app sh
# Inside: echo $DATABASE_URL

Check if variables are set at the time the process reads them:

Some frameworks (like Node.js with dotenv) load .env themselves at runtime. If you are using env_file in Compose AND dotenv in the app, the app’s .env inside the container might override or conflict with the Compose-injected variables:

// If your app has dotenv, it reads /app/.env (inside the container)
require('dotenv').config();

// This .env is different from the .env on your host that Compose reads
// Either remove dotenv and rely on Compose env_file, or mount the .env file

Mount the .env file into the container if the app reads it directly:

services:
  app:
    image: myapp
    volumes:
      - ./.env:/app/.env:ro  # App reads its own .env at runtime

Fix 7: Secrets — Avoid Storing Sensitive Values in .env

For production, use Docker secrets or a secrets manager instead of .env files:

# docker-compose.yml — using Docker secrets
services:
  app:
    image: myapp
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt  # On host — outside version control
# In your application — read from the secrets file
const dbPassword = fs.readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim();

For Docker Swarm or production environments, use external secrets:

secrets:
  db_password:
    external: true  # Managed by Docker Swarm or Kubernetes secrets

Still Not Working?

Run docker compose config to see the resolved compose file. This shows exactly what values Compose will use after substitution — if a variable shows as blank there, it was not set in .env or the host environment:

docker compose config
# Look for empty values like: DATABASE_URL: ''

Check for BOM or encoding issues in .env. Files saved with a BOM (Byte Order Mark) from Windows editors can cause the first variable to be misread:

file .env           # Should say "ASCII text" not "UTF-8 Unicode (with BOM)"
hexdump -C .env | head  # BOM appears as EF BB BF at the start

Fix with:

sed -i '1s/^\xEF\xBB\xBF//' .env

Check for trailing whitespace in values:

cat -A .env | grep "DATABASE_URL"
# DATABASE_URL=postgres://...$ (correct — $ marks end of line with no trailing space)
# DATABASE_URL=postgres://... $ (wrong — trailing space before $)

For related Docker issues, see Fix: dotenv Not Loading and Fix: Docker Compose depends_on Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles