sehaj/learn
Docker/06 Compose

06.04 · with code · 11 min

Env files and profiles

Keep settings out of compose.yaml with variables and .env files, and keep optional tools out of the way with profiles.

The core idea

Two different things in Compose both get called "env", and mixing them up causes most of the confusion:

  • Variables in the compose file, like ${POSTGRES_PASSWORD}. Compose fills these in from your shell or a .env file before it does anything else.
  • Environment variables in a container, set with environment: or env_file:. These are what the app sees.

Variables in the file

compose.yaml
services:
proxy:
  ports:
    - "${PROXY_PORT:-8080}:80"
api:
  environment:
    DATABASE_URL: postgres://${POSTGRES_USER:-notes}:${POSTGRES_PASSWORD:-notes}@db:5432/${POSTGRES_DB:-notes}
    MAX_NOTE_LENGTH: ${MAX_NOTE_LENGTH:-280}

${VAR:-default} uses default when VAR is unset or empty. Others you'll meet: ${VAR} (empty if unset) and ${VAR:?message} (refuse to run if unset), handy for things that must never default, like production passwords.

Values come from, highest priority first:

  1. your shell: MAX_NOTE_LENGTH=99 docker compose up
  2. a file named .env next to compose.yaml, read automatically
  3. the default in the file

docker compose config prints the file with everything filled in, so you can check before you run:

❯terminal
$ docker compose config | grep MAX_NOTE_LENGTH
MAX_NOTE_LENGTH: "280"
$ echo "MAX_NOTE_LENGTH=50" > .env
$ docker compose config | grep MAX_NOTE_LENGTH
MAX_NOTE_LENGTH: "50"
$ MAX_NOTE_LENGTH=99 docker compose config | grep MAX_NOTE_LENGTH
MAX_NOTE_LENGTH: "99"

The project commits a .env.example with safe defaults and ignores .env, so everyone copies the example and changes their own copy:

.env.example
# Copy to .env and change as needed. Compose reads .env from this folder automatically (see 06.04).
POSTGRES_USER=notes
POSTGRES_PASSWORD=notes
POSTGRES_DB=notes
MAX_NOTE_LENGTH=280
PROXY_PORT=8080

Variables in the container

environment: sets variables directly. env_file: loads them from a file into that service's container:

compose.yaml
services:
api:
  env_file: ./api/.env.local     # every KEY=value in it becomes a container variable
  environment:
    LOG_LEVEL: debug             # environment: wins over env_file for the same key

The difference in one line: .env configures Compose; env_file: configures a container. A variable in .env only reaches the app if the compose file passes it on, like MAX_NOTE_LENGTH above.

Profiles: optional services

Some services are only sometimes useful: a database UI, a load generator, a debugging toolbox. Give them a profile and they're skipped unless you ask for it:

compose.yaml
adminer:
  image: adminer
  profiles: [tools]
  ports:
    - "127.0.0.1:8081:8080"
  environment:
    ADMINER_DEFAULT_SERVER: db
  networks: [backend]
❯terminal
$ docker compose up -d # proxy, api, db, cache: no adminer
$ docker compose --profile tools up -d # the same, plus adminer
$ curl -s -o /dev/null -w "%{http_code}\n" localhost:8081
200

Adminer is a web UI for Postgres at localhost:8081, published on 127.0.0.1 only so it isn't reachable from your network. You can also set COMPOSE_PROFILES=tools in .env to turn it on by default for yourself.

Predict first

Your .env contains DEBUG=true. compose.yaml never mentions DEBUG. Does the API container see DEBUG?

Commit to an answer before you read on.
CheckpointConfigure without editing YAML

Copy .env.example to .env, set PROXY_PORT=9090 and MAX_NOTE_LENGTH=50, and confirm both with docker compose config. Run up -d, reach the API on 9090, and check that a 60-character note is rejected. Then start Adminer with the tools profile and log in with the values from your .env.

STUCK? ▸

If docker compose config still shows the default, check that .env is in the same folder as compose.yaml and that there are no spaces around =.

Recap

Interview questions

MidfreqGood to knowConcept

What's the difference between the .env file and env_file in Docker Compose?

Report a mistakeEdit this page
☰ Compose06.04 · 35/44next →