Docker Environment Variables Explained

What Are Environment Variables?

Environment variables are key-value pairs that configure application behavior without hardcoding values in files. In Docker, they’re the primary way to configure containers — setting database passwords, API keys, ports, timezone, and feature flags.

Every self-hosted Docker app uses environment variables. Getting them right is the difference between a working deployment and a broken one.

Prerequisites

Setting Environment Variables in Docker Compose

Method 1: Inline in docker-compose.yml

services:
  nextcloud:
    image: nextcloud:29.0
    environment:
      - MYSQL_HOST=db
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=changeme-strong-password
      - NEXTCLOUD_ADMIN_USER=admin
      - NEXTCLOUD_ADMIN_PASSWORD=changeme-admin-password
      - NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
    restart: unless-stopped

Or using the map syntax (equivalent):

    environment:
      MYSQL_HOST: db
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_PASSWORD: changeme-strong-password

Both formats work. The list format (- KEY=value) is more common in self-hosting documentation.

Separate secrets from your Compose file:

# .env (in the same directory as docker-compose.yml)
MYSQL_ROOT_PASSWORD=super-secret-root-password
MYSQL_DATABASE=nextcloud
MYSQL_USER=nextcloud
MYSQL_PASSWORD=changeme-strong-password
NEXTCLOUD_ADMIN_USER=admin
NEXTCLOUD_ADMIN_PASSWORD=changeme-admin-password
TZ=America/New_York
# docker-compose.yml
services:
  db:
    image: mariadb:11.4
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    restart: unless-stopped

  nextcloud:
    image: nextcloud:29.0
    environment:
      - MYSQL_HOST=db
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER}
      - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD}
    restart: unless-stopped

Docker Compose automatically reads the .env file in the same directory. Variables referenced with ${VAR_NAME} are substituted.

Advantages of .env files:

  • Keep secrets out of docker-compose.yml (which you might commit to git)
  • Share variables between services (one MYSQL_PASSWORD used by both the database and the app)
  • Easy to have different .env files per environment

Method 3: env_file Directive

Point to a specific env file (doesn’t have to be named .env):

services:
  myapp:
    image: myapp:v1.0
    env_file:
      - ./myapp.env
      - ./shared.env
    restart: unless-stopped
# myapp.env
APP_SECRET=some-secret
FEATURE_FLAG=true

# shared.env
TZ=America/New_York

Difference from .env: The .env file is used for variable substitution in the Compose file itself. env_file passes variables directly to the container. They serve different purposes and can be used together.

Variable Substitution Syntax

Docker Compose supports several substitution forms:

environment:
  # Simple substitution
  - DB_HOST=${DB_HOST}

  # Default value if not set
  - DB_PORT=${DB_PORT:-5432}

  # Error if not set
  - DB_PASSWORD=${DB_PASSWORD:?Database password is required}

  # Default value if empty or not set
  - DB_NAME=${DB_NAME:-myapp}
SyntaxBehavior
${VAR}Use VAR’s value, empty string if not set
${VAR:-default}Use VAR’s value, or default if not set/empty
${VAR-default}Use VAR’s value, or default if not set (but allows empty)
${VAR:?error}Use VAR’s value, or exit with error message if not set/empty

Common Environment Variables

These appear across many self-hosted apps:

VariablePurposeExample
TZTimezoneAmerica/New_York, Europe/London, UTC
PUIDProcess user ID (LinuxServer.io)1000
PGIDProcess group ID (LinuxServer.io)1000
MYSQL_ROOT_PASSWORDMySQL/MariaDB root passwordStrong random string
MYSQL_DATABASEDatabase name to createmyapp
MYSQL_USERDatabase usernamemyapp
MYSQL_PASSWORDDatabase user passwordStrong random string
POSTGRES_PASSWORDPostgreSQL superuser passwordStrong random string
POSTGRES_DBPostgreSQL database namemyapp
POSTGRES_USERPostgreSQL usernamemyapp
REDIS_PASSWORDRedis authentication passwordStrong random string

Generating Strong Passwords

Never use password123 or changeme in production. Generate random passwords:

# 32-character random password
openssl rand -base64 32

# 24-character alphanumeric
openssl rand -hex 12

# Using /dev/urandom
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32; echo

Put generated passwords in your .env file:

# Generate and write to .env
echo "MYSQL_ROOT_PASSWORD=$(openssl rand -base64 32)" >> .env
echo "MYSQL_PASSWORD=$(openssl rand -base64 32)" >> .env
echo "APP_SECRET=$(openssl rand -hex 32)" >> .env

Debugging Environment Variables

Check What’s Set Inside a Container

# List all env vars in a running container
docker exec mycontainer env

# Check a specific variable
docker exec mycontainer printenv MYSQL_HOST

# Check the Compose-resolved config
docker compose config

docker compose config shows the final Compose file with all variables substituted — useful for verifying .env values are being picked up.

Common Issues

Variable not being substituted:

# Check if .env file is in the right directory
ls -la .env

# Check if variable is defined
grep MY_VAR .env

# Check Docker Compose's resolved config
docker compose config | grep MY_VAR

Variable is set but app doesn’t see it:

Some apps read env vars only at first startup. After changing variables:

# Recreate the container (not just restart)
docker compose up -d --force-recreate

docker compose restart does NOT re-read environment variables. You need up -d --force-recreate or down + up -d.

Secrets vs Environment Variables

Environment variables are visible to anyone who can run docker inspect or docker exec env. For highly sensitive values (private keys, API tokens), Docker Secrets provide better isolation:

services:
  db:
    image: postgres:16.2
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
    secrets:
      - db_password
    restart: unless-stopped

secrets:
  db_password:
    file: ./secrets/db_password.txt

Note: Not all apps support _FILE suffix variables. Check the app’s documentation. PostgreSQL, MySQL, and many official Docker images do support them.

For most self-hosting setups, .env files with proper file permissions (chmod 600 .env) are sufficient. Docker Secrets add complexity that’s only warranted in multi-node or high-security environments.

Best Practices

1. Never Commit Secrets to Git

Add .env to .gitignore. Commit a .env.example with placeholder values instead:

# .gitignore
.env
secrets/

# .env.example (commit this)
MYSQL_ROOT_PASSWORD=change-me
MYSQL_PASSWORD=change-me
APP_SECRET=change-me
TZ=UTC

2. Use One .env File Per Service Stack

Don’t share a single .env across unrelated services. Each docker-compose.yml gets its own .env:

/opt/nextcloud/.env
/opt/nextcloud/docker-compose.yml

/opt/jellyfin/.env
/opt/jellyfin/docker-compose.yml

3. Document Every Variable

Add comments to your .env file:

# Database configuration
MYSQL_ROOT_PASSWORD=abc123    # Root password — change this
MYSQL_DATABASE=nextcloud      # Database name
MYSQL_USER=nextcloud          # App database user
MYSQL_PASSWORD=xyz789         # App database password — change this

# App configuration
NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com  # Your domain
TZ=America/New_York                          # Server timezone

4. Set Restrictive File Permissions

chmod 600 .env
chmod 600 secrets/*

5. Always Set TZ

Without TZ, containers default to UTC. Set your timezone consistently:

# In .env
TZ=America/New_York

Find your timezone string: timedatectl list-timezones

Common Mistakes

1. Using Quotes in .env Files

Docker Compose .env files don’t need quotes. Quotes become part of the value:

# Wrong — password includes the quotes
MYSQL_PASSWORD="my-password"
# Container sees: "my-password" (with quotes)

# Right
MYSQL_PASSWORD=my-password
# Container sees: my-password

2. Spaces Around the Equals Sign

# Wrong — variable name includes a space
MYSQL_PASSWORD = my-password

# Right
MYSQL_PASSWORD=my-password

3. Forgetting to Recreate After Changing Variables

# This does NOT apply new env vars
docker compose restart

# This does
docker compose up -d --force-recreate

4. Using the Same Password Everywhere

Each service should have its own unique password. A compromise of one doesn’t compromise all:

# Bad
MYSQL_ROOT_PASSWORD=same-password
MYSQL_PASSWORD=same-password
REDIS_PASSWORD=same-password

# Good — unique password per service
MYSQL_ROOT_PASSWORD=aK9x4Bm2pQ7nR3tW
MYSQL_PASSWORD=jH6sD8fL1yE5cV9b
REDIS_PASSWORD=mN3wX7qZ2uP4gT8k

5. Hardcoding Values That Should Be Configurable

If you might change a value later (domain name, port, storage path), put it in .env rather than hardcoding in docker-compose.yml.

FAQ

What’s the difference between .env and env_file?

The .env file substitutes variables in the docker-compose.yml file itself (the ${VAR} syntax). The env_file directive passes variables directly to the container. Use .env for Compose-level substitution and env_file for container-level configuration.

Can I use environment variables for Docker image tags?

Yes. This is a good practice for version pinning:

# .env
NEXTCLOUD_VERSION=29.0
MARIADB_VERSION=11.4
services:
  nextcloud:
    image: nextcloud:${NEXTCLOUD_VERSION}

Do I need to restart Docker after changing .env?

You need to recreate the container, not just restart it. Run docker compose up -d --force-recreate. A simple restart reuses the existing container with the old variables.

How do I pass multiline values?

Use \n for newlines or put the content in a file and mount it as a volume instead:

volumes:
  - ./config/my-config.conf:/etc/app/config.conf:ro

Are environment variables secure?

They’re visible via docker inspect and inside the container. For production secrets, use Docker Secrets, mount secret files as read-only volumes, or use a secrets manager like Vault. For homelab use, .env files with chmod 600 are adequate.

Next Steps