ARG vs ENV in Docker
Two Dockerfile instructions that look almost identical on the surface but control completely different moments in an image's life — and mixing them up is one of the most common ways teams accidentally ship secrets, bloat images, or break reproducible builds.
If you have ever opened a Dockerfile and seen both ARG and ENV sitting a few lines apart, each assigning a value to something that looks like a variable, you have probably wondered whether they are really two different tools or just two spellings of the same idea. They are not the same idea. One of them exists only for the few seconds or minutes that Docker spends assembling your image. The other exists for the entire lifetime of every container that image will ever produce. Confusing the two is one of the most repeated small mistakes in containerized software — and it quietly causes real problems: build secrets that end up baked permanently into a public image, configuration that “disappears” the moment a container starts, and Dockerfiles that behave differently on a teammate’s laptop than they do in CI. This guide walks through what each instruction actually is, how Docker handles them internally, where they fit in the build-to-runtime pipeline, and how experienced teams use them together without stepping on the landmines.
ACore Concepts
What ARG actually is
ARG is a Dockerfile instruction that defines a variable a person or a build system can pass in at the moment an image is being built, using a flag like --build-arg. Think of it as a form field on the front door of the build process. Before Docker starts stacking layers, it asks, “Do you want to fill in any of these blanks?” Once the build finishes and the image is sealed up, that form is thrown away. The value never automatically becomes part of the finished image, and it is never available to a container running from that image unless you deliberately copy it somewhere else.
What ENV actually is
ENV is a Dockerfile instruction that sets an environment variable that becomes a permanent, visible property of the image itself. Every container started from that image inherits it automatically, the same way every employee who joins a company inherits the badge policy written into the employee handbook — nobody has to re-explain it each time someone starts a new shift. An ENV value is baked into the image metadata, so tools like docker inspect can read it, and any process running inside the container can read it too, without anyone doing anything special.
Why Docker needs both at all
A container image serves two very different audiences. During the build, the audience is the build engine itself and whoever triggered the build — a developer typing a command, or a CI pipeline substituting a version number. After the build, the audience is the running application and whoever operates it in production. ARG speaks to the first audience. ENV speaks to the second. Keeping them separate means a Dockerfile can accept build-time choices (which base image version to use, which internal package registry to pull from) without those choices leaking into the final, shipped artifact as if they were application settings.
Picture a furniture factory. ARG is like the custom-order slip a customer fills out before production starts — “oak, not pine; 6 drawers, not 4.” Once the dresser is built and shipped, that order slip stays in the factory’s filing cabinet; it doesn’t travel inside the dresser. ENV is like the manufacturer’s plate screwed onto the back of the dresser — “Made by Acme Furniture, Model 42, Care Instructions: wipe with dry cloth.” That plate ships with the product forever, and anyone who ever owns the dresser can read it.
BArchitecture & Components
A Docker build is not one single operation; it is a small pipeline made of distinct components, and ARG and ENV attach to different parts of that pipeline.
The folder Docker can see
The set of files sent to the build engine when you run a build command. Build-time values from ARG are supplied alongside this context, not stored inside it.
The engine that reads the Dockerfile
Processes instructions top to bottom, resolves any declared ARG values, and produces a new filesystem layer for most instructions.
The stacked filesystem snapshots
ENV writes into the image’s configuration metadata, which travels with every layer reference; it is not a filesystem file you can accidentally delete.
The image’s permanent record
Stores the final list of ENV variables. This is what docker inspect and container runtimes actually read.
What starts when you run the image
Reads the image config and injects every stored ENV variable into the new container’s process environment automatically.
Docker’s layer reuse system
Both ARG and ENV values participate in cache invalidation — changing either one can force later instructions to rebuild.
Notice the pattern: ARG lives entirely on the left side of this pipeline, in the builder’s working memory. ENV crosses all the way to the right side, into the image manifest and, from there, into every container. That single architectural fact — which component a value is allowed to reach — is the root cause of almost every behavioral difference discussed in this guide.
CInternal Working
How ARG resolves
When the builder hits an ARG line, it checks three sources in order: a value passed on the command line, a default written directly after the variable name in the Dockerfile, or — if neither exists — an empty value. Whatever it resolves to becomes available only for instructions that come after that line, and only within that build stage. Crucially, this resolved value is treated as build-time data: the builder can substitute it into later instructions (for example, choosing which base image tag to pull), but it does not, by itself, write anything into the environment that a future container will inherit.
How ENV resolves
When the builder hits an ENV line, it does two things at once: it makes the variable available to every subsequent instruction in that stage, just like ARG would, and it permanently records that key-value pair into the image’s configuration object. That second part is the one ARG never does. From this point forward, the variable is not a build-time convenience — it is a committed property of the image, the same way a paint color becomes a permanent property of a car once it leaves the factory line.
The bridge between them
Because ARG values vanish after the build and ENV values persist, teams that want a build-time choice to also be visible at runtime combine the two deliberately: declare an ARG, then assign it to an ENV variable on the very next line. That single hand-off is the most common pattern in production Dockerfiles, and it is worth internalizing early because most of the “advanced” patterns later in this guide are variations on it.
ARG is scoped to the build; ENV is scoped to the image and every container made from it. An ARG value can be copied into an ENV value, but an ENV value can never be “downgraded” back into a private build-only secret once it has been set — anyone with the image can read it.
DData Flow & Lifecycle
Imagine a team building a Node.js application image and wanting to control which Node.js version gets installed, while also wanting the application itself to know which environment — staging or production — it is running in. The first value is a build-time decision. The second value is a runtime fact the application needs. Tracing both through the pipeline shows exactly where ARG and ENV diverge.
flowchart TD
A["Developer runs: docker build --build-arg NODE_VERSION=18"] --> B["Builder reads ARG NODE_VERSION instruction"]
B --> C["ARG value 18 used to pick base image tag"]
C --> D["Image layers assembled"]
D --> E["ENV APP_ENV=production instruction runs"]
E --> F["APP_ENV written into image config"]
F --> G["docker build finishes: final image created"]
G --> H["NODE_VERSION value is gone - never stored in image"]
G --> I["APP_ENV=production stored permanently in image config"]
I --> J["docker run starts a container"]
J --> K["Container inherits APP_ENV=production automatically"]
H -.->|"not available"| J
Fig. 1 — ARG (NODE_VERSION) influences the build then disappears; ENV (APP_ENV) is written into the image and survives into every container.
Two things stand out in this flow. First, the build-time ARG did real, meaningful work — it changed which base image got used — before it vanished. It was not wasted; it simply finished its job at build time and had nothing left to do at runtime. Second, the ENV value made exactly one hop, from a Dockerfile instruction into the image config, and from there it is copied automatically into every container without any extra instruction needed. Once you can trace this diagram from memory, most Dockerfile debugging involving these two instructions becomes straightforward: ask “is this value supposed to still exist after the build finished?” and the answer tells you which instruction you needed.
Multi-stage builds add a second wrinkle
In a multi-stage Dockerfile, each FROM line starts a brand-new stage with its own scope. An ARG declared before the first FROM is only visible to later FROM lines unless it is re-declared inside a stage; an ARG or ENV declared inside one stage does not automatically carry into the next stage at all. This trips up teams who assume a variable set early in the file is visible everywhere — in Docker, each stage is closer to a fresh container than a continuous script.
EAdvantages, Disadvantages & Trade-offs
ARG — Strengths
- Lets one Dockerfile serve many purposes (different base versions, registries, feature flags) without editing the file
- Never lingers in the final image by default, keeping images smaller and leaner
- Ideal for values that genuinely only matter while assembling the image, like a cache-busting timestamp
ARG — Limitations
- Not available inside a running container unless explicitly copied into ENV
- Build history and intermediate layers can still expose values passed as build args, so it is not a true secrets mechanism
- Re-declaring ARG is required in every build stage that needs it, which surprises newcomers to multi-stage builds
ENV — Strengths
- Automatically available to every process in every container from that image, with zero extra wiring
- Readable with standard tooling (docker inspect, container orchestration dashboards), which aids debugging
- Perfect for genuine runtime configuration: log level, port number, feature toggles the app itself checks
ENV — Limitations
- Permanent once set — it becomes part of the image and travels with it, including to anyone who pulls the image
- Can silently override a value a container operator intended to set at “docker run” time if defaults are chosen carelessly
- Never a safe place for secrets, since anyone with pull access to the image can read it in plain text
The trade-off in one sentence: ARG trades permanence for flexibility at build time, and ENV trades flexibility for guaranteed availability at runtime. Choosing correctly usually just means asking whether the value should still exist tomorrow, in every copy of the image, on every machine it gets pulled to.
FSecurity
A surprisingly common mistake is passing a database password, an API key, or a signing certificate into an image using either --build-arg or a plain ENV line, under the assumption that it stays private because it “isn’t in the source code.” Neither assumption holds up. An ENV value is, by design, stored in plain text in the image configuration — anyone who can run docker pull and docker inspect on that image can read it in seconds, no special tools required. An ARG value is somewhat better hidden by default, since it does not automatically land in the final image, but it is frequently still recoverable: it can appear in the build cache, in intermediate layer history depending on how the build was structured, and in CI logs if the build command itself is echoed. Companies including Docker’s own security team and major cloud providers have published guidance specifically warning against using either instruction to pass credentials.
A developer sets ARG NPM_TOKEN to authenticate against a private package registry during install, then pushes the image to a public registry for convenience. Even though the running container never displays the token, security scanners regularly find it sitting in an old, cached intermediate layer — because a layer, once created, is not automatically deleted just because a later instruction “removed” the file in a new layer on top of it.
The safe pattern for actual secrets is to avoid both plain ARG and plain ENV for anything sensitive, and instead use a dedicated build-time secrets mechanism (such as BuildKit’s mount-based secrets feature) that never gets written into any layer at all, or to inject the secret only at container-start time through the orchestrator’s own secret-management system (Kubernetes Secrets, a cloud provider’s secret manager, or a runtime -e flag that is not baked into the image). The rule of thumb worth remembering: if a value must never be readable by anyone who obtains the image, it does not belong in an ARG or an ENV line at all.
GDesign Patterns & Anti-patterns
Pattern
The “hand-off” pattern: declare an ARG with a sensible default, then immediately assign it to an ENV variable of the same or related name on the next line.
Why it works
This gives a build-time override path (via --build-arg) for something that also needs to be readable at runtime, such as an application version string baked into a health-check endpoint. The default keeps the Dockerfile usable without any extra flags, and the override keeps it flexible for CI pipelines that inject the real version number.
Pattern
Using ARG to parameterize a base image tag near the very top of the Dockerfile, before the first FROM.
Why it works
It lets one Dockerfile build variants against several base image versions (for testing a language runtime upgrade, for example) without maintaining separate files, while guaranteeing the choice never leaks into the shipped image as an environment variable nobody asked for.
Anti-pattern
Using ENV for values that are only ever needed while the image is being built, such as a temporary compiler flag or a scratch directory path.
Why it causes problems
It leaves build-only clutter permanently visible in every container’s environment, which bloats the output of tools like printenv, confuses operators debugging a live container, and occasionally collides with an unrelated variable an application or orchestrator was expecting to control.
Anti-pattern
Assuming an ARG declared in one build stage of a multi-stage Dockerfile is automatically visible in a later stage without re-declaring it.
Why it causes problems
Each stage in a multi-stage build is its own isolated environment. A value that “worked” in stage one silently resolves to empty in stage two, producing hard-to-diagnose build failures that only show up deep in the log, far from the line that actually caused them.
HBest Practices & Common Mistakes
Give every ARG a safe default
A Dockerfile that fails to build without a manually supplied --build-arg is fragile. Defaults keep it usable for anyone who clones the repository.
Group related ENV lines together
Keeping runtime configuration in one visible block near the top of the relevant stage makes the image’s “public contract” easy to review at a glance.
Re-declare ARG per stage
In multi-stage builds, repeat the ARG line in every stage that actually needs the value — treat each stage as its own script.
Passing secrets through either instruction
Covered in depth above — this remains the single most consequential mistake teams make with these two instructions.
Overloading one variable for two jobs
Reusing the same name for both a build-time and a runtime concern (for example, one VERSION meaning “which base image” in one place and “what the app reports” in another) invites confusion during debugging.
Hardcoding what should be configurable
Values like a port number or a feature flag that operators legitimately need to change per environment belong in ENV (often with a runtime -e override), not buried as a literal deep in application code.
IReal-World & Industry Examples
Official language runtime images
Official Node.js, Python, and Go base images on Docker Hub commonly use ENV to set language-level defaults such as the installation path and the default locale, so that every container built from them behaves predictably without extra configuration from downstream teams.
Multi-architecture builds
Projects that publish images for several CPU architectures frequently use ARG TARGETARCH and related automatically populated build arguments (provided by BuildKit) to select the correct binary at build time, without that architecture string ever needing to appear as a runtime environment variable.
CI/CD pipelines
Continuous integration systems such as GitHub Actions and GitLab CI routinely pass the current commit SHA or build number into an image using --build-arg, which is then written into an ENV variable so the running application can expose its own build identity through a health or version endpoint.
Large platform teams
Organizations operating many microservices, including companies with sizable internal container platforms, standardize on a small, well-documented set of ENV variables (log level, service name, environment tier) that every internal image is expected to expose, so their orchestration and observability tooling can rely on consistent names across hundreds of services.
JFrequently Asked Questions
ARG value is gone unless it was explicitly assigned to an ENV variable before the build completed. If a container needs a value, it needs to have been handed off to ENV, or supplied fresh at docker run time.docker run time with -e overrides the ENV default baked into the image for that specific container, without changing the image itself. The ENV instruction really just sets the starting default every new container begins with.ARG or ENV.FROM line in a multi-stage Dockerfile has its own scope. An ARG declared in one stage is not automatically inherited by the next — it must be re-declared in any stage that needs it.ARG supplies a build-time default or override, and an ENV of the same name captures that value so it also exists at runtime — with the ENV line typically written to reference the ARG directly.KSummary and Key Takeaways
The essentials, in one place
- Scope is everything:
ARGlives only during the build;ENVlives inside the image and every container built from it. - ARG is a build-time form field. It can shape the build — choosing a base image, a version, a feature flag — and then it disappears.
- ENV is a permanent manufacturer’s plate. Once set, it travels with the image everywhere it is pulled and runs.
- The hand-off pattern is the workhorse: declare
ARG, assign it toENV, and you get a build-time override that also becomes visible at runtime. - Neither instruction is a secrets vault. Values from both can be recovered by anyone with access to the image or its build history — use dedicated secrets tooling instead.
- Multi-stage builds reset scope at every FROM. Re-declare an
ARGin each stage that actually needs it. - When in doubt, ask one question: “does this value need to still exist after the image is finished and shipped?” If yes, it is an
ENV. If no, it is anARG.