CertGrid CertGrid
Concepts·Docker

Dockerfile Instructions in Practice

ARG versus ENV, ENTRYPOINT versus CMD, and why USER matters - shown with one Dockerfile that exercises all of them and the inspect output proving what each instruction actually recorded.

Dockerfiles and Builds Guide 15 of 46 Intermediate

Tested on the versions above. Environment values and image IDs vary per build. Match the shape of the output, not the exact strings.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. One Dockerfile that uses all of them

    Rather than a list of definitions, here is a single file exercising the instructions that cause the most confusion. Each is explained by what it produces, not by what the reference says.

    bash
    cat DockerfileFROM alpine:3.22LABEL org.opencontainers.image.title="cg-demo"ARG APP_VERSION=0.0.0ENV APP_HOME=/app APP_VERSION=$APP_VERSIONWORKDIR $APP_HOMECOPY src/ ./src/RUN adduser -D -u 10001 appuserUSER appuserEXPOSE 8080ENTRYPOINT ["/bin/sh","-c"]CMD ["echo running $APP_VERSION as $(id -un)"]

    Expected resultEleven instructions covering metadata, variables, identity and the default command.

    Success conditionThe file builds. Each following step proves what one instruction did.

  2. ARG is build-time; ENV is runtime

    This is the distinction people get wrong most often. ARG exists only while the image is being built and is not present in the running container. ENV is baked into the image and is visible to the process. The pattern here - ARG with a default, then ENV copying it - is how you let a build parameter survive into runtime.

    bash
    docker run --rm cg-inst:1running 0.0.0 as appuser

    Expected resultThe ARG default, 0.0.0, appearing at runtime because ENV captured it.

    Success conditionYou see 0.0.0. Had the Dockerfile only declared ARG, the variable would be empty here.

  3. Override the build argument

    --build-arg supplies a value at build time. The image is rebuilt with a different baked-in default. Note this is a property of the image, so every container from it sees the new value.

    bash
    docker build --build-arg APP_VERSION=2.5.0 -t cg-inst:2 .docker run --rm cg-inst:2running 2.5.0 as appuser

    Expected result2.5.0 rather than the default.

    Success conditionThe value changed without editing the Dockerfile. Never pass secrets this way - build args are visible in the image history.

  4. Override the environment variable at run time

    ENV sets a default that -e replaces per container, with no rebuild. This is the knob you expose to whoever deploys the image.

    bash
    docker run --rm -e APP_VERSION=9.9.9 cg-inst:1running 9.9.9 as appuser

    Expected result9.9.9, from the same image that printed 0.0.0 a moment ago.

    Success conditionThe same image produced a different value. That is the difference between ARG and ENV in one line.

  5. What the image actually recorded

    Everything those instructions set is stored in the image config, and inspect shows it. This is the ground truth when a container behaves unexpectedly - the config tells you what it was built to do, regardless of what the Dockerfile in your editor currently says.

    bash Example session
    docker image inspect cg-inst:1 --format "user={{.Config.User}} env={{.Config.Env}} exposed={{.Config.ExposedPorts}} labels={{.Config.Labels}}"user=appuser env=[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin APP_HOME=/app APP_VERSION=0.0.0] exposed=map[8080/tcp:{}] labels=map[org.opencontainers.image.title:cg-demo]

    Expected resultThe user, the environment, the exposed port and the label, all recorded in the image.

    Success conditionuser=appuser confirms USER took effect. An empty user field means the container runs as root.

  6. USER, and why EXPOSE publishes nothing

    Two commonly misunderstood instructions. USER changes the account every later instruction and the eventual process runs as - the output above proves the process is appuser, not root, which is the single cheapest security improvement available. EXPOSE is documentation only: it records an intended port in the metadata and does not open anything. You still need -p to reach it.

    bash
    # EXPOSE 8080 does NOT publish the portdocker run -d --name cg-x cg-inst:1 && docker port cg-x# no output - nothing is publisheddocker rm -f cg-x

    Expected resultdocker port prints nothing despite EXPOSE being present.

    Success conditionYou understand EXPOSE as metadata. Publishing is -p HOST:CONTAINER at run time - see guide 9.

Troubleshooting

Official sources