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:17-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:17-alpine volumes: - pgdata:/var/lib/postgresql/data 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:17-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 -v