Back to Blog
Guide

Docker Build Explained: Dockerfile, Layers, BuildKit

Recep Erdoğan12 min read
Docker Build Explained: Dockerfile, Layers, BuildKit

Every container image starts with docker build. You write a Dockerfile, run one command, and Docker turns your source into a portable image that behaves the same on your laptop, a teammate's machine, and a production server. Knowing how docker build works is the difference between images that rebuild in a few seconds from cache and images that recompile everything on every commit.

This guide follows the full path from Dockerfile instructions to image layers. It covers the anatomy of the build command, the build context, caching, multi-stage builds, and the BuildKit engine behind docker buildx. At the end we look at why production platforms run the build for you, so you get reproducible images without maintaining a builder.

The short answer: docker build reads a Dockerfile and executes its instructions in order, turning each into a cached, read-only layer that stacks into a single container image. You run it as docker build -t name:tag ., where the final dot is the build context. Since Docker 23 the default engine is BuildKit, which parallelizes steps and caches aggressively.

What docker build actually does

A build is a translation. Each instruction in the Dockerfile (FROM, COPY, RUN, ENV, CMD, and the rest) becomes one build step. Most of those steps produce a new filesystem layer stacked on top of the previous one. The finished stack of layers, together with configuration metadata like the default command and exposed ports, is the image.

docker build is the command that reads a Dockerfile, runs each instruction as a build step, and produces a container image made of stacked read-only layers. The finished image can run as a container locally or push to a registry for others to pull.

Layers are content addressed, which means Docker fingerprints the contents of each one. Two images that share an identical base and identical dependency layers store those bytes only once. This is why a small code change rebuilds only the top of the stack while the heavy base and dependency layers stay untouched. The official Docker build documentation describes each instruction and how it maps to a layer.

The anatomy of the docker build command

The canonical form is short and worth reading slowly:

bash
docker build -t myapp:1.0.0 .

Three pieces matter here. docker build invokes the builder. The -t flag tags the resulting image with a name and version (myapp:1.0.0), so you can reference it later instead of a random image ID. The trailing dot is the build context, the directory whose files the builder is allowed to read.

When the build finishes, Docker prints the image ID and stores the image in your local image store. You then run it with docker run myapp:1.0.0 or push it to a registry with docker push. If you are deciding where that image should ultimately live, our guide on hosting a Docker container walks through the options.

The build context and .dockerignore

The build context is the set of files packaged and sent to the builder before any instruction runs. With docker build ... ., the context is the current directory and everything beneath it. Any COPY or ADD instruction can only reference paths inside that context, which is a common source of confusion for newcomers who try to copy a file from a parent folder.

A bloated context slows every build, because the whole directory is scanned and transferred first. A repository that ships node_modules, .git, build output, and local logs into the context wastes seconds on each run. The fix is a .dockerignore file at the project root:

.git
node_modules
dist
*.log
.env

This keeps large and sensitive paths out of the context entirely. Excluding .env also stops secrets from leaking into layers by accident. For a proper way to handle credentials at build time, see how to manage Docker build secrets with BuildKit mounts.

Layer caching and instruction order

Caching is where builds get fast or stay slow. Docker caches each layer and reuses it on the next build as long as the instruction and its inputs are unchanged. The moment one layer changes, that layer and every layer after it are rebuilt. The order of your instructions therefore decides how much of the cache survives an edit.

The rule is to place stable instructions early and volatile ones late. Dependencies change far less often than application code, so copy your manifests and install packages before you copy the source:

dockerfile
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

With this order, editing a source file invalidates only the final COPY and the steps below it. The npm ci layer, often the slowest step in the build, stays cached. Reverse the two COPY lines and every code change reinstalls all dependencies from scratch.

Multi-stage builds to shrink images

Compilers, bundlers, and dev dependencies are needed to build an app but not to run it. Shipping them in the final image bloats size, slows pulls, and widens the attack surface. Multi-stage builds solve this cleanly.

A multi-stage build uses several FROM instructions in one Dockerfile. Early stages compile or bundle your app, and a final lean stage copies only the built artifacts, so build tools and source never ship in the runtime image.

Here is a realistic multi-stage Dockerfile for a Node service. The first stage installs everything and builds. The second stage starts from a clean base and copies only the production dependencies and the compiled output.

dockerfile
# syntax=docker/dockerfile:1

# Stage 1: install and build
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: minimal runtime image
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]

You build it exactly like a single-stage file:

bash
docker build -t myapp:1.0.0 .

The --from=build reference is the key. It reaches back into the first stage and copies just the dist directory, leaving source files, dev dependencies, and the build toolchain behind. The final image contains a production runtime and compiled code, nothing more.

BuildKit, the modern builder

BuildKit is the engine that runs behind docker build on any current Docker install. It replaced the legacy builder and brings real gains in speed and capability.

BuildKit is Docker's modern build engine, the default since Docker 23. It models the build as a graph, runs independent stages in parallel, skips layers no output needs, and handles caching, secrets, and cache mounts far better than the legacy builder.

Because BuildKit understands the dependency graph between stages, two independent stages can build at the same time instead of one after the other. It also mounts secrets into a single RUN step without baking them into a layer, and it supports cache mounts that persist a package cache across builds. The first line of the Dockerfile above, # syntax=docker/dockerfile:1, opts into the current frontend so these features are available. The BuildKit documentation covers the full feature set.

docker buildx and multi-platform builds

docker buildx is the extended build command that drives BuildKit directly. Its headline use is building one image that runs on more than one CPU architecture, most often amd64 for typical servers and arm64 for Apple Silicon laptops and Arm instances.

A single command produces both variants and pushes a combined image to a registry:

bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/myapp:1.0.0 \
  --push .

Under the hood, BuildKit builds each platform, using emulation when your machine is not native to a target architecture, and publishes a manifest list. Consumers pulling that tag automatically receive the image for their own architecture. The guide on multi-platform builds explains builder setup and the emulation caveats in detail.

Common docker build flags

A handful of flags cover most day-to-day builds:

  • -t name:tag sets the image name and tag. Repeat it to apply several tags at once.
  • -f path/to/Dockerfile points at a Dockerfile that is not named Dockerfile or lives outside the context root.
  • --build-arg KEY=value passes a value into an ARG declared in the Dockerfile, useful for version pins or non-secret configuration.
  • --target stagename stops the build at a named stage, handy for building just the build stage in CI to run tests.
  • --no-cache forces a full rebuild that ignores every cached layer.
  • --pull always fetches the newest base image instead of reusing a local copy.
  • --platform linux/arm64 sets the target architecture for a single-platform build.

Why you rarely run docker build by hand in production

Running docker build on your own machine is how you learn and how you debug. Running it by hand for every production release is where teams get into trouble. Local builds depend on your Docker version, your cache state, and your architecture, so the image that ships can differ from the one a colleague built. Maintaining a shared, warm, multi-platform builder is real work.

A PaaS removes that work. You push code to a repository, and the platform builds the image for you on every push, with build caching so repeat builds stay fast. There is no builder to install and no docker build to run in your release pipeline. If you want the background on this model, our explainer on what a PaaS is covers the tradeoffs.

Out Plane follows exactly this pattern. It builds your image from the Dockerfile in your repository, and when no Dockerfile is present it auto-detects a buildpack and builds from plain source. The build runs on the platform's managed builder with caching, HTTPS is issued automatically, and a managed PostgreSQL database can sit alongside your app. For services PostgreSQL does not cover, such as Redis or MongoDB, you self-host them as containers with a persistent volume.

Aspectdocker build by handBuild on Out Plane
Who runs the buildYou, on a laptop or CI runnerThe platform, on every push
Builder engineBuildKit you install and updateManaged builder, kept current for you
CacheLocal, warm only on that machineRemote cache shared across builds
Multi-platformYou configure buildx and emulationBuilds for the platform's runtime architecture
Registry pushManual docker push stepAutomatic after a successful build
ReproducibilityVaries by machine and cache stateConsistent for the whole team
MaintenanceOngoing builder upkeepNone

Frequently Asked Questions

What is the difference between docker build and dockerfile build?

There is no separate dockerfile build command. Docker builds images with docker build, which reads a file named Dockerfile by default. People search for dockerfile build meaning the same thing, how to turn a Dockerfile into an image. Point at another file with docker build -f path/to/Dockerfile ..

What does the dot at the end of docker build mean?

The dot sets the build context, the set of files sent to the builder. docker build -t app . uses the current directory as context, so any COPY or ADD instruction can only reference files inside it. Use a .dockerignore file to keep large or secret paths out of the context.

How do I speed up docker build with caching?

Order instructions from least to most frequently changed. Copy dependency manifests like package.json and install packages before copying source code, so a code edit does not invalidate the install layer. BuildKit adds cache mounts and inline remote cache. Avoid --no-cache unless you need a clean rebuild.

What is docker buildx used for?

docker buildx is the extended build CLI that drives BuildKit directly. Its main use is multi-platform builds, producing one image that runs on both amd64 and arm64 from a single docker buildx build --platform linux/amd64,linux/arm64 command. It also manages named builders and advanced cache backends.

Do I need docker build to deploy to a PaaS?

No. A PaaS reads the Dockerfile in your repository and runs the build for you on every push, with managed build caching. You never install a builder or run docker build yourself. Out Plane also auto-detects a buildpack when no Dockerfile is present, so plain source deploys too.

When should I use a multi-stage Dockerfile?

Use multi-stage builds whenever your app needs compilers, bundlers, or dev dependencies at build time but not at run time. A build stage installs everything and produces artifacts. A slim final stage copies only those artifacts, cutting image size, attack surface, and pull time. Node, Go, and Java apps benefit most.

Is BuildKit enabled by default?

Yes. Since Docker Engine 23.0, BuildKit is the default builder, so a plain docker build already uses it. Older versions needed DOCKER_BUILDKIT=1 in the environment. BuildKit powers parallel stages, better caching, build secrets, and the # syntax=docker/dockerfile:1 frontend that enables newer Dockerfile features.

Conclusion

docker build is a small command with a lot of depth. It reads a Dockerfile top to bottom, turns each instruction into a cached layer, and stacks those layers into an image. Order your instructions for cache hits, split build and runtime work into stages, and lean on BuildKit and docker buildx when you need speed or multiple architectures. Those habits alone will cut most build times and image sizes.

In production, the honest move is to stop running the build by hand. Push your Dockerfile and let the platform build it reproducibly with managed caching, so every release is consistent. Create an app in the console at https://console.outplane.com and see the free Hobby tier and pay-as-you-go Pro details on /pricing.


Tags

docker
docker-build
dockerfile
buildkit
buildx
multi-stage-builds
paas

Start deploying in minutes

Connect your GitHub repository and deploy your first application today. $20 free credit. No credit card required.