Skip to content

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.

docker build reads a Dockerfile and produces an image. The basic syntax is:

Terminal window
docker build [OPTIONS] PATH

The 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.

Terminal window
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.

The -t flag assigns a human-readable name:tag to the image:

Terminal window
docker build -t myapp:1.0 .

The full form of a reference is:

[registry/][owner/]name[:tag][@digest]

Examples:

myapp:1.0
myapp:latest
ghcr.io/acme/myapp:1.0
docker.io/library/node:24-alpine
alpine@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6

If you omit the tag part, Docker defaults to :latest.

Terminal window
docker images

Real 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 EXTRA
docker-b1-bt:1.0 a225065a711d 28.7MB 0B
docker-b1-bt:latest a225065a711d 28.7MB 0B

Both 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).

docker tag adds a new name or tag to an existing image without copying it. Both names point to the same image ID.

Terminal window
# Promote a build image to a release name
docker tag myapp:1.0 myapp:stable
docker tag myapp:1.0 ghcr.io/acme/myapp:1.0

Proven: 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.

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 3cd9c70ed14e
2.0 9adad49089a1
1.0 a225065a711d
latest a225065a711d # still the first build

latest 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.

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-digest
by-digest

Locally built images have no digest until they are pushed to a registry.

Dockerfile.multi
# @build
FROM alpine:3.24 AS base
ARG GREETING=hi
RUN echo "$GREETING from base" > /msg
FROM base AS test
RUN echo tested >> /msg
FROM base AS release
CMD ["cat", "/msg"]
$ docker build -f Dockerfile.multi --target release --build-arg GREETING=hello -t multi:rel .
$ docker run --rm multi:rel
hello from base # --build-arg overrode the ARG default
$ docker build -f Dockerfile.multi --target test -t multi:test .
$ docker run --rm multi:test cat /msg
hi from base
tested # the `test` stage has the extra line; release does not
  • -f <file> selects a Dockerfile other than ./Dockerfile (the context is still the PATH).
  • --target <stage> stops at that stage of a multi-stage Dockerfile.
  • --build-arg NAME=value sets an ARG for 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 done
.dockerignore
node_modules/
.git/
.env
*.log
dist/
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.

Terminal window
# Build with a version tag
docker build -t myapp:1.0 .
# Also tag as latest
docker tag myapp:1.0 myapp:latest
# Inspect the result
docker images myapp

Expected output: two rows with the same image ID, as shown above.

# 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.0

An AI-written .dockerignore “to keep the context small”, for a Dockerfile that does COPY src/app.js ./app.js:

🐛 Spot the bug

.dockerignore
**/*.js
node_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 found
ERROR: failed to build: failed to solve: failed to compute cache key: … "/src/app.js": not found

Two 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:

.dockerignore
node_modules/
.git/
.env
**/*.test.js
dist/

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.

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.

OptionBenefitCost
alpine base imageSmall image, pulls fastUses musl libc, so some native binaries or npm packages can fail to compile or break at runtime
debian/full base imageFull set of tools, high glibc compatibilityMuch larger image, slower to push/pull
Tag by version (1.4.2)Readable, easy rollbackA tag can be re-pushed and change
Pin by digest (@sha256:…)Immutable, reproducibleUnreadable; you must update it deliberately
Broad .dockerignoreSmall context (20.01 MB → 152 B), no leaked filesAn over-broad pattern breaks COPY
  • 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 build without -t at all, ending up with an unnamed image you have to chase down by ID later.
  • Assuming latest follows your newest build.
  • Writing *.ext in .dockerignore and 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.

  • “docker tag copies the image.” It adds a name; both rows show ID a225065a711d and use the same disk space.
  • “latest is the newest image.” It stayed on the first build until re-tagged.
  • “.dockerignore only speeds things up.” It also changes what COPY . . puts in the image (.env and node_modules vanished from /app).
  • “Build context = the Dockerfile’s directory.” The context is the PATH argument; -f can point elsewhere.
What is the build context in docker build .?
What does docker tag myapp:1.0 myapp:stable do?
What is the main purpose of a .dockerignore file?
Which statement about the latest tag is true?
docker build . with no -t finishes successfully. What do you have?
Your Dockerfile has COPY src/app.js ./app.js; the file exists but the build says "/src/app.js": not found. What do you check first?