06 · Building Images¶
Once you have a Dockerfile, docker build turns it into an image. This
module covers the build command, tagging, and reading build output.
The basic build command¶
-t hello-app:1.0tags the resulting image with a repository name (hello-app) and a version tag (1.0). You can pass-tmultiple times to apply several tags to the same build..is the build context: the directory whose contents Docker sends to the daemon and whichCOPY/ADDinstructions can reference. Docker cannotCOPYa file that lives outside the build context.- By default, Docker looks for a file literally named
Dockerfilein the build context; use-f <path>to point at a differently named or located file.
What actually happens during a build¶
- The CLI packages up the build context (the directory tree at
., minus anything excluded by.dockerignore) and sends it to the Docker daemon. - The daemon parses the Dockerfile into a sequence of steps.
- For each instruction, the daemon checks whether a cached layer already exists for that exact instruction plus the layer it builds on (see Level 2's caching module) — if so, it reuses the cache; if not, it executes the instruction in a temporary container and commits the result as a new layer.
- Once every instruction is processed, the final set of layers plus
metadata (default
CMD,EXPOSEd ports, etc.) is assembled into an image and stored locally under the tag(s) you gave it.
Typical output looks like a numbered list of steps:
[+] Building 12.4s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load .dockerignore
=> [internal] load metadata for docker.io/library/python:3.12-slim
=> [1/5] FROM docker.io/library/python:3.12-slim
=> [internal] load build context
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt
=> [5/5] COPY . .
=> exporting to image
=> => naming to docker.io/library/hello-app:1.0
(This output style reflects Docker's BuildKit builder, which has been the
default builder in current Docker versions; older Docker releases show a
simpler Step 1/5 : FROM ... style log instead — the instructions and
layers are the same either way.)
The .dockerignore file¶
Just like .gitignore, a .dockerignore file in the build context
excludes files/directories from being sent to the daemon at all:
This matters for two reasons: it keeps the build context small (faster
builds, especially over a remote daemon), and it prevents accidentally
COPY .-ing secrets, local virtual environments, or dependency
directories that should be reinstalled fresh inside the image rather than
copied from the host.
Tagging images¶
A single build can be tagged multiple times; both tags point at the same image ID. Common conventions:
- A semantic version tag (
1.0,1.2.3) for a specific, reproducible build. latestas a convenience alias for "the most recently built/pushed version" — but remember from module 03 thatlatestis just a tag like any other, not a guarantee of freshness or stability.- A tag matching a git commit SHA (
hello-app:a1b2c3d) for exact traceability from image back to source in CI pipelines.
Verifying and inspecting the result¶
docker images hello-app
# REPOSITORY TAG IMAGE ID CREATED SIZE
# hello-app 1.0 7e2f... 10 seconds ago 135MB
docker run --rm hello-app:1.0
docker history shows the layer-by-layer size breakdown, which is useful
for spotting which instruction bloated the image:
docker history hello-app:1.0
# IMAGE CREATED BY SIZE
# 7e2f... CMD ["python" "app.py"] 0B
# <missing> COPY . . 2.1kB
# <missing> RUN pip install --no-cache-dir -r requiremen… 28.4MB
# <missing> COPY requirements.txt . 31B
# <missing> WORKDIR /app 0B
# <missing> FROM python:3.12-slim 130MB
Common build errors and what they mean¶
| Error | Cause |
|---|---|
COPY failed: file not found |
The file isn't inside the build context, or the path is wrong relative to it |
failed to solve: process ... did not complete successfully |
A RUN command exited non-zero (check the printed command output above the error) |
unable to prepare context: path ... does not exist |
You ran docker build from the wrong directory, or gave the wrong context path |
| Build is unexpectedly huge/slow | Missing a .dockerignore, so large directories like .git or node_modules are being sent and possibly copied |
Worked example: building the Node Dockerfile from module 05¶
Given the Dockerfile from the previous exercise, and a minimal
package.json + server.js in the same directory:
docker build -t node-hello:1.0 .
docker images node-hello
docker run --rm -p 3000:3000 node-hello:1.0
If the build succeeds and docker run starts without error, the image is
correctly structured; if npm install fails inside the build, the error
output will point at that RUN step specifically, since BuildKit reports
which step failed.
How It Actually Works¶
Build cache keys, precisely. BuildKit doesn't decide "reuse this
layer" by comparing your Dockerfile text to a previous run in a fuzzy way
— it computes a cache key per instruction from two inputs: the parent
layer's content hash, and a normalized representation of the instruction
itself (the command string for RUN, or a hash of the actual file
contents being copied for COPY/ADD, not just the filename). If either
input changes, the cache key changes, and every subsequent instruction's
cache key changes too, because each one's key incorporates the previous
layer's hash — this is exactly why reordering COPY requirements.txt .
before COPY . . matters: COPY . changes its cache key on any source
file edit, so if it comes before RUN pip install, that install step's
cache key changes too and reruns, even though the actual dependency
manifest didn't change.
What an image actually is on disk/registry. An image is not one
opaque blob — it's a JSON manifest listing an ordered array of layer
digests (each a sha256:... of a compressed tarball) plus a reference to
a separate image config JSON object (environment variables, CMD,
ENTRYPOINT, exposed ports, architecture). docker build assembles this
manifest+config+layers set locally; docker images's IMAGE ID is the
hash of the config JSON, while each layer keeps its own independent
content hash. docker history is reading that manifest's layer list back
out and matching sizes to instructions — <missing> appears for layers
that came from a pulled base image rather than being built locally in
this session, since Docker doesn't always retain per-instruction metadata
for layers it didn't just build.
Exercise¶
Using the Dockerfile you wrote in module 05 (or the Python one shown
above), run a build tagging the image with two tags at once — a
version number and latest — then use docker images to confirm both
tags point at the same IMAGE ID. Then add a .dockerignore excluding
at least __pycache__ (or node_modules) and rebuild, confirming the
build still succeeds.