docker build

docker build creates a Docker image from a Dockerfile and a build context. It matters because it turns your application files, dependency setup, and runtime defaults into a repeatable image that can be run, tagged, tested, and pushed to a registry.

Building is separate from running. A build produces read-only image layers and image metadata; a later docker run creates a container with a thin writable layer and starts the configured process.

Overview: How docker build Works

The basic command is usually docker build -t name:tag .. The final dot is important: it is the build context path, not punctuation. The context is the set of files Docker is allowed to send to the builder and make available to Dockerfile instructions such as COPY and ADD. Files excluded by .dockerignore are left out before the context is sent.

Modern Docker uses BuildKit by default in current Docker Desktop and Docker Engine setups. The Docker CLI sends the build request to the Docker daemon or builder, the builder reads the Dockerfile, resolves the FROM image, pulls missing base-image layers, and executes each Dockerfile instruction in order. The result is an image made of read-only filesystem layers plus a configuration document containing metadata such as CMD, ENTRYPOINT, ENV, working directory, exposed ports, labels, and default user.

Each filesystem-changing instruction can create a layer. A RUN instruction starts a temporary build container from the current image state, runs the command, then stores the changed filesystem as a new read-only layer. A COPY instruction stores the copied files in another layer. When you later run the image, Docker stacks those layers with a thin writable container layer on top. Deleting a container does not delete the image, and rebuilding an image does not automatically update already-created containers.

The build cache is the feature that makes repeated builds fast. For each instruction, BuildKit decides whether a previous result can be reused. The cache key includes the previous image state, the instruction text, and for file-copying instructions, the relevant file contents and metadata. If one instruction changes, Docker must rebuild that instruction and every instruction after it. This is why Dockerfiles should copy dependency manifests first, install dependencies, and only then copy frequently changing source code.

The tag created by -t is a human-friendly name pointing to the resulting image ID. A single image can have multiple tags, such as my-api:1.0 and my-api:stable. Tags do not make the build more reproducible by themselves; reproducibility starts with a clear Dockerfile, a small build context, pinned base image tags such as node:20-alpine, and lockfiles for language dependencies.

Syntax

The common command form is:

docker build [OPTIONS] PATH | URL | -
Part Meaning
PATH A local directory used as the build context, most often ..
URL A remote Git repository or supported remote context. Docker fetches it and builds from that context.
- Read a Dockerfile or context from standard input, useful for advanced scripting but less common for beginners.

Useful options include:

Option Purpose
-t, --tag Name and optionally tag the image, such as myapp:1.0.
-f, --file Use a Dockerfile at a path other than Dockerfile.
--build-arg Set a build-time variable consumed by ARG. Do not use it for secrets.
--no-cache Ignore the build cache and rebuild every instruction.
--pull Always attempt to pull a newer version of referenced base images.
--target Build only up to a named stage in a multi-stage Dockerfile.
--platform Request a target platform, such as linux/amd64 or linux/arm64, when the builder supports it.
--label Add image metadata labels at build time.

Examples

Example 1: Build a simple nginx image

Assume the current directory contains a Dockerfile and an index.html. The Dockerfile uses a pinned nginx base image and copies the page into nginx’s default web root:

FROM nginx:1.27-alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

Build the image and give it a tag:

docker build -t static-site:1.0 .

Output:

[+] Building 2.3s (7/7) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load metadata for docker.io/library/nginx:1.27-alpine
 => [1/2] FROM docker.io/library/nginx:1.27-alpine
 => [2/2] COPY index.html /usr/share/nginx/html/index.html
 => exporting to image
 => naming to docker.io/library/static-site:1.0

The build created an image named static-site:1.0. EXPOSE 80 is only metadata documenting that nginx listens on port 80 inside the container. It does not publish anything to the host; a later run would need docker run -p 8080:80 static-site:1.0.

Example 2: Build with a Dockerfile in another directory

Sometimes the Dockerfile is not at the root of the project. The -f option chooses the Dockerfile, while the final argument still chooses the build context:

docker build -f docker/Dockerfile -t inventory-api:1.0 .

Output:

[+] Building 4.8s (10/10) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [internal] load build context
 => exporting to image
 => naming to docker.io/library/inventory-api:1.0

This command means: use docker/Dockerfile as the recipe, but send the current directory as the context. A COPY package.json ./ instruction inside that Dockerfile still reads from the context root, not from the docker/ directory. This distinction prevents many confusing missing-file errors.

Example 3: Build a cache-friendly Node.js image

This Dockerfile is arranged so dependency installation can stay cached when only application source changes:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Build it normally:

docker build -t cache-demo-api:1.0 .

Output after a later rebuild where only server.js changed may look like this:

[+] Building 1.1s (9/9) FINISHED
 => CACHED [2/6] WORKDIR /app
 => CACHED [3/6] COPY package*.json ./
 => CACHED [4/6] RUN npm ci --omit=dev
 => [5/6] COPY server.js ./
 => [6/6] USER node
 => exporting to image

Because the package files did not change, Docker reused the dependency layer. If the Dockerfile had used COPY . . before RUN npm ci, a small edit to server.js could force a slow dependency install again.

Example 4: Build only one stage

With a multi-stage Dockerfile, --target lets you stop at a named stage. This is useful for testing a build environment without producing the final runtime image:

FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN go build -o /out/server ./cmd/server

FROM alpine:3.20
WORKDIR /app
COPY --from=builder /out/server /app/server
USER 10001
CMD ["/app/server"]
docker build --target builder -t server-builder:1.0 .

Output:

[+] Building 8.6s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => [1/6] FROM docker.io/library/golang:1.23-alpine
 => [builder 6/6] RUN go build -o /out/server ./cmd/server
 => exporting to image
 => naming to docker.io/library/server-builder:1.0

The image is tagged at the builder stage, so it contains the Go toolchain and build files. A normal build without --target would continue into the final Alpine stage and copy only the compiled binary.

How It Works Step By Step

  1. The Docker CLI parses options such as -t, -f, --target, and the context path.
  2. Docker reads .dockerignore and prepares the build context. Large ignored directories such as node_modules, .git, logs, and local build output should not be sent.
  3. The builder loads the Dockerfile and resolves the base image named by FROM. Missing layers are pulled from a registry.
  4. For each instruction, BuildKit checks its cache. A cache hit reuses an existing layer or metadata result. A cache miss rebuilds that instruction and all later instructions that depend on it.
  5. RUN instructions execute in temporary build containers. Their filesystem changes are captured into image layers; the temporary build containers are not your final application containers.
  6. COPY and ADD read only from the build context unless you use specific advanced features. They cannot reach arbitrary files elsewhere on your host.
  7. When the last requested stage finishes, Docker writes the image configuration, creates or updates any requested tags, and stores the image in the local image store.

Common Mistakes

Forgetting the build context

This command fails when run from a directory that does not contain the files expected by the Dockerfile:

docker build -f docker/Dockerfile -t broken-api:1.0 docker

The problem is that the context is docker, so COPY package.json ./ can only see files under docker/. If the project files live at the repository root, use the repository root as the context:

docker build -f docker/Dockerfile -t fixed-api:1.0 .

Sending a huge context

A build context containing dependencies, Git history, logs, and local artifacts slows every build and can accidentally copy private files. Add a .dockerignore like this:

node_modules
.git
*.log
dist
coverage
.env

This file is not a security boundary by itself, but it is an important build hygiene tool. Keep secrets out of the project directory and out of image layers.

Using latest and assuming –pull fixes reproducibility

This builds, but it is not stable over time:

FROM node:latest
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
CMD ["node", "server.js"]

--pull can make Docker check for a newer base image, but if the base is latest, the meaning of the build can change from one day to the next. Use a specific tag such as node:20-alpine, and for high-control production systems consider digest pinning in addition to tags.

Putting secrets in build arguments

This is unsafe with real credentials:

docker build --build-arg "API_TOKEN=<YOUR_API_TOKEN>" -t secret-demo:1.0 .

Build arguments can appear in build metadata, logs, cache records, or image history depending on how they are used. Use BuildKit secret mounts or your CI and deployment platform’s secret store instead of ordinary ARG, ENV, RUN echo, COPY, or ADD for secret material.

Best Practices

  • Always notice the final build context argument. In docker build -t app:1.0 ., the dot controls what files Docker can see.
  • Use clear tags such as my-api:1.0, my-api:dev, or a commit-based tag in CI.
  • Pin base image tags instead of relying on latest. Tags such as node:20-alpine are easier to reason about.
  • Use .dockerignore early. It improves speed and reduces accidental file exposure.
  • Order Dockerfile instructions from least-changing to most-changing to preserve cache hits.
  • Copy dependency manifests before source code, then run dependency installation, then copy the rest of the app.
  • Use --no-cache only when you intentionally want to verify a clean rebuild; it makes builds slower.
  • Use --pull in CI when you want regular base-image updates, but pair it with deliberate version choices.
  • Use --target for debugging or testing intermediate stages in multi-stage builds.
  • Remember that docker build creates an image. It does not start your app, publish ports, or create persistent volumes.

Practice Exercises

  1. Create a directory with index.html and a Dockerfile based on nginx:1.27-alpine. Build it as practice-site:1.0, then decide what separate docker run command would publish it on host port 8080.
  2. Move a Dockerfile into a docker/ subdirectory while keeping application files at the project root. Practice using -f docker/Dockerfile with the correct context path.
  3. Take a Node.js Dockerfile that uses COPY . . before npm ci. Reorder it so dependency installation stays cached when only source files change, then compare the build output before and after editing one source file.

Summary

  • docker build turns a Dockerfile and build context into a local image.
  • The build context controls what files COPY and ADD can access.
  • -t tags the image; -f selects the Dockerfile; --target stops at a named build stage.
  • BuildKit reuses cached layers when the instruction and its inputs have not changed.
  • Instruction order is critical: slow, stable dependency steps should come before fast-changing source copies.
  • .dockerignore keeps builds faster and helps avoid sending irrelevant or private files.
  • Building an image does not run a container or publish a port; those happen later with docker run or docker compose.