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
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 14 min
- Reviewed20 August 2026
Tested on the versions above. Environment values and image IDs vary per build. Match the shape of the output, not the exact strings.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- You can build and run an image from a Dockerfile - guide 14.
- A writable working directory.
-
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.
-
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 appuserExpected 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.
-
Override the build argument
--build-argsupplies 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 appuserExpected 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.
-
Override the environment variable at run time
ENV sets a default that
-ereplaces 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 appuserExpected 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.
-
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 condition
user=appuserconfirms USER took effect. An empty user field means the container runs as root. -
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-pto 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-xExpected result
docker portprints nothing despite EXPOSE being present.Success conditionYou understand EXPOSE as metadata. Publishing is
-p HOST:CONTAINERat run time - see guide 9.
Troubleshooting
An ARG value is empty inside the running container
Why: ARG is build-time only. It does not survive into the image environment unless an ENV instruction copies it.
Fix:Add
ENV NAME=$NAMEafter the ARG, as this guide's Dockerfile does.bash docker image inspect IMAGE --format "{{.Config.Env}}"# if your variable is absent here, ENV never captured itArguments passed to docker run are ignored
Why: With ENTRYPOINT in exec form, anything you pass on the command line replaces CMD and is appended to ENTRYPOINT - it does not replace the whole command.
Fix:Use
--entrypointto replace the entrypoint itself, or design CMD to carry the arguments you expect callers to override.bash docker run --rm --entrypoint sh cg-inst:1 -c "echo replaced"Permission denied writing inside the container after adding USER
Why: Directories created before the USER instruction are owned by root, and the unprivileged user cannot write to them.
Fix:Create and chown the directories in the same RUN that sets them up, before switching user.
bash RUN mkdir -p /app/data && chown -R appuser:appuser /appUSER appuser