CalSync — Automate Outlook Calendar Colors

Auto-color-code events for your team using rules. Faster visibility, less admin. 10-user minimum · 12-month term.

CalSync Colors is a service by CPI Consulting

In this blog post Mastering Docker environment variables with Docker Compose we will walk through how to use environment variables confidently across Docker and Docker Compose, and how to avoid the common gotchas.

Environment variables are a simple, time-tested way to keep configuration outside your code. With containers, they become even more useful: the same image can run in multiple environments by swapping values at runtime. In this guide, we’ll start with the big picture and then show practical steps you can copy into your projects.

High-level overview

Docker images are built once, then run many times. Build-time settings shape how an image is produced. Runtime settings configure how a container behaves when it starts. Environment variables bridge both worlds:

  • At build time, ARG values let you parameterise the build without baking sensitive data into the image.
  • At runtime, ENV and Compose’s environment entries set application config inside the container.
  • Compose adds a .env file and variable substitution to keep your YAML tidy and DRY across machines.

The result: one codebase and one set of Dockerfiles can support dev, test, and production with minimal drift.

The technology behind it

Dockerfile: ARG vs ENV

  • ARG exists only during docker build. It’s not present at runtime unless you copy it into an ENV or your app.
  • ENV persists in the built image and becomes part of every container spawned from it.

Use ARG for build toggles (e.g., base image tag, package registry), and ENV for runtime defaults (e.g., NODE_ENV=production).

Docker Compose: .env, environment, env_file

  • .env in the Compose project directory provides values for variable substitution inside the YAML (e.g., ${IMAGE_TAG}). These are parsed at compose-file load time.
  • environment sets variables inside the running container.
  • env_file loads key-value pairs and injects them into the container environment (similar to --env-file with docker run).

Key point: variables in .env do not automatically become container environment variables unless you reference them in environment or include them via env_file.

Quick start in three files

Here’s a minimal but realistic setup. It shows build-time args, runtime env, and Compose substitution.

1) .env

2) Dockerfile

3) docker-compose.yml

Run it:

docker compose up --build -d

How Compose variable substitution works

Compose supports shell-style interpolation inside the YAML. Common patterns:

  • ${VAR} – use value from .env or your shell’s environment.
  • ${VAR:-default} – use default if VAR is unset or empty.
  • ${VAR?error} – fail with a message if VAR is missing.
services:
  api:
    image: cloudproinc/${APP_NAME}:${IMAGE_TAG:-latest}
    environment:
      LOG_LEVEL: ${LOG_LEVEL:-info}
      REQUIRED_TOKEN: ${REQUIRED_TOKEN?Set REQUIRED_TOKEN in your environment}

Remember: these substitutions happen when Compose parses the YAML, not at container runtime. If a value looks wrong, render the final config to inspect it:

docker compose config

Precedence and scope

Where does a container’s final environment variable value come from? Highest to lowest precedence:

  • Explicit values passed at runtime via docker run -e or --env-file (when not using Compose).
  • Compose environment entries.
  • Compose env_file entries.
  • ENV defaults in the Dockerfile image.

Separately, for Compose file interpolation, values come from (in order):

  • Environment of the Compose process (your shell).
  • The .env file located in the Compose project directory.
  • Defaults like ${VAR:-...} inside the YAML.

These two ladders are often confused. Interpolation fills the YAML. Runtime precedence decides what’s inside the container.

Using docker run directly

If you’re not using Compose, you can pass vars inline or via files:

Check what the container sees:

Handling secrets safely

Environment variables are convenient but not ideal for secrets. They may appear in docker inspect, crash logs, or process listings inside the container. Prefer:

  • Docker secrets (Swarm) or Compose secrets with files mounted at runtime.
  • External secret managers (cloud KMS, Vault) with SDKs or sidecars.

Example using file-based secret with Compose:

Your app reads the file at /run/secrets/db_password instead of a plain env var.

Debugging and verification

  • Render the final Compose config: docker compose config
  • Inspect a container: docker inspect <container> shows Config.Env
  • Print inside the container: docker exec <container> env | sort
  • Recreate with new values: docker compose up -d --build --force-recreate

    Production tips

    • Keep a minimal, documented .env.example checked into source control; never commit real secrets.
    • Separate concerns: build once with ARG, configure many times with runtime env.
    • Pin base images and package versions via ARG for reproducibility.
    • Use Compose profiles or separate override files to switch stacks or turn features on/off.
    • Centralise environment management in your CI/CD and pass values to Compose at deploy time.

    Putting it all together

    Environment variables give you a clean separation between code and configuration. Docker provides the mechanics (ARG, ENV), while Compose adds ergonomics (.env, interpolation, env_file). With a small amount of structure—defaults in the Dockerfile, environment blocks in Compose, and secrets out of env—you get portable builds, predictable runtime behavior, and a safer path to production.

    Start with the three-file quick start above, verify with docker compose config, and evolve from there. You’ll quickly find that managing configuration this way scales smoothly from a developer laptop to your production cluster.


    Discover more from CPI Consulting

    Subscribe to get the latest posts sent to your email.