Build, Tag & the Build Context
Measured on Docker Desktop 4.81.0 / Engine 29.6.1, Buildx v0.35.0-desktop.2, BuildKit v0.31.1 (macOS, linux/arm64), 2026-09-30. Image IDs and sizes are from that run and will differ on your machine.
The docker build command
Section titled “The docker build command”docker build reads a Dockerfile and produces an image. The basic syntax is:
docker build [OPTIONS] PATHThe PATH argument (usually .) is called the build context — it is the directory that Docker sends to the builder. Files in the build context are available to COPY instructions inside the Dockerfile. Files outside the build context are not accessible.
docker build .This sends the current directory as the build context. Without -t, the resulting image has no name. Proven: docker build -q . printed only sha256:a225065a…, and the image shows up with <none> as repository and tag (a dangling image, listed by docker image ls --filter dangling=true). You will almost always add -t.
Tagging with -t
Section titled “Tagging with -t”The -t flag assigns a human-readable name:tag to the image:
docker build -t myapp:1.0 .The full form of a reference is:
[registry/][owner/]name[:tag][@digest]Examples:
myapp:1.0myapp:latestghcr.io/acme/myapp:1.0docker.io/library/node:24-alpinealpine@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6If you omit the tag part, Docker defaults to :latest.
Listing images with docker images
Section titled “Listing images with docker images”docker imagesReal output for the image built above after tagging it twice (Engine 29’s table adds DISK USAGE and CONTENT SIZE columns; docker image ls --format gives stable scripting output):
IMAGE ID DISK USAGE CONTENT SIZE EXTRAdocker-b1-bt:1.0 a225065a711d 28.7MB 0Bdocker-b1-bt:latest a225065a711d 28.7MB 0BBoth names show the same image ID. The DISK USAGE includes the 20 MB of node_modules this demo copied into the image (see the .dockerignore section).
Retagging with docker tag
Section titled “Retagging with docker tag”docker tag adds a new name or tag to an existing image without copying it. Both names point to the same image ID.
# Promote a build image to a release namedocker tag myapp:1.0 myapp:stabledocker tag myapp:1.0 ghcr.io/acme/myapp:1.0Proven: docker tag $ID docker-b1-bt:1.0 and docker tag $ID docker-b1-bt:latest produced two rows with the identical ID a225065a711d and no extra disk use.
The meaning of latest
Section titled “The meaning of latest”latest is not a magic “newest” tag. It is just the default string Docker uses when you omit the tag. Nothing moves it for you. Proven: after building 1.0 (also tagged latest), then building a changed 2.0 and 3.0 with only -t …:2.0/:3.0:
$ docker image ls docker-b1-bt --format '{{.Tag}} {{.ID}}'3.0 3cd9c70ed14e2.0 9adad49089a11.0 a225065a711dlatest a225065a711d # still the first buildlatest still points at the oldest image. If you push myapp:1.0 and myapp:latest independently, they can point to completely different image layers. In production pipelines, always use an explicit, meaningful tag (version number, git SHA, date) so you know exactly what is deployed; for byte-exact pinning use the digest.
Digest versus tag
Section titled “Digest versus tag”A tag can be moved; a digest (@sha256:…) identifies content. docker buildx imagetools inspect alpine:3.24 printed Digest: sha256:294b683c…, and docker image ls --digests alpine showed the same value for the 3 and 3.24 tags (two names, one image). Running by digest works and cannot drift:
$ docker run --rm alpine@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 echo by-digestby-digestLocally built images have no digest until they are pushed to a registry.
More docker build options
Section titled “More docker build options”# @buildFROM alpine:3.24 AS baseARG GREETING=hiRUN echo "$GREETING from base" > /msg
FROM base AS testRUN echo tested >> /msg
FROM base AS releaseCMD ["cat", "/msg"]$ docker build -f Dockerfile.multi --target release --build-arg GREETING=hello -t multi:rel .$ docker run --rm multi:relhello from base # --build-arg overrode the ARG default$ docker build -f Dockerfile.multi --target test -t multi:test .$ docker run --rm multi:test cat /msghi from basetested # the `test` stage has the extra line; release does not-f <file>selects a Dockerfile other than./Dockerfile(the context is still thePATH).--target <stage>stops at that stage of a multi-stage Dockerfile.--build-arg NAME=valuesets anARGfor this build.
Speeding up the context with .dockerignore
Section titled “Speeding up the context with .dockerignore”By default, docker build . sends your entire current directory to the builder — including node_modules/, .git/, test fixtures, and local secrets. A .dockerignore file excludes paths from the build context, which:
- Speeds up the build (less data to transfer)
- Prevents accidentally
COPY-ing secrets into your image
Measured on a directory with 20 MB in node_modules/, a .env, and a small src/app.js, built with COPY . .:
# no .dockerignore#6 [internal] load build context#6 transferring context: 20.01MB 0.2s done# with the .dockerignore below#5 transferring context: 152B donenode_modules/.git/.env*.logdist/coverage/The context dropped from 20.01 MB to 152 bytes (the extra #3 transferring context: 71B line is BuildKit reading the .dockerignore file itself). And the image content changed: ls -a /app in the built image listed .env, Dockerfile, node_modules, src without the ignore file and only .dockerignore, Dockerfile, src with it.
Syntax is close to .gitignore but not identical: patterns are matched relative to the context root, *.js matches only root-level files (see the bug below), and **/*.js matches at any depth. The reference is .dockerignore file.
Putting it together
Section titled “Putting it together”# Build with a version tagdocker build -t myapp:1.0 .
# Also tag as latestdocker tag myapp:1.0 myapp:latest
# Inspect the resultdocker images myappExpected output: two rows with the same image ID, as shown above.
Hands-on practice
Section titled “Hands-on practice”# syntax=docker/dockerfile:1
FROM alpine:3.24
LABEL maintainer="[email protected]"
WORKDIR /app
RUN echo "v1.0" > version.txt
CMD ["cat", "version.txt"]
# --- build, tag, and inspect ---
# docker build -t myapp:1.0 .
# docker tag myapp:1.0 myapp:latest
# docker images myapp
# docker run --rm myapp:1.0Reading AI-generated code
Section titled “Reading AI-generated code”An AI-written .dockerignore “to keep the context small”, for a Dockerfile that does COPY src/app.js ./app.js:
🐛 Spot the bug
**/*.jsnode_modules/Show the answer
The pattern removes every JavaScript file from the context, including the ones the Dockerfile needs. The build fails at the COPY, with an error that mentions the file, not the ignore file:
#7 ERROR: failed to calculate checksum of ref …: "/src/app.js": not foundERROR: failed to build: failed to solve: failed to compute cache key: … "/src/app.js": not foundTwo lessons. First, when COPY says “not found” for a file that exists on disk, read .dockerignore before anything else. Second, *.js would not have caused this: with the shorter pattern the same build succeeded, because * does not cross directories (it only matched ./app.js-style files at the root). The AI’s **/*.js is what matches src/app.js. Ignore what should not ship (tests, build output), not source the image needs:
node_modules/.git/.env**/*.test.jsdist/The opposite mistake is worse because it is silent: no .dockerignore plus COPY . . copied a .env file (SECRET=<your-api-key>) into the image, and docker run … cat /app/.env printed it. Ignore .env files; pass secrets at run time.
Mental Model
Section titled “Mental Model”docker build is a two-step handshake: the CLI packs the context (minus .dockerignore) and sends it to the builder; the builder then runs the Dockerfile against only that context. A tag is a movable label on an image ID — many labels, one image; a digest is the image’s content hash and cannot move. latest is just one more label that you have to move yourself.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
alpine base image | Small image, pulls fast | Uses musl libc, so some native binaries or npm packages can fail to compile or break at runtime |
debian/full base image | Full set of tools, high glibc compatibility | Much larger image, slower to push/pull |
Tag by version (1.4.2) | Readable, easy rollback | A tag can be re-pushed and change |
Pin by digest (@sha256:…) | Immutable, reproducible | Unreadable; you must update it deliberately |
Broad .dockerignore | Small context (20.01 MB → 152 B), no leaked files | An over-broad pattern breaks COPY |
Common mistakes
Section titled “Common mistakes”- Forgetting to pin an image tag (leaving it as
latest, or omitting a tag entirely), so today’s build and tomorrow’s produce different images. - Having no
.dockerignore, bloating the build context, slowing down builds, and risking secrets leaking into the image. - Running
docker buildwithout-tat all, ending up with an unnamed image you have to chase down by ID later. - Assuming
latestfollows your newest build. - Writing
*.extin.dockerignoreand expecting it to match in subdirectories.
Official images such as node and postgres publish many tags per release (version, -alpine, -slim) as multi-platform images; see the tag lists on Docker Hub and pin the one that fits.
Common Misconceptions
Section titled “Common Misconceptions”- “
docker tagcopies the image.” It adds a name; both rows show IDa225065a711dand use the same disk space. - “
latestis the newest image.” It stayed on the first build until re-tagged. - “
.dockerignoreonly speeds things up.” It also changes whatCOPY . .puts in the image (.envandnode_modulesvanished from/app). - “Build context = the Dockerfile’s directory.” The context is the
PATHargument;-fcan point elsewhere.