| |

Docker 16 🐳 Setting Environment Variables and Working Directories: ENV, ARG, and WORKDIR

Every Dockerfile that installs software, compiles code, or configures a runtime needs to control two things: the values that parameterize the build and the environment in which the application runs. ARG provides build-time variables that exist only during the image build. ENV provides environment variables that persist into the running container. WORKDIR sets the working directory for every subsequent instruction. Together, these three instructions shape how the image is built and how the container behaves at runtime. Understanding their scopes, their persistence, and their interaction is essential for writing Dockerfiles that are flexible, secure, and predictable.

The distinction between ARG and ENV is one of the most commonly misunderstood aspects of Dockerfile authoring. An ARG is a build argument: it can be overridden with --build-arg at build time, it is available only during the build, and it does not persist in the final image. An ENV is an environment variable: it is baked into the image, it persists into every container created from that image, and it can be overridden at runtime with docker run -e. Confusing the two leads to secrets baked into images, build failures from missing variables, and configuration that cannot be changed without rebuilding.

WORKDIR is the third piece. It sets the working directory for all subsequent RUN, CMD, ENTRYPOINT, COPY, and ADD instructions. A WORKDIR persists across instructions, unlike cd inside a RUN, which only affects that single instruction. Using absolute paths and setting WORKDIR once per build stage is the idiomatic pattern.

This chapter covers the ARG instruction and its scope, the ENV instruction and its persistence, the ARG + ENV combination, the WORKDIR instruction and its interaction with environment variables, and the patterns that keep builds flexible and containers secure.

Key point: ARG defines build-time variables that are overridable with --build-arg and do not persist into the container. ENV defines environment variables that persist into the container and can be overridden with docker run -e. WORKDIR sets the working directory for all subsequent instructions and persists across them. Use ARG for version numbers and build flags, ENV for runtime configuration, and WORKDIR with absolute paths.


Why ARG, ENV, and WORKDIR exist

The parameterization problem. A Dockerfile that hardcodes the Node.js version, the package repository, or the build configuration cannot be reused without editing. ARG parameterizes the build, so the same Dockerfile can produce different images with different --build-arg values.

The runtime configuration problem. An application needs to know its port, its database URL, and its log level at runtime. ENV bakes default values into the image, so the container starts with sensible defaults. The values can be overridden at runtime without rebuilding.

The persistence problem. A variable set with ARG is gone after the build. If the application needs the value at runtime, the ARG must be promoted to an ENV. This explicit promotion is the mechanism that separates build-time concerns from runtime concerns.

The working-directory problem. Every RUN instruction executes in a new shell. A cd in one RUN does not affect the next. WORKDIR provides a persistent working directory that applies to all subsequent instructions, which is why it replaces the pattern of chaining cd commands.

The security problem. Both ARG and ENV are visible in the image history. A secret passed as an ARG or set as an ENV can be extracted from the image. The correct approach for secrets is BuildKit’s secret mounting, not environment variables or build arguments. Understanding the visibility of ARG and ENV is what prevents the accidental leakage of credentials.


a. The ARG instruction

ARG declares a build-time variable. It is available only during the build, and it can be overridden with --build-arg at build time.

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

The ARG before FROM is in the global scope. It is available for the FROM instruction, but it is not automatically available inside the build stage. To use it within the stage, it must be re-declared after FROM .

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

# Re-declare to use inside the stage
ARG NODE_VERSION
RUN echo "Building with Node ${NODE_VERSION}"

An ARG declared after FROM is scoped to that build stage. In a multi-stage build, each stage needs its own declaration .

ARG values are overridden at build time with the --build-arg flag:

docker build --build-arg NODE_VERSION=22 -t my-app .

If no --build-arg is provided, the default value is used. If no default is provided and no --build-arg is given, the variable is empty.

ARG is the right tool for version numbers, build flags, and any value that parameterizes the build. It is not the right tool for secrets, because the value is visible in the image history and metadata .


b. The ENV instruction

ENV sets an environment variable that persists into the container. The value is baked into the image and is available to every process that runs in a container created from the image.

ENV NODE_ENV=production
ENV PORT=3000

Multiple variables can be set in one ENV instruction:

ENV NODE_ENV=production \
    PORT=3000 \
    LOG_LEVEL=info

ENV values can be overridden at runtime with docker run -e:

docker run -e PORT=4000 my-app

The variable inside the container is set to 4000, overriding the value baked into the image.

ENV is the right tool for runtime configuration: the application’s port, the log level, the database host, the feature flags. The values are defaults that the runtime can override.

A common use of ENV is updating PATH so that installed software can be found without an absolute path:

ENV PATH=/usr/local/nginx/bin:$PATH
CMD ["nginx"]

Each ENV instruction creates a layer, and the value persists in that layer even if it is unset in a later layer. To use a variable temporarily and then unset it, the set, use, and unset must happen in a single RUN instruction .


c. The ARG + ENV pattern

An ARG is not available at runtime. To make a build-time value available to the container, the ARG must be promoted to an ENV.

ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}

The ARG receives the value at build time, and the ENV bakes it into the image. The container can then read APP_VERSION from its environment.

This pattern is common for embedding the build version, the git commit hash, or the build timestamp into the image so the application can report it.

ARG GIT_COMMIT=unknown
ENV GIT_COMMIT=${GIT_COMMIT}

At build time:

docker build --build-arg GIT_COMMIT=$(git rev-parse HEAD) -t my-app .

The container’s environment now contains the commit hash, and the application can read it from process.env.GIT_COMMIT or the equivalent.

The promotion is explicit, which is the point. The developer decides which build-time values become runtime values, and the decision is visible in the Dockerfile.


d. The WORKDIR instruction

WORKDIR sets the working directory for all subsequent RUN, CMD, ENTRYPOINT, COPY, and ADD instructions. It creates the directory if it does not exist.

WORKDIR /app
COPY package.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

Every instruction after WORKDIR /app executes in /app. The COPY package.json ./ copies into /app/package.json. The npm ci runs in /app. The COPY . . copies the build context into /app.

WORKDIR persists across instructions, unlike cd inside a RUN, which only affects that single shell. This is the reason WORKDIR exists: it provides a persistent working directory without the fragility of chained cd commands .

If the directory does not exist, WORKDIR creates it. A preceding RUN mkdir -p /app is unnecessary .

WORKDIR can resolve environment variables that were set with ENV:

ENV APP_HOME=/app
WORKDIR $APP_HOME

The WORKDIR resolves to /app because APP_HOME was set with ENV .


e. Absolute versus relative paths in WORKDIR

An absolute path in WORKDIR starts from the root of the filesystem. A relative path resolves against the previous WORKDIR, creating an implicit dependency chain.

WORKDIR /app
WORKDIR src
WORKDIR components

The final working directory is /app/src/components. Each relative WORKDIR resolves against the previous one .

The relative form is convenient for creating nested directories, but it is fragile. If the base image’s WORKDIR changes, or if a WORKDIR is reordered, the resulting directory hierarchy changes silently. The Docker documentation warns that relative WORKDIR paths can produce unexpected results and recommends absolute paths .

# Recommended
WORKDIR /app/src/components

# Avoid
WORKDIR app
WORKDIR src
WORKDIR components

The absolute form is self-documenting. The reader sees the full path, and the path does not depend on the instructions that came before.

In a multi-stage build, each stage has its own WORKDIR. The build stage and the runtime stage can use different working directories, and each should set its own with an absolute path .


f. WORKDIR and the build context

The WORKDIR affects the destination of COPY and ADD when the destination is relative.

WORKDIR /app
COPY package.json ./

The ./ resolves to /app/, so the file is copied to /app/package.json. If WORKDIR were /usr/src/app, the same instruction would copy to /usr/src/app/package.json.

This is why the order of WORKDIR and COPY matters. Setting WORKDIR before the first COPY ensures that all relative copy destinations resolve against the intended directory.

The COPY instruction can also use an absolute destination that ignores WORKDIR:

WORKDIR /app
COPY config.json /etc/myapp/config.json

The absolute destination is used as-is, and the WORKDIR is not applied.


Complete Example Session

# ============================================
# PART 1: BASIC ARG
# ============================================
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
# ============================================
# PART 2: ARG SCOPE
# ============================================
ARG BASE_VERSION=20
FROM node:${BASE_VERSION}-alpine

# Re-declare to use inside the stage
ARG BASE_VERSION
RUN echo "Building with Node ${BASE_VERSION}"
# ============================================
# PART 3: OVERRIDE ARG AT BUILD TIME
# ============================================
# docker build --build-arg BASE_VERSION=22 -t my-app .
# ============================================
# PART 4: BASIC ENV
# ============================================
FROM node:20-alpine
ENV NODE_ENV=production
ENV PORT=3000
ENV LOG_LEVEL=info
# ============================================
# PART 5: ENV WITH MULTIPLE VARIABLES
# ============================================
ENV NODE_ENV=production \
    PORT=3000 \
    LOG_LEVEL=info
# ============================================
# PART 6: OVERRIDE ENV AT RUNTIME
# ============================================
# docker run -e PORT=4000 my-app
# ============================================
# PART 7: ARG PROMOTED TO ENV
# ============================================
ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}
# ============================================
# PART 8: BASIC WORKDIR
# ============================================
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
# ============================================
# PART 9: WORKDIR WITH ENV
# ============================================
ENV APP_HOME=/app
WORKDIR $APP_HOME
# ============================================
# PART 10: MULTI-STAGE WITH SEPARATE WORKDIRS
# ============================================
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app

FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app /app
CMD ["/app"]

These ten parts cover basic ARG, ARG scope, overriding ARG, basic ENV, multiple ENV variables, overriding ENV, ARG promoted to ENV, basic WORKDIR, WORKDIR with ENV, and multi-stage builds with separate working directories.


Quick Reference

ARG vs ENV

PropertyARGENV
Available during buildYesYes
Available at runtimeNoYes
Overridable at build--build-argNo
Overridable at runtimeNodocker run -e
Visible in image historyYesYes
Suitable for secretsNoNo

ARG Scope

DeclarationScope
Before FROMGlobal; available only for FROM
After FROMBuild stage
Re-declared in stageAvailable in that stage
Multi-stageEach stage needs its own

ENV Persistence

AspectDetail
Baked into imageYes
Available to RUNYes
Available to CMDYes
Available in containerYes
Overridable at runtimedocker run -e

WORKDIR

AspectDetail
Persists across instructionsYes
Creates directoryYes
Resolves ENV variablesYes
Absolute path recommendedYes
Default if not set/

Common Commands

CommandPurpose
docker build --build-arg KEY=valOverride ARG
docker run -e KEY=valOverride ENV
docker run --env-file fileLoad ENV from file

Best Practices

✅ Do This:

# Use ARG for version numbers
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine

# Re-declare ARG to use in the stage
ARG NODE_VERSION
RUN echo "Node ${NODE_VERSION}"

# Use ENV for runtime configuration
ENV NODE_ENV=production
ENV PORT=3000

# Promote ARG to ENV when needed at runtime
ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}

# Use absolute WORKDIR
WORKDIR /app

# Set WORKDIR once per stage
WORKDIR /src

❌ Don’t Do This:

# Use ENV for build-time versions
ENV NODE_VERSION=20  # ❌ cannot override at build time

# Use ARG for runtime configuration
ARG PORT=3000  # ❌ not available in the container

# Use relative WORKDIR
WORKDIR app  # ❌ implicit dependency chain

# Forget to re-declare ARG after FROM
ARG VERSION=1.0
FROM alpine
RUN echo $VERSION  # ❌ empty

# Use ARG or ENV for secrets
ARG DB_PASSWORD=secret  # ❌ visible in image history

Common Pitfalls

PitfallWhy It HappensFix
ARG empty inside stageNot re-declared after FROMRe-declare without default
ENV not overridable at buildENV cannot be set via --build-argUse ARG + ENV
WORKDIR path unexpectedRelative path chainUse absolute paths
cd in RUN does not persistEach RUN is a new shellUse WORKDIR
Secrets visibleUsed ARG or ENVUse BuildKit secret mount
ENV persists after unsetLayer still contains the valueSet, use, unset in one RUN

Real-World Examples

1. Version ARG

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

2. Re-declared ARG

ARG NODE_VERSION
RUN echo "Node ${NODE_VERSION}"

3. Runtime ENV

ENV NODE_ENV=production
ENV PORT=3000

4. ARG to ENV

ARG APP_VERSION=1.0.0
ENV APP_VERSION=${APP_VERSION}

5. Basic WORKDIR

WORKDIR /app
COPY . .

6. WORKDIR with ENV

ENV APP_HOME=/app
WORKDIR $APP_HOME

7. Absolute WORKDIR in Multi-Stage

FROM golang:1.22 AS builder
WORKDIR /src

8. Override at Build

docker build --build-arg NODE_VERSION=22 -t my-app .

9. Override at Runtime

docker run -e PORT=4000 my-app

10. BuildKit Secret

RUN --mount=type=secret,id=token \
    TOKEN=$(cat /run/secrets/token) && \
    ./configure --token=$TOKEN

Visual

ARG vs ENV Scope

┌──────────────────────────────────────────────────────────────┐
│  BUILD TIME                          RUNTIME                 │
│  ┌────────────────────────────┐      ┌────────────────────┐  │
│  │  ARG                       │      │                    │  │
│  │  Available during build    │      │  Not available     │  │
│  │  Overridable --build-arg   │      │                    │  │
│  └────────────────────────────┘      └────────────────────┘  │
│                                                              │
│  ┌────────────────────────────┐      ┌────────────────────┐  │
│  │  ENV                       │      │  ENV               │  │
│  │  Available during build    │ ───▶ │  Persists          │  │
│  │  Baked into image          │      │  Overridable -e    │  │
│  └────────────────────────────┘      └────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

ARG Scope

┌──────────────────────────────────────────────────────────────┐
│  ARG VERSION=1.0          ← global scope                     │
│       │                                                      │
│       ▼                                                      │
│  FROM base:${VERSION}     ← available for FROM               │
│       │                                                      │
│       ▼                                                      │
│  ARG VERSION              ← re-declared for the stage        │
│       │                                                      │
│       ▼                                                      │
│  RUN echo $VERSION        ← available in the stage           │
└──────────────────────────────────────────────────────────────┘

WORKDIR Persistence

┌──────────────────────────────────────────────────────────────┐
│  WORKDIR /app                                                │
│       │                                                      │
│       ├── RUN npm ci          ← runs in /app                 │
│       ├── COPY . .            ← copies to /app               │
│       └── CMD ["node"]        ← runs in /app                 │
│                                                              │
│  Unlike cd in a RUN, WORKDIR persists across instructions.   │
└──────────────────────────────────────────────────────────────┘

Multi-Stage WORKDIR

┌──────────────────────────────────────────────────────────────┐
│  STAGE 1: builder                                            │
│  WORKDIR /src                                                │
│  └── compiles the application                                │
│                                                              │
│  STAGE 2: runtime                                            │
│  WORKDIR /app                                                │
│  └── copies the binary and runs it                           │
│                                                              │
│  Each stage has its own WORKDIR.                             │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
ARGBuild-time variable, overridable with --build-arg
ENVRuntime variable, persists into container
ARG scopeGlobal before FROM, stage after
ENV overridedocker run -e KEY=val
ARG + ENVPromote build-time value to runtime
WORKDIRWorking directory for subsequent instructions
WORKDIR persistenceAcross all subsequent instructions
WORKDIR creationCreates directory if absent
Absolute pathsRecommended for WORKDIR
SecretsUse BuildKit secret mount, not ARG/ENV

Key takeaways:

  • ARG is for build-time variables. It is overridable with --build-arg and does not persist into the container. Use it for version numbers, build flags, and repository URLs .
  • ENV is for runtime variables. It is baked into the image and available in every container. It can be overridden at runtime with docker run -e .
  • ARG before FROM is not available inside the stage. It must be re-declared after FROM to be used within the build stage .
  • Promote ARG to ENV when a build-time value is needed at runtime. The ENV APP_VERSION=${APP_VERSION} pattern makes the value visible to the container .
  • WORKDIR sets a persistent working directory. It applies to all subsequent RUN, CMD, ENTRYPOINT, COPY, and ADD instructions, unlike cd inside a RUN .
  • Use absolute paths in WORKDIR. Relative paths create implicit dependency chains that break when instructions are reordered or the base image changes .
  • Never use ARG or ENV for secrets. Both are visible in the image history. Use BuildKit secret mounting for sensitive values .

Remember: ARG, ENV, and WORKDIR control the build parameters, the runtime environment, and the working directory. ARG is the build-time variable that parameterizes the Dockerfile. ENV is the runtime variable that configures the container. WORKDIR is the persistent directory that applies to every instruction. The ARG + ENV combination is the mechanism for promoting build-time values to runtime. Use ARG for versions and flags, ENV for configuration, and WORKDIR with absolute paths. Keep secrets out of both ARG and ENV.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!