Fix: Docker Build ARG Not Available in RUN Commands
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Docker build ARG variables that are empty or undefined inside RUN commands — why ARG scope is limited, how ARG and ENV interact, multi-stage build ARG scoping, and secrets that shouldn't use ARG.
The Error
A Docker build ARG is passed but evaluates to empty inside a RUN command:
ARG API_URL
RUN echo "API URL is: $API_URL"
# Output: API URL is: ← empty!Or the build succeeds but the ARG value is not embedded in the image:
docker build --build-arg API_URL=https://api.example.com .
# Inside container: echo $API_URL → emptyOr in a multi-stage build, an ARG defined in one stage is not available in another:
ARG VERSION=1.0.0
FROM node:20 AS builder
RUN echo $VERSION # Empty — ARG before FROM is not automatically availableWhy This Happens
Docker’s ARG instruction has scope rules that are easy to get wrong. The mental model that trips developers up is treating ARG like a global build variable. It is not. ARG is scoped to the stage it is declared in, and an ARG declared before any FROM lives in its own pre-stage scope that is only visible to FROM itself.
The common pitfalls:
ARGbeforeFROMis only available toFROM— anARGdeclared before the firstFROMinstruction can only be used inFROMitself (e.g., to set the base image version). It is not available inRUN,ENV, or other instructions.ARGscope ends at the stage boundary — in multi-stage builds, eachFROMstarts a new stage with a fresh scope. AnARGfrom stage 1 is not inherited by stage 2 unless redeclared.ARGvsENV—ARGvalues are only available at build time. They are not persisted in the final image or available at container runtime. UseENVto make a value available at runtime.- ARG with no default and no
--build-argvalue — if you declareARG MYVARwithout a default and don’t pass--build-arg MYVAR=value, the variable is empty.
There is also a subtle BuildKit behavior worth knowing. BuildKit (Docker 20.10+, default in modern installs) treats ARG references inside RUN commands as part of the cache key. If you change --build-arg API_URL=..., only the layers from the first RUN that uses $API_URL onward are rebuilt — earlier layers stay cached. That is usually what you want, but it means a stale build cache can hide an ARG misconfiguration: the layer that prints the value was built with an old value and never re-ran. When debugging, force a full rebuild with --no-cache before concluding that the ARG itself is broken.
Fix 1: Declare ARG in the Correct Scope
The scope rule: Every ARG must be declared within the stage that uses it:
# Wrong — ARG before FROM is only available to FROM, not to RUN
ARG NODE_VERSION=20
FROM node:${NODE_VERSION} # ✓ Works here
RUN echo $NODE_VERSION # ✗ Empty — out of scope
# Correct — redeclare ARG after FROM to make it available in the stage
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}
ARG NODE_VERSION # Redeclare (no default needed — inherits build-arg value)
RUN echo $NODE_VERSION # ✓ WorksFull example with multi-stage build:
# Global ARG — only for FROM instructions
ARG BASE_IMAGE=node:20-alpine
# Stage 1 — Builder
FROM ${BASE_IMAGE} AS builder
# Redeclare ARGs needed in this stage
ARG APP_VERSION=dev
ARG BUILD_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN echo "Building version: $APP_VERSION for env: $BUILD_ENV"
RUN npm run build
# Stage 2 — Production
FROM ${BASE_IMAGE} AS production
# ARGs do NOT carry over — redeclare what you need
ARG APP_VERSION=dev # Must redeclare here if used in this stage
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
ENV NODE_ENV=production
ENV APP_VERSION=${APP_VERSION} # Convert ARG to ENV for runtime availability
CMD ["node", "dist/server.js"]Fix 2: Pass ARG Values at Build Time
# Pass a single ARG
docker build --build-arg API_URL=https://api.example.com .
# Pass multiple ARGs
docker build \
--build-arg API_URL=https://api.example.com \
--build-arg APP_VERSION=1.2.3 \
--build-arg BUILD_ENV=production \
-t myapp:latest .
# With docker-compose# docker-compose.yml
services:
app:
build:
context: .
args:
API_URL: https://api.example.com
APP_VERSION: ${APP_VERSION:-dev} # Falls back to 'dev' if not set in host env
BUILD_ENV: productionVerify the ARG is received:
ARG API_URL
RUN echo "=== Build Args ===" && \
echo "API_URL=${API_URL}" && \
test -n "$API_URL" || (echo "ERROR: API_URL is empty" && exit 1)Fix 3: Convert ARG to ENV for Runtime Availability
ARG variables exist only during the build. If you need the value at container runtime (when the container runs), convert it to ENV:
FROM node:20-alpine
# Build-time only — empty at runtime
ARG API_URL
ARG APP_VERSION
# Convert to ENV — available at both build time AND runtime
ENV API_URL=${API_URL}
ENV APP_VERSION=${APP_VERSION:-unknown}
# Now available in RUN (build time)
RUN echo "Building with API_URL=${API_URL}"
# And available at container startup (runtime)
CMD ["node", "-e", "console.log(process.env.API_URL)"]Difference between ARG and ENV:
| Feature | ARG | ENV |
|---|---|---|
Available in RUN | ✓ (within scope) | ✓ |
| Available at runtime | ✗ | ✓ |
Appears in docker inspect | ✗ | ✓ |
| Overridable at build time | ✓ (via --build-arg) | ✗ |
| Overridable at run time | ✗ | ✓ (via -e flag) |
Warning:
ENVvalues set fromARGare baked into the image layers and visible indocker inspectand the image history. Do not useARG/ENVfor secrets (API keys, passwords). Use Docker secrets or a runtime secrets manager instead.
Fix 4: Fix ARG in Multi-Stage Builds
A common pattern — pass a build argument through multiple stages:
ARG REGISTRY=docker.io
ARG IMAGE_TAG=latest
# Stage 1 — Dependencies
FROM ${REGISTRY}/node:20-alpine AS deps
ARG NPM_TOKEN # Redeclare for this stage
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
COPY package*.json ./
RUN npm ci
RUN rm ~/.npmrc # Remove token file — not in final image
# Stage 2 — Builder
FROM deps AS builder
ARG APP_VERSION=dev # Redeclare for this stage
WORKDIR /app
COPY . .
ENV NEXT_PUBLIC_APP_VERSION=${APP_VERSION}
RUN npm run build
# Stage 3 — Runner
FROM ${REGISTRY}/node:20-alpine AS runner
ARG APP_VERSION=dev # Redeclare again if needed in runner stage
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
ENV NODE_ENV=production
ENV APP_VERSION=${APP_VERSION}
EXPOSE 3000
CMD ["npm", "start"]Build with all required args:
docker build \
--build-arg REGISTRY=my-registry.example.com \
--build-arg IMAGE_TAG=20-alpine \
--build-arg NPM_TOKEN=${NPM_TOKEN} \
--build-arg APP_VERSION=$(git describe --tags --always) \
--target runner \
-t myapp:latest .Fix 5: Avoid Using ARG for Secrets
ARG values appear in the Docker build cache and image history — they are not secure for secrets:
# This exposes the secret in docker history
docker build --build-arg DATABASE_PASSWORD=secret123 .
docker history myimage
# IMAGE CREATED BY
# ... /bin/sh -c #(nop) ARG DATABASE_PASSWORD=secret123 ← Visible!Use BuildKit secrets instead:
# syntax=docker/dockerfile:1
FROM node:20-alpine
# Mount a secret — not baked into any layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm config set //registry.npmjs.org/:_authToken="${NPM_TOKEN}" && \
npm ci && \
npm config delete //registry.npmjs.org/:_authToken# Pass secret at build time — not stored in image
DOCKER_BUILDKIT=1 docker build \
--secret id=npm_token,env=NPM_TOKEN \
.Or use .env file as secret:
DOCKER_BUILDKIT=1 docker build \
--secret id=envfile,src=.env.production \
.RUN --mount=type=secret,id=envfile \
export $(cat /run/secrets/envfile | xargs) && \
your-command-that-needs-env-varsFix 6: Use ARG for Conditional Logic
FROM node:20-alpine
ARG INSTALL_DEV_DEPS=false
COPY package*.json ./
# Conditional install based on ARG
RUN if [ "$INSTALL_DEV_DEPS" = "true" ]; then \
npm install; \
else \
npm ci --omit=dev; \
fi
# Build with dev deps
# docker build --build-arg INSTALL_DEV_DEPS=true .
# Build for production (default)
# docker build .ARG for platform-specific builds:
FROM --platform=${BUILDPLATFORM} node:20-alpine AS builder
ARG TARGETARCH
ARG TARGETPLATFORM
RUN echo "Building for platform: ${TARGETPLATFORM}, arch: ${TARGETARCH}"# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-t myapp:latest .In Production: Incident Lens
The classic Docker ARG incident has the worst shape: the image builds successfully, passes its smoke tests in CI, and gets promoted to production. Then, on first start in the cluster, the container crashes with a “missing environment variable” error or — worse — boots clean while pointing at the wrong API. The image looked correct because RUN saw the ARG, but the runtime process inherits nothing from ARG, only from ENV. Blast radius: every replica that pulls the new image crashes on startup, the rollout halts, and traffic drains to the previous revision.
Detection. Wire a startup probe that fails on missing config. Kubernetes liveness and readiness probes catch this within the rollout window if the entrypoint exits non-zero on missing env. For Docker Swarm or ECS, an explicit HEALTHCHECK that verifies required env vars achieves the same. Add a CI check that runs docker run --rm IMAGE printenv | grep -E '^(APP_VERSION|API_URL)=' against the built image so missing runtime vars fail the pipeline before promote.
Recovery. When the alert fires, the fast fix is to set the missing variables at runtime via --env, -e, or the orchestrator’s env config and restart the container. That gets traffic flowing again. Then patch the Dockerfile to convert the build-time ARG into a runtime ENV: ARG API_URL followed by ENV API_URL=${API_URL} in the same stage. Rebuild, push, and roll forward. Do not edit the running container’s env in place as a permanent fix — the next deploy will reintroduce the bug.
Prevention. Adopt a dual-declaration pattern: every value that the application reads at runtime is declared as ARG NAME and immediately bridged to ENV NAME=${NAME} in the final stage. The entrypoint script then validates required env vars are non-empty and exits 1 if any are missing — failing fast at startup instead of bleeding into a broken response from the application. Document which vars are build-time-only (versions, feature flags baked into bundles) vs runtime, and code-review every Dockerfile change against that list.
Still Not Working?
Enable BuildKit for better caching and secret support:
# Enable BuildKit
export DOCKER_BUILDKIT=1
docker build .
# Or set globally in Docker daemon
# /etc/docker/daemon.json: { "features": { "buildkit": true } }Print all available ARGs in your Dockerfile for debugging:
ARG API_URL
ARG APP_VERSION
ARG BUILD_ENV
RUN echo "=== All Build Args ===" && \
echo "API_URL=${API_URL:-NOT SET}" && \
echo "APP_VERSION=${APP_VERSION:-NOT SET}" && \
echo "BUILD_ENV=${BUILD_ENV:-NOT SET}"Check if the ARG is being shadowed by ENV. If an ENV instruction sets a variable with the same name as an ARG, the ENV value takes precedence for subsequent instructions:
ARG NODE_ENV=development
ENV NODE_ENV=production # Overrides ARG
RUN echo $NODE_ENV # 'production' — not 'development'Check docker-compose.yml does not silently drop the build arg. Compose only forwards an args: entry to the build if it is explicitly listed; values inherited from the shell environment are not passed through unless declared with ARG NAME (no value) under args:. Run docker compose config to see the resolved build args before debugging the Dockerfile itself.
Inspect the image layers for the ARG value to confirm it was set at build time:
docker history --no-trunc IMAGE | grep ARG
docker inspect IMAGE --format '{{json .Config.Env}}'If the ENV in Config.Env is empty or missing, the ARG never made it to the final stage — usually because the ENV NAME=${NAME} line is in an earlier stage and not the runtime stage.
Check for special build args injected automatically by BuildKit. BUILDPLATFORM, TARGETPLATFORM, TARGETARCH, TARGETOS, and TARGETVARIANT are pre-defined and do not need --build-arg — but you still need to declare them with ARG TARGETARCH inside the stage that uses them. Forgetting the declaration is the single most common cause of empty multi-arch builds.
For related Docker and configuration issues, see Fix: Docker Compose Environment Variables Not Loading, Fix: dotenv Not Loading, Fix: Docker Build ARG Not Set, and Fix: env Variable Undefined.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Coolify Not Working — Deployment Failing, SSL Not Working, or Containers Not Starting
How to fix Coolify self-hosted PaaS issues — server setup, application deployment, Docker and Nixpacks builds, environment variables, SSL certificates, database provisioning, and GitHub integration.
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 Multi-Platform Build Not Working — buildx Fails, Wrong Architecture, or QEMU Error
How to fix Docker multi-platform build issues — buildx setup, QEMU registration, --platform flag usage, architecture-specific dependencies, and pushing multi-arch manifests to a registry.