Docker’s ARG and ENV instructions both seem like they’re for setting variables, but they operate at fundamentally different stages of the build process and have distinct implications for your image and running containers.
Let’s see ARG in action. Imagine you want to build a Go application, but you need to specify the version of Go to use during the build.
# Dockerfile
FROM golang:1.21-alpine AS builder
ARG GO_VERSION=1.21
RUN apk add --no-cache curl && \
curl -fsSL "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz" | tar -C /usr/local -xzf - && \
export PATH=$PATH:/usr/local/go/bin
# ... rest of your build steps
Now, you can build this with a specific Go version:
docker build --build-arg GO_VERSION=1.20 -t my-go-app .
Or, if you don’t provide GO_VERSION, it defaults to 1.21.
Contrast this with ENV. ENV sets environment variables that are available both during the build and after the container has started. Let’s say you want to set the application’s port and a configuration directory.
# Dockerfile
FROM alpine:latest
ENV APP_PORT=8080
ENV CONFIG_DIR=/etc/myapp/conf
RUN mkdir -p ${CONFIG_DIR} && \
echo "Setting up application directory: ${CONFIG_DIR}"
# ... your application files and entrypoint
CMD ["sh", "-c", "echo 'Application running on port ${APP_PORT}'"]
When you build this, APP_PORT and CONFIG_DIR are set. If you then run a container from this image:
docker run my-app
The output will be "Application running on port 8080".
The core difference is timing and scope. ARG variables are only available during the docker build command. They are resolved and injected into the build process, but they do not persist in the final image unless you explicitly use them to set an ENV variable. Think of ARG as build-time parameters. You can use ARG to control aspects of the build itself, like the version of a dependency or a build-time flag.
ENV variables, on the other hand, are baked into the image. They are accessible by any RUN instruction after the ENV declaration, and crucially, they are present in the running container’s environment. This makes ENV suitable for configuration that your application needs at runtime, such as API keys, database connection strings, or application ports.
Here’s a common pattern: using ARG to pass a value during build, and then using that ARG to set an ENV variable. This allows you to parameterize runtime configuration during the build.
# Dockerfile
FROM ubuntu:latest
ARG APP_VERSION=1.0.0
ENV APPLICATION_VERSION=${APP_VERSION}
RUN echo "Building application version: ${APP_VERSION}" && \
echo "This version will be available at runtime as: ${APPLICATION_VERSION}"
# ... rest of your image
When you build this with docker build --build-arg APP_VERSION=1.5.0 -t my-app ., the output of the RUN command will show 1.0.0 because APP_VERSION is resolved before it’s used in the RUN command. However, the ENV APPLICATION_VERSION=1.0.0 instruction sets the environment variable in the image. If you then docker run my-app and inspect its environment, you’ll see APPLICATION_VERSION=1.0.0. If you had built without --build-arg, both would be 1.0.0.
The most counterintuitive aspect is how ARG declared before FROM behaves. Such an ARG is only available to RUN instructions before the first FROM instruction (e.g., for setting up build-time tooling or selecting a base image variant). It is not available to subsequent build stages or to ENV instructions in the main build stage unless it’s redeclared.
The next concept you’ll likely grapple with is multi-stage builds and how ARG and ENV interact across different stages.