DevOps Peach

Docker & Kubernetes · Docker in Real Projects – Operations and Docker Compose · Lesson 4 of 4

Environment variables and networking in Compose

.env substitutes variables in the Compose file, environment/env_file put variables into containers; services reach each other by name on the default network.

Practice interactively →

Two kinds of variables that get mixed up

Mechanism Used for
A .env file next to compose.yaml (loaded automatically) or --env-file Substituting variables in the Compose file, for example image: node-app:${TAG}
The environment: key Variables inside the container, like docker run -e
The env_file: key Loading many variables from a file into the container
services:
  api:
    image: registry.example.com/node-app:${TAG}
    environment:
      DB_HOST: db
      DEBUG:            # empty: take the value from the shell running compose
    env_file: [api.env]

Variables in .env do not reach the container by themselves; declare them with environment or env_file. Switch environments with another file: docker compose --env-file .env.dev up -d. --env-file, -f and -p are global options and go before the subcommand (up, logs…).

The default network

flowchart LR
  subgraph NET["network node-app_default"]
    A["api"] -- "db:5432" --> D["db"]
  end

Compose creates the network <project>_default; every service joins it and reaches the others by service name (DNS). The project name defaults to the directory name (node-app); change it with -p myapp or COMPOSE_PROJECT_NAME, and the network becomes myapp_default.

The classic trap: the app in the api container connects to localhost:5432. localhost is the api container itself, so use db:5432. links is a legacy feature and is not needed for communication.

Command Meaning
docker compose --env-file .env.dev up -d Use another variables file
docker compose -p myapp up -d Set the project name
docker compose run --rm -e DEBUG=1 api node console.js One-off container with its own variable

Substitution tricks

Syntax Meaning
${TAG:-latest} Use latest when TAG is unset or empty
${TAG-latest} Use latest only when TAG is unset
${DB_PASSWORD:?set DB_PASSWORD} Stop with an error message when missing
$$ A literal dollar sign passed to the container

Variables come from the shell first, then from .env. docker compose config prints the final file with every variable resolved, which is the quickest way to check what Compose will really run.

Splitting networks

services:
  proxy:
    image: nginx:1.27
    ports: ["80:80"]
    networks: [frontend]
  api:
    build: .
    networks: [frontend, backend]
  db:
    image: postgres:16
    networks: [backend]
networks:
  frontend:
  backend:
    internal: true

Here the proxy can reach api but not db, and db has no route to the internet. ports: publishes to the host, while expose: only documents ports for other services on the same network.

Secrets in Compose

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
secrets:
  db_password:
    file: ./secrets/db_password.txt

The value is mounted as a file instead of appearing in docker inspect output. Many official images accept *_FILE variables for this reason. Keep the secrets folder out of Git.

Overrides and profiles

compose.override.yaml is merged automatically on top of compose.yaml, a common place for development-only settings such as bind mounts and debug ports. Profiles keep optional services out of the default start:

  adminer:
    image: adminer
    profiles: [tools]

docker compose --profile tools up -d starts them only when you ask.

Practice interactively →