Skip to content

Fix: Docker Build ARG Not Available — ENV Variables Missing at Runtime

FixDevs ·

Quick Answer

How to fix Docker ARG and ENV variable issues — build-time vs runtime scope, ARG before FROM, multi-stage build variable passing, secret handling, and .env file patterns.

The Problem

A Docker ARG variable is defined but not available inside the build:

ARG APP_VERSION

FROM node:20-alpine

RUN echo "Version: $APP_VERSION"   # Prints: Version: (empty)

Or an ENV set from an ARG isn’t available when the container runs:

ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL

# Build passes fine
# Container starts — but DATABASE_URL is empty at runtime

Or variables disappear in multi-stage builds:

FROM node:20 AS builder
ARG NPM_TOKEN
RUN npm install   # NPM_TOKEN is available here

FROM node:20-alpine AS final
RUN echo $NPM_TOKEN   # Empty — ARG scope doesn't cross FROM

Or --build-arg on the command line has no effect:

docker build --build-arg API_KEY=abc123 .
# Inside Dockerfile: $API_KEY is empty

Why This Happens

Docker treats ARG and ENV as separate mechanisms with different scopes:

  • ARG is build-time only — available during docker build, not when the container runs. If you need a value at runtime, you must copy it into an ENV.
  • ARG before FROM is global but limited — an ARG declared before the first FROM is available for FROM instructions (e.g., setting a base image tag) but not inside the build stages unless re-declared.
  • Multi-stage builds reset ARG scope — each FROM starts a new build stage. ARG values from a previous stage are not inherited. You must re-declare ARG in each stage that needs it.
  • ENV persists to runtimeENV values set during build are baked into the image and available when the container runs. This makes them visible in docker inspect — don’t use ENV for secrets.
  • ARG with no default requires --build-arg — if an ARG has no default value and --build-arg isn’t passed, the value is an empty string. Docker doesn’t warn unless you use --build-arg for an ARG not declared in the Dockerfile.

Fix 1: Understand ARG vs ENV Scope

# ARG — available only during build
ARG BUILD_DATE
RUN echo "Built on: $BUILD_DATE"   # Works during build
# After container starts: BUILD_DATE is gone

# ENV — available during build AND at runtime
ENV NODE_ENV=production
RUN echo "Env: $NODE_ENV"          # Works during build
# After container starts: NODE_ENV=production still set

# Copy ARG into ENV to use at runtime
ARG API_URL
ENV API_URL=$API_URL               # Now available at runtime

# Verify at build time
RUN echo "API_URL during build: $API_URL"

Check which variables are set in the final image:

# Inspect ENV values baked into an image
docker inspect my-image | jq '.[0].Config.Env'

# Run a shell to check runtime environment
docker run --rm my-image env | sort

# These show ENV values — ARG values (not copied to ENV) won't appear

Fix 2: Fix ARG Declared Before FROM

An ARG before the first FROM only controls the FROM line itself, not the build stage:

# WRONG — ARG before FROM doesn't flow into the stage automatically
ARG NODE_VERSION=20

FROM node:${NODE_VERSION}-alpine   # Works — ARG used in FROM

RUN echo $NODE_VERSION             # Empty — ARG scope ended at FROM

# CORRECT — re-declare ARG inside the stage to use it
ARG NODE_VERSION=20                # Global scope (for FROM)

FROM node:${NODE_VERSION}-alpine

ARG NODE_VERSION                   # Re-declare inside stage (inherits the value)
RUN echo $NODE_VERSION             # Now prints: 20

Practical example — version pinning:

ARG ALPINE_VERSION=3.19
ARG NODE_VERSION=20

FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS base

# Re-declare to use inside the stage
ARG NODE_VERSION
ARG ALPINE_VERSION

RUN echo "Node: ${NODE_VERSION}, Alpine: ${ALPINE_VERSION}"

LABEL build.node-version="${NODE_VERSION}"
LABEL build.alpine-version="${ALPINE_VERSION}"

Fix 3: Pass ARG Across Multi-Stage Builds

Each FROM starts a new stage with a clean variable scope:

# WRONG — ARG from builder stage doesn't carry over
FROM node:20 AS builder
ARG NPM_TOKEN
RUN npm ci

FROM node:20-alpine AS final
COPY --from=builder /app/node_modules ./node_modules
RUN echo $NPM_TOKEN   # Empty — different stage

# CORRECT — re-declare ARG in each stage that needs it
FROM node:20 AS builder
ARG NPM_TOKEN                      # Declare in builder
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
RUN npm ci
RUN rm ~/.npmrc                    # Remove token from layer

FROM node:20-alpine AS final
ARG NPM_TOKEN                      # Re-declare in final (if needed)
COPY --from=builder /app/node_modules ./node_modules
# NPM_TOKEN is now available in final stage too

Share build metadata across stages:

ARG VERSION=dev
ARG BUILD_DATE

FROM node:20 AS builder
ARG VERSION                        # Re-declare
ARG BUILD_DATE

RUN echo "Building version ${VERSION} on ${BUILD_DATE}"
COPY . .
RUN npm run build

FROM nginx:alpine AS final
ARG VERSION                        # Re-declare for labels
ARG BUILD_DATE

COPY --from=builder /app/dist /usr/share/nginx/html

# Embed metadata in the final image
LABEL version="${VERSION}"
LABEL build.date="${BUILD_DATE}"

Fix 4: Pass Build Args Correctly on the Command Line

# Single ARG
docker build --build-arg NODE_ENV=production -t myapp .

# Multiple ARGs
docker build \
  --build-arg VERSION=1.2.3 \
  --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
  --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
  -t myapp:1.2.3 .

# ARG with spaces — quote the value
docker build --build-arg DESCRIPTION="My App v1.2.3" .

# Read ARG value from environment variable (if your shell var matches the ARG name)
export NPM_TOKEN=mytoken
docker build --build-arg NPM_TOKEN .   # Passes current shell value of NPM_TOKEN
# Even shorter: if ARG name matches env var, Docker auto-passes it:
# docker build --build-arg NPM_TOKEN .  (no =value needed — uses shell env)

Docker Compose build args:

# docker-compose.yml
services:
  app:
    build:
      context: .
      args:
        - VERSION=1.0.0
        - BUILD_DATE=2026-03-22
        # Or pass from host environment:
        - NPM_TOKEN   # Passes the value of $NPM_TOKEN from host shell
# Build with compose
NPM_TOKEN=mytoken docker compose build

Fix 5: Handle Secrets Safely

Don’t use ARG or ENV for secrets — they’re visible in docker history and docker inspect:

# This leaks the secret into image layers
docker build --build-arg DATABASE_PASSWORD=secret123 .
# docker history myimage shows the ARG value in layer metadata

Use BuildKit mount secrets (recommended):

# syntax=docker/dockerfile:1
FROM node:20 AS builder

# Mount secret at build time — never stored in a layer
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) \
    npm ci
# Pass secret via BuildKit
DOCKER_BUILDKIT=1 docker build \
  --secret id=npm_token,env=NPM_TOKEN \
  -t myapp .

# Or from a file
docker build \
  --secret id=npm_token,src=.npm_token \
  -t myapp .

Runtime secrets — pass via environment variables, not baked into image:

# Don't bake secrets into the image
# WRONG:
ENV DATABASE_URL=postgres://user:password@host/db

# CORRECT — leave it unset, pass at runtime
ENV DATABASE_URL=""   # Optional: set empty default for documentation
# Pass secrets at runtime
docker run \
  -e DATABASE_URL=postgres://user:password@host/db \
  -e JWT_SECRET=mysecret \
  myapp

# Or use a .env file (don't commit this file)
docker run --env-file .env myapp

# Or use Docker secrets (Swarm) / Kubernetes secrets

Fix 6: Default Values and Conditional Behavior

# ARG with default — used if --build-arg not provided
ARG NODE_ENV=development
ARG PORT=3000
ARG LOG_LEVEL=info

# Conditional build steps based on ARG
ARG INSTALL_DEV_TOOLS=false
RUN if [ "$INSTALL_DEV_TOOLS" = "true" ]; then \
      apt-get install -y vim curl jq; \
    fi

# Use ARG to select different base configurations
ARG ENVIRONMENT=production
COPY config/${ENVIRONMENT}.json /app/config.json

# Verify required ARGs are set (fail fast if missing)
ARG REQUIRED_TOKEN
RUN test -n "$REQUIRED_TOKEN" || (echo "ERROR: REQUIRED_TOKEN build arg must be set" && exit 1)

Print all build args for debugging:

ARG VERSION
ARG BUILD_DATE
ARG GIT_COMMIT

# Debug layer — shows all ARG values
RUN echo "=== Build Arguments ===" && \
    echo "VERSION=${VERSION}" && \
    echo "BUILD_DATE=${BUILD_DATE}" && \
    echo "GIT_COMMIT=${GIT_COMMIT}" && \
    echo "======================="

Fix 7: ENV at Runtime — Override and Defaults

ENV values baked into an image can be overridden at docker run time:

# Dockerfile — set sensible defaults
ENV PORT=3000 \
    LOG_LEVEL=info \
    NODE_ENV=production \
    MAX_CONNECTIONS=10
# Override specific values at runtime — others keep their image defaults
docker run \
  -e PORT=8080 \
  -e LOG_LEVEL=debug \
  myapp
# PORT=8080, LOG_LEVEL=debug, NODE_ENV=production (from image), MAX_CONNECTIONS=10 (from image)

Docker Compose environment override:

# docker-compose.yml
services:
  app:
    image: myapp
    environment:
      - PORT=8080
      - LOG_LEVEL=debug
      - DATABASE_URL=${DATABASE_URL}   # From host .env file or shell
    env_file:
      - .env.local   # Additional overrides from file

Still Not Working?

Build cache and ARG — Docker invalidates the build cache when ARG values change. If you change --build-arg VERSION=1.2.4, all layers from the ARG VERSION instruction onward are rebuilt. Place frequently-changing ARG declarations as late as possible to maximize cache reuse.

ARG in ENTRYPOINT/CMDARG values are not available in ENTRYPOINT or CMD at runtime. Only ENV values and runtime -e flags are available. If you need a build-time value at runtime, copy it to an ENV:

ARG BUILD_VERSION
ENV BUILD_VERSION=$BUILD_VERSION   # Now available at runtime via $BUILD_VERSION
CMD echo "Running version $BUILD_VERSION"   # Works

Shell form vs exec formCMD and ENTRYPOINT in exec form (["cmd", "arg"]) don’t expand shell variables. Use shell form (CMD echo $VAR) or an entrypoint script for variable expansion.

For related Docker issues, see Fix: Docker Layer Cache Invalidated and Fix: Docker Volume Permission Denied.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles