Fix: Docker Build ARG Not Available — ENV Variables Missing at Runtime
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 runtimeOr 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 FROMOr --build-arg on the command line has no effect:
docker build --build-arg API_KEY=abc123 .
# Inside Dockerfile: $API_KEY is emptyWhy This Happens
Docker treats ARG and ENV as separate mechanisms with different scopes:
ARGis build-time only — available duringdocker build, not when the container runs. If you need a value at runtime, you must copy it into anENV.ARGbeforeFROMis global but limited — anARGdeclared before the firstFROMis available forFROMinstructions (e.g., setting a base image tag) but not inside the build stages unless re-declared.- Multi-stage builds reset
ARGscope — eachFROMstarts a new build stage.ARGvalues from a previous stage are not inherited. You must re-declareARGin each stage that needs it. ENVpersists to runtime —ENVvalues set during build are baked into the image and available when the container runs. This makes them visible indocker inspect— don’t useENVfor secrets.ARGwith no default requires--build-arg— if anARGhas no default value and--build-argisn’t passed, the value is an empty string. Docker doesn’t warn unless you use--build-argfor anARGnot 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 appearFix 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: 20Practical 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 tooShare 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 buildFix 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 metadataUse 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 secretsFix 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 fileStill 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/CMD — ARG 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" # WorksShell form vs exec form — CMD 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Docker Secrets Not Working — BuildKit --secret Not Mounting, Compose Secrets Undefined, or Secret Leaking into Image
How to fix Docker secrets — BuildKit secret mounts in Dockerfile, docker-compose secrets config, runtime vs build-time secrets, environment variable alternatives, and verifying secrets don't leak into image layers.
Fix: Docker Compose Healthcheck Not Working — depends_on Not Waiting or Always Unhealthy
How to fix Docker Compose healthcheck issues — depends_on condition service_healthy, healthcheck command syntax, start_period, custom health scripts, and debugging unhealthy containers.
Fix: docker-compose.override.yml Not Working — Override File Ignored or Not Merged
How to fix docker-compose.override.yml not being applied — file naming, merge behavior, explicit file flags, environment-specific configs, and common override pitfalls.
Fix: Docker HEALTHCHECK Failing — Container Marked Unhealthy Despite Running
How to fix Docker HEALTHCHECK failures — command syntax, curl vs wget availability, start period, interval tuning, health check in docker-compose, and debugging unhealthy containers.