DevToolBoxGRATIS
Blogg

Docker Compose Secrets & Miljövariabler: Rätt sätt

10 min läsningby DevToolBox

Environment variables are the primary mechanism for configuring containers in Docker Compose. But there are 4 different ways to pass them, each with distinct behaviors, security implications, and gotchas. This guide covers every approach, from simple .env files to production-grade Docker secrets, with real-world patterns for multi-environment setups.

4 Ways to Pass Environment Variables in Docker Compose

Docker Compose supports four distinct methods for injecting environment variables into containers. Understanding when to use each is critical for maintainable, secure configurations.

1. Inline with the environment Directive

Define key-value pairs directly in your docker-compose.yml. Best for non-sensitive, service-specific configuration that rarely changes.

services:
  api:
    image: node:20-alpine
    environment:
      NODE_ENV: production
      DB_HOST: postgres
      DB_PORT: "5432"
      LOG_LEVEL: info

  # List syntax (also valid)
  worker:
    image: node:20-alpine
    environment:
      - NODE_ENV=production
      - QUEUE_NAME=tasks
      - CONCURRENCY=4

2. External File with env_file

Load variables from one or more external files. Best for managing groups of related variables and keeping secrets out of version control.

services:
  api:
    image: node:20-alpine
    env_file:
      - .env              # Shared variables
      - .env.api          # Service-specific variables
      - .env.local        # Local overrides (gitignored)

  db:
    image: postgres:16
    env_file:
      - .env
      - .env.db

3. Shell Environment Variable Passthrough

Pass variables from your host shell into the container. Best for CI/CD pipelines and dynamic values.

# In your shell:
export API_KEY=sk-abc123
export DEBUG=true

# docker-compose.yml
services:
  api:
    image: node:20-alpine
    environment:
      - API_KEY          # Passes host $API_KEY into container
      - DEBUG            # Passes host $DEBUG into container
      - CI               # Passes host $CI (empty string if unset)

4. Variable Substitution in docker-compose.yml

Interpolate host environment variables directly in your Compose file values. Best for making your Compose file configurable across environments.

# .env file
POSTGRES_VERSION=16
APP_PORT=3000
REPLICAS=3

# docker-compose.yml
services:
  db:
    image: postgres:${POSTGRES_VERSION}    # Resolved from .env or shell
  app:
    ports:
      - "${APP_PORT}:3000"                 # Resolved from .env or shell
    deploy:
      replicas: ${REPLICAS}               # Resolved from .env or shell

Comparison Table

MethodBest ForIn VCS?Secret-Safe?
environmentNon-sensitive defaultsYesNo
env_fileGrouped config, secretsNo (.gitignore)Partial
Shell passthroughCI/CD, dynamic valuesN/APartial
${} substitutionCompose file templatingYes (file), No (.env)No

.env File: Syntax, Placement, and Interpolation

Docker Compose automatically reads a file named .env in the same directory as your docker-compose.yml. This is the most common way to externalize configuration.

Syntax Rules

# Comments start with #
# Blank lines are ignored

# Simple key=value (no spaces around =)
DB_HOST=localhost
DB_PORT=5432

# Values with spaces need quotes
APP_NAME="My Docker App"
GREETING='Hello World'

# Multi-line values (double quotes only)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"

# Variable expansion (Compose v2.20+)
BASE_URL=https://api.example.com
HEALTH_URL=${BASE_URL}/health

# Export prefix is ignored (compatible with shell source)
export SECRET_KEY=mysecretkey

# Empty values
EMPTY_VAR=
ALSO_EMPTY=''

File Placement and Priority

Compose looks for .env in the project directory (where docker-compose.yml lives). You can override this with the --env-file flag:

# Default: reads .env from project directory
docker compose up

# Override with --env-file
docker compose --env-file .env.staging up

# Multiple --env-file flags (Compose v2.24+)
docker compose --env-file .env --env-file .env.local up

# Project directory structure:
my-project/
  docker-compose.yml    # Compose file
  .env                  # Auto-loaded by Compose
  .env.staging          # Loaded with --env-file
  .env.production       # Loaded with --env-file

Interpolation in docker-compose.yml

Variables from .env are available for substitution in docker-compose.yml, but they are NOT automatically injected into containers:

# .env
TAG=3.2.1
EXTERNAL_PORT=8080

# docker-compose.yml
services:
  web:
    image: myapp:${TAG}              # Uses TAG from .env -> myapp:3.2.1
    ports:
      - "${EXTERNAL_PORT}:80"        # Uses EXTERNAL_PORT from .env -> 8080:80
    environment:
      - TAG                           # WRONG! This passes host $TAG, not .env TAG
      - TAG=${TAG}                   # RIGHT! Explicitly interpolate .env value

# Key insight:
# .env -> docker-compose.yml interpolation   (automatic)
# .env -> container environment              (NOT automatic, use env_file)

env_file vs environment: Differences and Gotchas

These two directives look similar but behave differently. Understanding the distinctions prevents subtle bugs.

DirectiveBehavior
environment:Inlined in docker-compose.yml. Visible in version control. Supports variable substitution. Overrides env_file.
env_file:Loaded from external file(s). Can be gitignored. No variable substitution inside the file. Lower priority than environment.
.env (auto)Only for Compose file interpolation (${} syntax). NOT injected into containers. Lowest priority.

Critical Gotchas

  • 1. Precedence: environment values override env_file values for the same key. This is intentional and useful for overrides.
  • 2. The .env file (auto-loaded) is for Compose file interpolation only. It does NOT inject variables into containers unless you explicitly use env_file or environment.
  • 3. env_file paths are relative to the docker-compose.yml file, not the current working directory.
  • 4. If an env_file is missing, Compose will fail with an error. Use required: false (Compose v2.24+) to make it optional.
# Precedence demonstration:

# .env.shared
DB_HOST=shared-db
DB_PORT=5432

# docker-compose.yml
services:
  api:
    env_file:
      - .env.shared              # DB_HOST=shared-db, DB_PORT=5432
    environment:
      DB_HOST: override-db       # Overrides env_file! DB_HOST=override-db
      # DB_PORT not listed here  # Keeps env_file value: DB_PORT=5432

# Result inside container:
# DB_HOST=override-db    (from environment, overrides env_file)
# DB_PORT=5432           (from env_file)

# Optional env_file (Compose v2.24+):
services:
  api:
    env_file:
      - path: .env.local
        required: false          # Won't fail if file doesn't exist

Docker Secrets: File-Based Secrets for Production

Docker secrets provide a more secure alternative to environment variables for sensitive data. Secrets are mounted as files inside the container, not exposed as environment variables.

Step 1: Create Your Secret Files

# Create secret files (don't commit these!)
echo "SuperSecretPassword123" > db_password.txt
echo "sk-prod-abc123xyz789" > api_key.txt

# Add to .gitignore
echo "*.txt" >> .gitignore
echo "secrets/" >> .gitignore

# Alternative: use a secrets directory
mkdir secrets
echo "SuperSecretPassword123" > secrets/db_password
echo "sk-prod-abc123xyz789" > secrets/api_key

Step 2: Define Secrets in docker-compose.yml

# docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password  # Note: _FILE suffix
    secrets:
      - db_password                    # Grant access to this secret

  api:
    image: myapp:latest
    secrets:
      - db_password
      - api_key
    # Secrets available at:
    # /run/secrets/db_password
    # /run/secrets/api_key

# Top-level secrets definition
secrets:
  db_password:
    file: ./secrets/db_password        # From local file
  api_key:
    file: ./secrets/api_key            # From local file

  # Alternative: use environment variable as source
  # jwt_secret:
  #   environment: JWT_SECRET_VALUE    # From host env var (Compose v2.23+)

Step 3: Read Secrets in Your Application

# Inside the container, secrets are plain text files:
$ docker compose exec api cat /run/secrets/db_password
SuperSecretPassword123

$ docker compose exec api cat /run/secrets/api_key
sk-prod-abc123xyz789

$ docker compose exec api ls -la /run/secrets/
total 8
-r--r--r-- 1 root root 24 Jan  1 00:00 db_password
-r--r--r-- 1 root root 22 Jan  1 00:00 api_key

Step 4: Handle Secrets in Your Code

// Node.js - Read secret from file
const fs = require('fs');

function getSecret(name) {
  const secretPath = `/run/secrets/${name}`;
  try {
    return fs.readFileSync(secretPath, 'utf8').trim();
  } catch (err) {
    // Fallback to environment variable (for development)
    return process.env[name.toUpperCase()];
  }
}

const dbPassword = getSecret('db_password');
const apiKey = getSecret('api_key');
# Python - Read secret from file
import os

def get_secret(name: str) -> str:
    secret_path = f"/run/secrets/{name}"
    try:
        with open(secret_path) as f:
            return f.read().strip()
    except FileNotFoundError:
        # Fallback to environment variable
        return os.environ.get(name.upper(), "")

db_password = get_secret("db_password")
api_key = get_secret("api_key")

Why Secrets Over Environment Variables?

  • Environment variables can leak via docker inspect, /proc/environ, error logs, and crash dumps
  • Secrets are file-based and only accessible inside the container at /run/secrets/
  • Secrets support granular access control per service
  • Many databases and services natively support _FILE suffix for secret files (e.g., POSTGRES_PASSWORD_FILE)

Variable Substitution: ${VAR:-default} Syntax

Docker Compose supports shell-style variable substitution with default values and error handling.

SyntaxResult
${VAR}Value of VAR. Error if unset.
${VAR:-default}Value of VAR if set, otherwise "default".
${VAR-default}Value of VAR if set (even if empty), otherwise "default".
${VAR:?error msg}Value of VAR if set, otherwise exit with "error msg".
${VAR?error msg}Value of VAR if set (even if empty), otherwise exit with "error msg".
${VAR:+replacement}"replacement" if VAR is set and non-empty, otherwise empty.
${VAR+replacement}"replacement" if VAR is set (even if empty), otherwise empty.

Practical Example

# docker-compose.yml with variable substitution
services:
  web:
    image: nginx:${NGINX_VERSION:-1.25-alpine}     # Default: 1.25-alpine
    ports:
      - "${WEB_PORT:-80}:80"                        # Default: 80
      - "${SSL_PORT:-443}:443"                      # Default: 443
    volumes:
      - ${CONFIG_PATH:-./nginx.conf}:/etc/nginx/nginx.conf:ro

  api:
    image: ${REGISTRY:-docker.io}/myapp:${TAG:?TAG is required}
    #                                     ^ Fails if TAG is not set
    environment:
      NODE_ENV: ${NODE_ENV:-development}
      LOG_LEVEL: ${LOG_LEVEL:-info}
      DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:?DB_PASS required}@db:5432/${DB_NAME:-myapp}

  db:
    image: postgres:${PG_VERSION:-16}
    volumes:
      - ${DATA_DIR:-./data}/postgres:/var/lib/postgresql/data

Multi-Environment Setup: Dev/Staging/Production Patterns

Real projects need different configurations for development, staging, and production. Here are proven patterns.

Pattern 1: Override Files (Recommended)

Use a base docker-compose.yml with environment-specific override files. Compose merges them automatically.

# File structure:
project/
  docker-compose.yml              # Base configuration
  docker-compose.override.yml     # Dev overrides (auto-loaded!)
  docker-compose.staging.yml      # Staging overrides
  docker-compose.prod.yml         # Production overrides
# docker-compose.yml (base)
services:
  api:
    image: myapp:${TAG:-latest}
    restart: unless-stopped
    depends_on:
      - db
      - redis

  db:
    image: postgres:16
    volumes:
      - pg-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  pg-data:
# docker-compose.override.yml (development - auto-loaded)
services:
  api:
    build: .
    volumes:
      - .:/app                          # Hot reload
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229"                     # Debugger
    environment:
      NODE_ENV: development
      LOG_LEVEL: debug
      DB_HOST: db

  db:
    ports:
      - "5432:5432"                     # Expose DB for local tools
    environment:
      POSTGRES_PASSWORD: devpassword    # OK for dev only
# docker-compose.prod.yml (production)
services:
  api:
    # No build, no volumes, no debug port
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      LOG_LEVEL: warn
    env_file:
      - .env.production
    secrets:
      - db_password
      - api_key
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

  db:
    # No exposed ports
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G

secrets:
  db_password:
    file: ./secrets/db_password
  api_key:
    file: ./secrets/api_key
# Usage:

# Development (auto-loads docker-compose.override.yml)
docker compose up

# Staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Validate merged config before deploying
docker compose -f docker-compose.yml -f docker-compose.prod.yml config

Pattern 2: Environment-Specific .env Files

Use the --env-file flag to load different variable sets per environment.

# .env.development
TAG=latest
APP_PORT=3000
DB_PASSWORD=devpassword
LOG_LEVEL=debug
REPLICAS=1

# .env.staging
TAG=rc-1.2.3
APP_PORT=3000
DB_PASSWORD=staging-secret-pw
LOG_LEVEL=info
REPLICAS=2

# .env.production
TAG=1.2.3
APP_PORT=3000
DB_PASSWORD=prod-ultra-secret-pw
LOG_LEVEL=warn
REPLICAS=3

# Usage:
docker compose --env-file .env.development up       # Dev
docker compose --env-file .env.staging up -d         # Staging
docker compose --env-file .env.production up -d      # Production

Pattern 3: Profiles for Optional Services

Use Compose profiles to include or exclude services per environment.

# docker-compose.yml
services:
  api:
    image: myapp:${TAG:-latest}
    ports:
      - "3000:3000"

  db:
    image: postgres:16
    volumes:
      - pg-data:/var/lib/postgresql/data

  # Dev-only services
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - dev                           # Only starts with --profile dev

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"
    profiles:
      - dev                           # Only starts with --profile dev

  # Monitoring (staging + production)
  prometheus:
    image: prom/prometheus
    profiles:
      - monitoring                    # Only starts with --profile monitoring

volumes:
  pg-data:

# Usage:
docker compose --profile dev up                     # Includes adminer + mailhog
docker compose --profile monitoring up -d           # Includes prometheus
docker compose --profile dev --profile monitoring up  # Both profiles

Security Checklist: What NEVER to Put in Environment Variables

Environment variables are convenient but inherently insecure for sensitive data. Follow these rules:

NEVER Do ThisDo This Instead
Hardcode passwords in docker-compose.ymlUse .env files (gitignored) or Docker secrets
Commit .env files to version controlAdd .env to .gitignore, commit .env.example instead
Use environment variables for private keys or certificatesMount them as files via volumes or Docker secrets
Log environment variables in application codeRedact sensitive values in logs
Share .env files via Slack, email, or chatUse a secrets manager (Vault, AWS Secrets Manager, 1Password)
Use the same secrets across dev/staging/productionGenerate unique secrets per environment
# .gitignore — essential entries for Docker projects
.env
.env.*
!.env.example
secrets/
*.pem
*.key
*.crt
# .env.example — commit this as a template
# Database
DB_HOST=localhost
DB_PORT=5432
DB_USER=myapp
DB_PASSWORD=CHANGE_ME

# API Keys
API_KEY=CHANGE_ME
JWT_SECRET=CHANGE_ME

# Application
NODE_ENV=development
LOG_LEVEL=debug
APP_PORT=3000

Debugging: Why Isn't My Env Var Loading?

Follow this systematic flowchart when an environment variable isn't reaching your container.

Step 1: Is the variable defined?

Check that the variable exists in your .env file, env_file, or environment directive.

Step 2: Is it reaching the container?

Run docker compose exec <service> env to see all environment variables inside the container.

Step 3: Is the .env file in the right place?

The .env file must be in the same directory as docker-compose.yml, or specified with --env-file.

Step 4: Is there a naming conflict?

Check for typos, case sensitivity, and precedence (environment > env_file > .env).

Step 5: Is the Compose file valid?

Run docker compose config to see the fully resolved configuration with all variables substituted.

Step 6: Is the variable being overridden?

Shell environment variables override .env file values. Check with echo $VAR_NAME on your host.

Useful Debug Commands

# 1. See the fully resolved Compose config (all variables substituted)
docker compose config

# 2. Check environment variables inside a running container
docker compose exec api env

# 3. Check a specific variable
docker compose exec api sh -c 'echo $DATABASE_URL'

# 4. Inspect container config (shows env vars, mounts, etc.)
docker inspect <container_id> --format '{{json .Config.Env}}' | jq .

# 5. Check if .env file is being read
docker compose config --format json | jq '.services.api.environment'

# 6. Verify env_file exists and is readable
docker compose config 2>&1 | grep -i "env_file"

# 7. Run a one-off container to test environment
docker compose run --rm api env | sort

# 8. Check for shell variable conflicts
env | grep -i "DB_"

# 9. Validate Compose file syntax
docker compose config --quiet && echo "Valid" || echo "Invalid"

# 10. Show variable substitution warnings
docker compose --verbose up 2>&1 | grep -i "variable"

Frequently Asked Questions

What is the difference between .env and env_file in Docker Compose?

The .env file is automatically read by Docker Compose for variable substitution in docker-compose.yml itself. The env_file directive loads variables directly into the container environment. They serve different purposes: .env is for Compose file interpolation, env_file is for container environment injection.

How do I pass environment variables from the host to a Docker container?

You can pass host environment variables by listing the variable name without a value in the environment directive (e.g., "- MY_VAR"), or by using variable substitution syntax like ${MY_VAR} in your docker-compose.yml. The host shell must have the variable set before running docker compose up.

Are Docker environment variables secure?

No, environment variables are not secure for sensitive data. They can be viewed with docker inspect, appear in /proc/environ inside the container, and may be logged by applications. For sensitive data like passwords and API keys, use Docker secrets (file-based) or a dedicated secrets manager.

What is the precedence order for environment variables in Docker Compose?

The precedence from highest to lowest is: 1) Values set with docker compose run -e, 2) Shell environment variables on the host, 3) The environment directive in docker-compose.yml, 4) The env_file directive, 5) Values from the Dockerfile ENV instruction. Higher-precedence values override lower ones.

How do I use different environment variables for development and production?

Use override files: create a base docker-compose.yml with shared config, then docker-compose.override.yml for development and docker-compose.prod.yml for production. Run with docker compose -f docker-compose.yml -f docker-compose.prod.yml up for production. Alternatively, use --env-file to load environment-specific .env files.

How do Docker secrets differ from environment variables?

Docker secrets are mounted as files (at /run/secrets/<secret_name>) rather than set as environment variables. They are more secure because they don't appear in docker inspect, process listings, or crash dumps. Many official Docker images support reading config from secret files using the _FILE suffix convention (e.g., POSTGRES_PASSWORD_FILE).

𝕏 Twitterin LinkedIn
Var detta hjälpsamt?

Håll dig uppdaterad

Få veckovisa dev-tips och nya verktyg.

Ingen spam. Avsluta när som helst.

Try These Related Tools

🐳Docker Compose GeneratorYMLYAML Validator & Formatter.gi.gitignore Generator

Related Articles

Docker Compose Fuskblad: Tjänster, Volymer och Nätverk

Docker Compose referens: tjänstedefinitioner, volymer, nätverk, miljövariabler och stack-exempel.

.env Guide: Best practices för miljövariabler

Bemästra .env filer och miljövariabler.