Env Vars, Volumes & Networks
Environment variables in Compose
Section titled “Environment variables in Compose”There are two ways to pass environment variables into a service.
Inline environment
Section titled “Inline environment”Define variables directly in compose.yaml:
services: app: image: myapp:latest environment: - NODE_ENV=production - PORT=3000env_file
Section titled “env_file”Reference an external file. Compose reads each KEY=VALUE line from the file and injects it into the container:
services: app: image: myapp:latest env_file: - .envThe .env file:
NODE_ENV=productionPORT=3000DATABASE_URL=postgres://db:5432/mydbKeep .env out of version control — it usually contains secrets.
Variable interpolation with ${VAR}
Section titled “Variable interpolation with ${VAR}”Compose reads a .env file in the same directory as compose.yaml and uses it to substitute ${VAR} placeholders inside the compose.yaml itself:
services: db: image: postgres:18-alpine environment: - POSTGRES_PASSWORD=${DB_PASSWORD} - POSTGRES_DB=${DB_NAME}With a .env file containing:
DB_PASSWORD=supersecretDB_NAME=mydbCompose substitutes the values before starting the container. This keeps secrets out of compose.yaml and lets different developers use their own local .env files.
Named volumes
Section titled “Named volumes”Containers are ephemeral — data written inside a container disappears when the container is removed. Named volumes persist data on the host and survive container restarts and recreations.
Declare a named volume at the top level and mount it per-service:
services: db: image: postgres:18-alpine volumes: - pgdata:/var/lib/postgresql environment: - POSTGRES_PASSWORD=secret
volumes: pgdata:The top-level volumes: key registers the volume with Compose. The per-service volumes: entry mounts it. You can mount the same named volume in multiple services.
Custom networks
Section titled “Custom networks”Compose creates a default network automatically. For more control — isolating services or connecting services across Compose files — define custom networks:
services: frontend: image: nginx:alpine networks: - public
backend: image: myapp:latest networks: - public - private
db: image: postgres:18-alpine networks: - private
networks: public: private:Here frontend can reach backend, but frontend cannot reach db directly. backend bridges both networks.
Hands-on practice
Section titled “Hands-on practice”The snippet below wires together a service with an env file and a named volume so the data survives restarts.
# Create .env file
cat > .env <<'EOF'
APP_MSG=Hello from .env interpolation!
VOLUME_DATA_DIR=/data
EOF
# Create compose.yaml with env interpolation and a named volume
cat > compose.yaml <<'EOF'
services:
writer:
image: alpine
env_file:
- .env
volumes:
- mydata:${VOLUME_DATA_DIR}
command: >
sh -c "
echo ${APP_MSG} &&
echo 'Writing to named volume...' &&
echo 'persistent data' > ${VOLUME_DATA_DIR}/file.txt &&
echo 'Wrote file to volume.'
"
reader:
image: alpine
volumes:
- mydata:/data
command: >
sh -c "
sleep 2 &&
echo 'Reading from volume:' &&
cat /data/file.txt
"
depends_on:
- writer
volumes:
mydata:
EOF
# Run both services
docker compose up
# Verify the volume still exists after containers stop
docker volume ls | grep mydata
# Clean up (keeps the volume)
docker compose down
# Clean up including the volume
docker compose down -vTrade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
.env + interpolation | No secrets hardcoded in compose.yaml, each developer can use their own values | Have to manage the .env file yourself outside git and keep its keys in sync with what compose.yaml references |
| Named volume + custom network | Data survives restarts, and traffic is clearly separated by network | Volumes/networks are tied to the project name — a name collision can mount the wrong data |
Common mistakes
Section titled “Common mistakes”- Accidentally committing a
.envfile with real secrets (likeDB_PASSWORD) to git — always put.envin.gitignoreand commit only.env.example. - Forgetting that the named volumes and networks Compose creates are project-scoped (named from the directory) — similarly named projects can create duplicate volumes without realizing it.
- Confusing
environment:withenv_file:and expecting a.envfile to be read into the container automatically without declaringenv_file:— interpolation andenv_fileare two separate mechanisms.
💡 Real-world example
Teams doing CI/CD commonly keep
.env.example(committed to git to document which keys need setting) separate from the real.envthat holds secrets, injecting the real values through a secrets manager or CI variable instead — the same pattern as using Compose locally and passing real secrets through a Kubernetes Secret when deploying to production.