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
| Property | ARG | ENV |
|---|---|---|
| Available during build | Yes | Yes |
| Available at runtime | No | Yes |
| Overridable at build | --build-arg | No |
| Overridable at runtime | No | docker run -e |
| Visible in image history | Yes | Yes |
| Suitable for secrets | No | No |
ARG Scope
| Declaration | Scope |
|---|---|
Before FROM | Global; available only for FROM |
After FROM | Build stage |
| Re-declared in stage | Available in that stage |
| Multi-stage | Each stage needs its own |
ENV Persistence
| Aspect | Detail |
|---|---|
| Baked into image | Yes |
Available to RUN | Yes |
Available to CMD | Yes |
| Available in container | Yes |
| Overridable at runtime | docker run -e |
WORKDIR
| Aspect | Detail |
|---|---|
| Persists across instructions | Yes |
| Creates directory | Yes |
Resolves ENV variables | Yes |
| Absolute path recommended | Yes |
| Default if not set | / |
Common Commands
| Command | Purpose |
|---|---|
docker build --build-arg KEY=val | Override ARG |
docker run -e KEY=val | Override ENV |
docker run --env-file file | Load 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
ARG empty inside stage | Not re-declared after FROM | Re-declare without default |
ENV not overridable at build | ENV cannot be set via --build-arg | Use ARG + ENV |
WORKDIR path unexpected | Relative path chain | Use absolute paths |
cd in RUN does not persist | Each RUN is a new shell | Use WORKDIR |
| Secrets visible | Used ARG or ENV | Use BuildKit secret mount |
ENV persists after unset | Layer still contains the value | Set, 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
| Item | Value |
|---|---|
ARG | Build-time variable, overridable with --build-arg |
ENV | Runtime variable, persists into container |
ARG scope | Global before FROM, stage after |
ENV override | docker run -e KEY=val |
ARG + ENV | Promote build-time value to runtime |
WORKDIR | Working directory for subsequent instructions |
WORKDIR persistence | Across all subsequent instructions |
WORKDIR creation | Creates directory if absent |
| Absolute paths | Recommended for WORKDIR |
| Secrets | Use BuildKit secret mount, not ARG/ENV |
Key takeaways:
ARGis for build-time variables. It is overridable with--build-argand does not persist into the container. Use it for version numbers, build flags, and repository URLs .ENVis for runtime variables. It is baked into the image and available in every container. It can be overridden at runtime withdocker run -e.ARGbeforeFROMis not available inside the stage. It must be re-declared afterFROMto be used within the build stage .- Promote
ARGtoENVwhen a build-time value is needed at runtime. TheENV APP_VERSION=${APP_VERSION}pattern makes the value visible to the container . WORKDIRsets a persistent working directory. It applies to all subsequentRUN,CMD,ENTRYPOINT,COPY, andADDinstructions, unlikecdinside aRUN.- Use absolute paths in
WORKDIR. Relative paths create implicit dependency chains that break when instructions are reordered or the base image changes . - Never use
ARGorENVfor 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!