How to Build a Docker Image: A Step-by-Step Guide
Docker images are the foundation of containerized applications. Understanding how to build one — and what shapes the process — is essential for anyone working with modern software development, DevOps pipelines, or self-hosted applications.
What Is a Docker Image?
A Docker image is a read-only template that contains everything needed to run an application: the operating system layer, runtime environment, dependencies, configuration files, and application code. When you run an image, Docker creates a container — a live, isolated instance based on that template.
Images are built in layers. Each instruction in a build file adds a new layer on top of the previous one. Docker caches these layers, which makes rebuilds faster when only part of your setup changes.
The Dockerfile: Where Every Build Starts
Every Docker image is built from a Dockerfile — a plain text file with a specific set of instructions. There's no GUI involved; it's a declarative script that Docker reads top to bottom.
Common Dockerfile instructions:
| Instruction | What It Does |
|---|---|
FROM | Sets the base image (e.g., Ubuntu, Node, Python) |
RUN | Executes a shell command during the build |
COPY / ADD | Copies files from your local machine into the image |
WORKDIR | Sets the working directory inside the image |
ENV | Defines environment variables |
EXPOSE | Documents which port the container listens on |
CMD / ENTRYPOINT | Defines the default command to run at startup |
A minimal example for a Node.js application:
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["node", "server.js"] This tells Docker: start from an official lightweight Node.js image, copy the project files in, install dependencies, and start the server.
Running the Build Command 🛠️
Once your Dockerfile is ready, you build the image using the Docker CLI:
docker build -t my-app:1.0 . Breaking this down:
docker build— triggers the image build process-t my-app:1.0— tags the image with a name and version.— tells Docker to look for the Dockerfile in the current directory
Docker will process each instruction sequentially and output the result of each layer. A successful build ends with a message showing the image ID and tag.
To verify your image was created:
docker images Key Variables That Affect How You Build
Building a Docker image isn't one-size-fits-all. Several factors determine the right approach for your situation.
Base Image Choice
The FROM instruction defines your starting point. Options range from full OS images (like ubuntu:22.04) to minimal variants (alpine) to language-specific official images (python:3.12-slim). Smaller base images reduce final image size and attack surface, but may require you to install more dependencies manually. The right base image depends on your application's runtime requirements and how much you want to control the environment.
Build Context Size
The build context is the set of files Docker sends to the build engine. A large project directory with node_modules, test data, or build artifacts will slow down every build. A .dockerignore file works like .gitignore — it tells Docker which files to exclude from the context:
node_modules .git *.log dist Neglecting this is one of the most common reasons builds feel slow.
Layer Caching Strategy
Docker caches each layer. If a layer hasn't changed since the last build, Docker reuses it instead of re-running that instruction. Layer order matters significantly. Instructions that change frequently (like COPY . .) should come after instructions that rarely change (like RUN npm install). Reversing this order breaks cache unnecessarily and slows every rebuild.
Multi-Stage Builds
For compiled languages (Go, Java, Rust, TypeScript) or projects with heavy build tooling, multi-stage builds let you separate the build environment from the runtime environment. You compile in one stage, then copy only the resulting binary or bundle into a clean final image. This keeps production images lean without changing how the application runs.
# Stage 1: Build FROM node:20 AS builder WORKDIR /app COPY . . RUN npm install && npm run build # Stage 2: Production FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist CMD ["node", "dist/index.js"] Platform and Architecture
If you're building on an Apple Silicon Mac (ARM) but deploying to a Linux server (x86), you may need to specify the target platform:
docker buildx build --platform linux/amd64 -t my-app:1.0 . docker buildx is Docker's extended build tool that supports multi-platform image creation. Ignoring this mismatch is a common source of confusing runtime errors.
What Changes Based on Your Setup 🔍
The complexity of your Dockerfile and build process scales with your use case:
- Simple static sites or scripts — a few lines, a small base image, done in seconds
- Full-stack web applications — multi-stage builds, environment variable management, port mapping
- Data science or ML workloads — large base images (CUDA, PyTorch), GPU access flags, significant image sizes
- Microservices in CI/CD pipelines — build caching strategies, registry pushing, automated tagging
Your operating system, available disk space, Docker version, and network speed all affect build time and behavior in practice. A developer building on a local laptop has different constraints than a pipeline running in a cloud CI environment.
The mechanics of docker build are consistent — but what goes inside the Dockerfile, and how you optimize the process, depends entirely on what you're building and where it needs to run.