Docker Best Practices: Using ARG and ENV in Your Dockerfiles
If you’ve worked with Docker for any length of time, you’re likely accustomed to writing or at least modifying a Dockerfile. This file can be thought of as a recipe for a Docker image; it contains both the ingredients (base images, packages, files) and the instructions (various RUN
, COPY
, and other commands that help build the image).
In most cases, Dockerfiles are written once, modified seldom, and used as-is unless something about the project changes. Because these files are created or modified on such an infrequent basis, developers tend to rely on only a handful of frequently used instructions — RUN
, COPY
, and EXPOSE
being the most common. Other instructions can enhance your image, making it more configurable, manageable, and easier to maintain.
In this post, we will discuss the ARG
and ENV
instructions and explore why, how, and when to use them.
ARG: Defining build-time variables
The ARG
instruction allows you to define variables that will be accessible during the build stage but not available after the image is built. For example, we will use this Dockerfile to build an image where we make the variable specified by the ARG
instruction available during the build process.
FROM ubuntu:latest ARG THEARG="foo" RUN echo $THEARG CMD ["env"]
If we run the build, we will see the echo foo
line in the output:
$ docker build --no-cache -t argtest . [+] Building 0.4s (6/6) FINISHED docker:desktop-linux <-- SNIP --> => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => [2/2] RUN echo foo 0.1s => exporting to image 0.0s <-- SNIP -->
However, if we run the image and inspect the output of the env
command, we do not see THEARG
:
$ docker run --rm argtest PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=d19f59677dcd HOME=/root
ENV: Defining build and runtime variables
Unlike ARG
, the ENV
command allows you to define a variable that can be accessed both at build time and run time:
FROM ubuntu:latest ENV THEENV="bar" RUN echo $THEENV CMD ["env"]
If we run the build, we will see the echo bar
line in the output:
$ docker build -t envtest . [+] Building 0.8s (7/7) FINISHED docker:desktop-linux <-- SNIP --> => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => [2/2] RUN echo bar 0.1s => exporting to image 0.0s <-- SNIP -->
If we run the image and inspect the output of the env
command, we do see THEENV
set, as expected:
$ docker run --rm envtest PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=f53f1d9712a9 THEENV=bar HOME=/root
Overriding ARG
A more advanced use of the ARG
instruction is to serve as a placeholder that is then updated at build time:
FROM ubuntu:latest ARG THEARG RUN echo $THEARG CMD ["env"]
If we build the image, we see that we are missing a value for $THEARG
:
$ docker build -t argtest . <-- SNIP --> => CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => [2/2] RUN echo $THEARG 0.1s => exporting to image 0.0s => => exporting layers 0.0s <-- SNIP -->
However, we can pass a value for THEARG
on the build command line using the --build-arg
argument. Notice that we now see THEARG
has been replaced with foo
in the output:
=> CACHED [1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => [2/2] RUN echo foo 0.1s => exporting to image 0.0s => => exporting layers 0.0s <-- SNIP -->
The same can be done in a Docker Compose file by using the args
key under the build
key. Note that these can be set as a mapping (THEARG: foo
) or a list (- THEARG=foo
):
services: argtest: build: context: . args: THEARG: foo
If we run docker compose up --build
, we can see the THEARG
has been replaced with foo
in the output:
$ docker compose up --build <-- SNIP --> => [argtest 1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => CACHED [argtest 2/2] RUN echo foo 0.0s => [argtest] exporting to image 0.0s => => exporting layers 0.0s <-- SNIP --> Attaching to argtest-1 argtest-1 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin argtest-1 | HOSTNAME=d9a3789ac47a argtest-1 | HOME=/root argtest-1 exited with code 0
Overriding ENV
You can also override ENV
at build time; this is slightly different from how ARG
is overridden. For example, you cannot supply a key without a value with the ENV
instruction, as shown in the following example Dockerfile:
FROM ubuntu:latest ENV THEENV RUN echo $THEENV CMD ["env"]
When we try to build the image, we receive an error:
$ docker build -t envtest . [+] Building 0.0s (1/1) FINISHED docker:desktop-linux => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 98B 0.0s Dockerfile:3 -------------------- 1 | FROM ubuntu:latest 2 | 3 | >>> ENV THEENV 4 | RUN echo $THEENV 5 | -------------------- ERROR: failed to solve: ENV must have two arguments
However, we can remove the ENV
instruction from the Dockerfile:
FROM ubuntu:latest RUN echo $THEENV CMD ["env"]
This allows us to build the image:
$ docker build -t envtest . <-- SNIP --> => [1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => CACHED [2/2] RUN echo $THEENV 0.0s => exporting to image 0.0s => => exporting layers 0.0s <-- SNIP -->
Then we can pass an environment variable via the docker run
command using the -e
flag:
$ docker run --rm -e THEENV=bar envtest PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=638cf682d61f THEENV=bar HOME=/root
Although the .env
file is usually associated with Docker Compose, it can also be used with docker run
.
$ cat .env THEENV=bar $ docker run --rm --env-file ./.env envtest PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=59efe1003811 THEENV=bar HOME=/root
This can also be done using Docker Compose by using the environment
key. Note that we use the variable format for the value:
services: envtest: build: context: . environment: THEENV: ${THEENV}
If we do not supply a value for THEENV
, a warning is thrown:
$ docker compose up --build WARN[0000] The "THEENV" variable is not set. Defaulting to a blank string. <-- SNIP --> => [envtest 1/2] FROM docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04 0.0s => => resolve docker.io/library/ubuntu:latest@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab6 0.0s => CACHED [envtest 2/2] RUN echo ${THEENV} 0.0s => [envtest] exporting to image 0.0s <-- SNIP --> ✔ Container dd-envtest-1 Recreated 0.1s Attaching to envtest-1 envtest-1 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin envtest-1 | HOSTNAME=816d164dc067 envtest-1 | THEENV= envtest-1 | HOME=/root envtest-1 exited with code 0
The value for our variable can be supplied in several different ways, as follows:
- On the compose command line:
$ THEENV=bar docker compose up [+] Running 2/0 ✔ Synchronized File Shares 0.0s ✔ Container dd-envtest-1 Recreated 0.1s Attaching to envtest-1 envtest-1 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin envtest-1 | HOSTNAME=20f67bb40c6a envtest-1 | THEENV=bar envtest-1 | HOME=/root envtest-1 exited with code 0
- In the shell environment on the host system:
$ export THEENV=bar $ docker compose up [+] Running 2/0 ✔ Synchronized File Shares 0.0s ✔ Container dd-envtest-1 Created 0.0s Attaching to envtest-1 envtest-1 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin envtest-1 | HOSTNAME=20f67bb40c6a envtest-1 | THEENV=bar envtest-1 | HOME=/root envtest-1 exited with code 0
- In the special
.env
file:
$ cat .env THEENV=bar $ docker compose up [+] Running 2/0 ✔ Synchronized File Shares 0.0s ✔ Container dd-envtest-1 Created 0.0s Attaching to envtest-1 envtest-1 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin envtest-1 | HOSTNAME=20f67bb40c6a envtest-1 | THEENV=bar envtest-1 | HOME=/root envtest-1 exited with code 0
Finally, when running services directly using docker compose run
, you can use the -e
flag to override the .env
file.
$ docker compose run -e THEENV=bar envtest [+] Creating 1/0 ✔ Synchronized File Shares 0.0s PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOSTNAME=219e96494ddd TERM=xterm THEENV=bar HOME=/root
The tl;dr
If you need to access a variable during the build process but not at runtime, use ARG
. If you need to access the variable both during the build and at runtime, or only at runtime, use ENV
.
To decide between them, consider the following flow (Figure 1):
Both ARG
and ENV
can be overridden from the command line in docker run
and docker compose
, giving you a powerful way to dynamically update variables and build flexible workflows.
Learn more
- Discover more Docker Best Practices.
- Subscribe to the Docker Newsletter.
- Get the latest release of Docker Desktop.
- Have questions? The Docker community is here to help.
- New to Docker? Get started.