Build args and environment variables both pass values to your containers, but they work at different times and serve different purposes. Mixing them up is a common source of confusion.

Build args: build-time only

Build args are available during docker build and are not present in the running container:

services:
  app:
    build:
      context: .
      args:
        NODE_VERSION: "20"
        APP_VERSION: "2.1.0"

In the Dockerfile, they’re consumed with ARG:

ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim

ARG APP_VERSION
RUN echo "Building version ${APP_VERSION}"

After the build, these values are gone — they don’t exist in the running container.

Environment variables: runtime only

Environment variables are set in the running container but are not available during the build:

services:
  app:
    image: myapp
    environment:
      DATABASE_URL: postgres://db:5432/myapp
      LOG_LEVEL: info
      NODE_ENV: production

When you need both

Sometimes you need a value at both build-time and runtime. Pass it as a build arg, then convert it to an environment variable in the Dockerfile:

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
services:
  app:
    build:
      context: .
      args:
        NODE_ENV: production
    environment:
      NODE_ENV: production

Build args from the host environment

Build args can reference host environment variables, just like other Compose values:

services:
  app:
    build:
      context: .
      args:
        # From host environment or .env file
        GIT_COMMIT: ${GIT_COMMIT:-unknown}
        BUILD_DATE: ${BUILD_DATE:-}

Where does interpolation fit in?

There’s a third level that’s easy to confuse with the other two: Compose interpolation. When you write ${VAR} in a Compose file, Compose resolves it from your host environment or .env file at parse time, before any build or container starts:

services:
  app:
    image: myapp:${TAG:-latest}        # Interpolation: resolved before anything runs
    build:
      args:
        VERSION: ${VERSION}            # Interpolation → then passed as build arg
    environment:
      DATABASE_URL: ${DB_URL}          # Interpolation → then passed as container env var
      STATIC_VALUE: "hello"            # No interpolation, just a runtime value

The subtle part: DATABASE_URL: ${DB_URL} involves both interpolation and a container env var. Compose resolves ${DB_URL} from the host, then passes the result to the container. The container never sees the ${DB_URL} syntax — only the resolved value.

This means ${HOME} in environment: uses the host’s $HOME, not the container’s. Use $$HOME if you want the literal string $HOME passed to the container for shell expansion at runtime.

For more on interpolation syntax and precedence, see Tip #42.

Security considerations

Build args are visible in the image history:

docker history myapp

Never pass secrets as build args — they’ll be baked into image layers. Use secrets or mounted files instead:

services:
  app:
    build:
      context: .
      secrets:
        - npm_token
    environment:
      # Runtime secrets are fine here
      API_KEY: ${API_KEY}

secrets:
  npm_token:
    file: ./secrets/npm_token.txt

Quick reference

Build argsEnvironment variables
WhenBuild timeRuntime
DockerfileARGENV
Composebuild.argsenvironment
In containerNoYes
In image layersYes (visible!)No
Use forBase image version, build configApp config, credentials

Further reading