Skip to content

Fix: Docker Build ARG Not Available in RUN Commands

FixDevs ·

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 → empty

Or 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 available

Why This Happens

Docker’s ARG instruction has scope rules that are easy to get wrong:

  • ARG before FROM is only available to FROM — an ARG declared before the first FROM instruction can only be used in FROM itself (e.g., to set the base image version). It is not available in RUN, ENV, or other instructions.
  • ARG scope ends at the stage boundary — in multi-stage builds, each FROM starts a new stage with a fresh scope. An ARG from stage 1 is not inherited by stage 2 unless redeclared.
  • ARG vs ENVARG values are only available at build time. They are not persisted in the final image or available at container runtime. Use ENV to make a value available at runtime.
  • ARG with no default and no --build-arg value — if you declare ARG MYVAR without a default and don’t pass --build-arg MYVAR=value, the variable is empty.

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      # ✓ Works

Full 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: production

Verify 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:

FeatureARGENV
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: ENV values set from ARG are baked into the image layers and visible in docker inspect and the image history. Do not use ARG/ENV for 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-vars

Fix 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 .

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'

For related Docker issues, see Fix: Docker Compose Environment Variables Not Loading and Fix: dotenv Not Loading.

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