A site on nginx:alpine
CMD is inherited from the base: nginx is already set to run in the
foreground. It serves files from /usr/share/nginx/html.
A Dockerfile is an image recipe: base, files, build and start commands; docker build turns it into an image.
sudo docker build -t app:1.1 .
build from ./Dockerfile
sudo docker build -t app:1.1 -f Dockerfile.dev .
another file
sudo docker build --no-cache -t app:1.1 .
everything, no cache
sudo docker history app:1.1
image layers
sudo docker image inspect -f '{{json .Config.Cmd}}' app:1.1
sudo docker run --rm app:1.1 ls /www
what got into the image
A Dockerfile is a text file that docker build
turns into an image. Every line is an instruction:
take a base, copy files, run a command, set the start command.
Instructions run top to bottom, and each one that changes files produces
a layer.
-t app:1.1 gives the image a name and a tag; the dot
at the end is the directory to take files from. The build lines are
numbered #1, #2…: first the service steps [internal], then the
instructions in order — [2/4] means the second of four. The last line
of a step tells how it ended: DONE with the time, or CACHED. CMD,
EXPOSE and ENV get no steps of their own: they record image settings.
On the lab machine BuildKit runs the build (the buildx component, the
docker-buildx package).
| Instruction | What it does | Layer with files |
|---|---|---|
FROM busybox |
the base image to build on | the base's layers |
WORKDIR /www |
working directory from here on and at run time; creates it | yes |
COPY index.html . |
a file from the build context into the image | yes |
RUN adduser -D web |
run a command at build time | yes |
ENV MODE=prod |
an environment variable at build and run time | no |
EXPOSE 80 |
a note "the service listens on 80"; publishes nothing | no |
USER web |
who runs the RUNs below and the main process |
no |
VOLUME /data |
a directory for a volume: without -v it gets a new anonymous volume |
no |
CMD ["httpd", "-f"] |
the default start command | no |
ENTRYPOINT ["echo"] |
the fixed part of the start command | no |
FROM comes first. Docker looks for the base among the images on the
machine and goes to a registry if it is missing. The lab machine has no
network, so FROM can only use loaded images: alpine, nginx:alpine,
busybox.
RUN runs at build time, CMD every time a container starts.
RUN apk add … fails without network: that command fetches packages from
the internet.
EXPOSE opens nothing: docker run -p publishes a host port
(Ports). An ENV from the image can be overridden at run
time: docker run -e MODE=dev. VOLUME /data in the image means every
docker run without -v mounts a new empty volume at /data — see
Volumes.
The build context is the directory docker build
sends to the Docker service in full: the last argument, usually a dot.
The [internal] load build context step says how much was sent:
transferring context: 31B done.
COPY takes files only from the context. A COPY path starts at its
root and cannot go above it: COPY ../secret.txt . looks for
/secret.txt inside the context and fails:
If the file you need is outside, copy it into the build directory or
build from the directory above: docker build -f app/Dockerfile ..
.dockerignore next to the Dockerfile keeps things out of the context —
one pattern per line:
That keeps logs and secrets out of the image and stops the build from shipping gigabytes when the directory holds data.
A rebuild with no changes redoes nothing: every step comes from the build cache.
A step comes from the cache if the instruction, the layer below it and —
for COPY — the content of the copied files are unchanged. Change one
step and it and every step below are rebuilt. After editing
index.html only WORKDIR comes from the cache; COPY and the steps
below it run again.
Hence the ordering rule: what rarely changes (base, user, dependency
install) goes up; application files you edit often go down. --no-cache
rebuilds everything.
The cache compares RUN by the command text only. RUN date > /built
keeps the date of the first build in the cache.
CMD comes in two forms:
| Form | Example | What runs |
|---|---|---|
| exec | CMD ["httpd", "-f", "-p", "80"] |
the program itself, PID 1 |
| shell | CMD httpd -f -p 80 |
/bin/sh -c "httpd -f -p 80" |
In exec form every word is a separate quoted string. One string for the
whole command — CMD ["httpd -f -p 80"] — means "run the program named
httpd -f -p 80". The build passes, the container does not start:
exec: "httpd -f -p 80": executable file not found in $PATH.
In shell form the main process is a shell, and variables in the line are
expanded. Busybox sh with a single command replaces itself with it:
docker top shows only httpd.
Words after the image name in docker run replace CMD entirely:
docker run --rm app:1.1 ls /www. If the image has an ENTRYPOINT,
those words are appended to it:
The main process must run in the foreground: the container lives as long
as it does (Main process). httpd without -f
goes to the background, and the container exits at once with
Exited (0).
CMD is inherited from the base: nginx is already set to run in the
foreground. It serves files from /usr/share/nginx/html.
adduser -D is the busybox version: a user with no password. Files from
COPY belong to root wherever USER stands: the process can read them
but not write to them. To hand files to the user —
COPY --chown=web:web data/ /data/.
A new image does not reach a running container by itself — the container has to be re-created.
The old builder runs the build: there is no buildx component (the
docker-buildx package), or the DOCKER_BUILDKIT=0 variable turns it
off. It is a warning; the build goes on. The error, if any, is in the
last lines.
The FROM image is not on the machine, so Docker went to the registry.
Check the name and tag against docker images: FROM busybox means
busybox:latest. Without network, load the base from a file:
docker load -i /opt/images/busybox.tar.
The COPY path goes above the context: ../file. BuildKit counts it from
the context root and answers "/file": not found; the old builder says
COPY failed: forbidden path outside the build context. Put the file into
the build directory — see Build context.
The old builder says COPY failed: file not found in build context.
The file is not in the context: a typo in the name, the build started
from the wrong directory, or .dockerignore excludes the file. Check
ls in the directory given as the last docker build argument.
failed to read dockerfile: open Dockerfile: no such file or directory
(the old builder says unable to prepare context) — the context
directory has no Dockerfile. Go to the application directory or name the file with
-f. path "…" not found means the context directory itself is
missing.
The image built and the container was created but did not start (status
Created): the image has no program by that name. Most often it is an
exec-form CMD written as one string — split the words:
["httpd", "-f", "-p", "80"]. Or the base image lacks the program:
busybox has no bash and no python.
The start command finished or went to the background. Check
docker image inspect -f '{{json .Config.Cmd}}': a service must run in
the foreground (httpd -f, nginx -g 'daemon off;').
Check three things in order. Was the image rebuilt after the edit? Was
the container created from the new image (docker inspect -f
'{{.Image}}' against the image ID)? Is the edited file the one COPY
copies, and not in .dockerignore? The last one shows in the COPY
step of the build output: CACHED after an edit means a different
file is being copied.
COPY . . without .dockerignore carries .env, keys and .git into
the image. A later RUN rm .env does not help: the file stays in the
COPY layer, and anyone with the image can pull it out. Exclude secrets
in .dockerignore and pass them at run time: -e, --env-file.
FROM nginx means nginx:latest, whatever image sits under that name on
build day. Two builds of one Dockerfile give different images. Pin a
version: FROM nginx:1.27-alpine.
docker load does.ENV.USER.Found an inaccuracy? Report an error in the article