How Do You Build a Docker Image From a Dockerfile?
Table of Contents
docker build -t myapp:1.0 .
That one command does more than it looks like. Breaking down what actually happens explains most of the confusing behavior people run into.
The build context #
The . at the end isn’t optional decoration. It’s the build context: the directory whose contents get sent to the Docker daemon before the build even starts. Every COPY and ADD instruction in the Dockerfile can only reference files inside that context; you cannot COPY ../shared-lib from a directory above it, because the daemon never received anything outside the context in the first place.
This matters for two practical reasons:
- Build context size affects build speed. If your project directory contains a 2GB
node_modulesor a.githistory, all of that has to be scanned and, at least on the first build, sent to the daemon unless you exclude it. - A
.dockerignorefile fixes it, working much like.gitignore:
# .dockerignore
node_modules
.git
*.log
dist
Layer caching #
Docker builds a Dockerfile one instruction at a time, and caches the result of each instruction as a layer. On a subsequent build, if an instruction and everything it depends on (the base image, prior layers, the instruction’s own content) are unchanged, Docker reuses the cached layer instead of re-executing it. This is why instruction order matters; see the caching example in What Is a Dockerfile?.
Force a full rebuild, ignoring all cache, with:
docker build --no-cache -t myapp:1.0 .
Useful when you suspect a cached layer is stale, for instance an apt-get install layer that won’t see newly available package updates because nothing in the Dockerfile changed to invalidate it.
Naming and tagging at build time #
docker build -t myapp:1.0 -t myapp:latest .
You can apply multiple tags in one build. -t is the same tagging mechanism as docker tag; see
What Are Docker Tags? for how to choose your tags.
Build arguments vs environment variables #
ARG values exist only during the build and are not set in the running container; ENV values persist into the running container. Pass a build arg with --build-arg:
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim
docker build --build-arg NODE_VERSION=22 -t myapp .
Don’t put secrets in ARG (or ENV): build args can end up visible in the image history (docker history). Use BuildKit secret mounts instead (below).
BuildKit #
Modern Docker builds use BuildKit by default (since Docker Engine 23.0), which parallelizes independent build stages, supports better cache reuse across builds (including remote cache backends in CI), and adds RUN --mount=type=secret for passing secrets into a build step without leaving them in any layer:
# syntax=docker/dockerfile:1
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .
The secret is available only during that specific RUN step and never written to a layer, which makes it the correct way to hand a build a private registry token without baking it into the image.
Building for a different platform #
If you’re building on an Apple Silicon Mac but deploying to linux/amd64 servers (or vice versa), cross-build explicitly:
docker build --platform linux/amd64 -t myapp .
Without --platform, Docker builds for the architecture of the machine running the build, which is a common source of “works on my machine, crashes in production” (typically as an exec format error) when dev and prod architectures differ.