How to Build Docker Image
How to Build Docker Image Docker has revolutionized the way software is developed, tested, and deployed. At the heart of Docker’s power lies the Docker image — a lightweight, standalone, executable package that includes everything needed to run a piece of software, including the code, runtime, libraries, environment variables, and configuration files. Building a Docker image is a foundational skil
How to Build Docker Image
Docker has revolutionized the way software is developed, tested, and deployed. At the heart of Dockers power lies the Docker image a lightweight, standalone, executable package that includes everything needed to run a piece of software, including the code, runtime, libraries, environment variables, and configuration files. Building a Docker image is a foundational skill for developers, DevOps engineers, and system administrators working in modern cloud-native environments. Whether youre containerizing a simple Python script or a complex microservice architecture, understanding how to build Docker images correctly ensures consistency across development, staging, and production environments.
This guide provides a comprehensive, step-by-step walkthrough on how to build Docker images from scratch. Youll learn not only the mechanics of the process but also the best practices that ensure your images are secure, efficient, and production-ready. Well explore real-world examples, recommend essential tools, and answer frequently asked questions to solidify your understanding. By the end of this tutorial, youll be equipped to build, optimize, and maintain Docker images with confidence.
Step-by-Step Guide
Prerequisites
Before you begin building Docker images, ensure your system meets the following requirements:
- Docker Engine installed on your machine (Windows, macOS, or Linux)
- A text editor or IDE (e.g., VS Code, Sublime Text)
- Basic familiarity with the command line
- A project or application you wish to containerize
To verify Docker is installed and running, open your terminal and run:
docker --version
If Docker is properly installed, youll see output similar to:
Docker version 24.0.7, build afdd53b
Next, ensure the Docker daemon is active:
docker info
If you encounter permission errors on Linux, you may need to add your user to the docker group:
sudo usermod -aG docker $USER
Log out and back in for the changes to take effect.
Step 1: Prepare Your Application
Before creating a Docker image, you need a working application. For this guide, well use a simple Python Flask web application as an example. Create a new directory for your project:
mkdir my-flask-app
cd my-flask-app
Inside this directory, create a file named app.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello, Docker World!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Next, create a requirements.txt file to list your Python dependencies:
Flask==3.0.0
These files form the foundation of your containerized application. The app.py file contains your application logic, and requirements.txt defines the packages needed to run it.
Step 2: Create a Dockerfile
The Dockerfile is a text file that contains a series of instructions used to build a Docker image. Its the blueprint for your container. Create a file named Dockerfile (with no extension) in the root of your project directory:
touch Dockerfile
Open the Dockerfile in your editor and add the following content:
Use an official Python runtime as a parent image
FROM python:3.11-slim
Set the working directory in the container
WORKDIR /app
Copy the current directory contents into the container at /app
COPY . /app
Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
Make port 5000 available to the world outside this container
EXPOSE 5000
Define environment variable
ENV FLASK_APP=app.py
Run app.py when the container launches
CMD ["flask", "run", "--host=0.0.0.0"]
Lets break down each instruction:
- FROM python:3.11-slim Specifies the base image. Using a slim variant reduces image size by excluding unnecessary packages.
- WORKDIR /app Sets the working directory inside the container. All subsequent commands run relative to this path.
- COPY . /app Copies all files from the current host directory into the containers /app directory.
- RUN pip install --no-cache-dir -r requirements.txt Installs Python dependencies. The
--no-cache-dirflag prevents pip from storing cached files, reducing image size. - EXPOSE 5000 Informs Docker that the container will listen on port 5000 at runtime. This doesnt publish the port thats done at runtime with the
-pflag. - ENV FLASK_APP=app.py Sets an environment variable used by Flask to locate the application module.
- CMD ["flask", "run", "--host=0.0.0.0"] Defines the default command to run when the container starts. Use JSON array syntax for better compatibility.
Step 3: Build the Docker Image
With your Dockerfile ready, you can now build the image. From the project root directory (where Dockerfile is located), run:
docker build -t my-flask-app .
The -t flag tags the image with a name (my-flask-app), and the . at the end specifies the build context the current directory. Docker reads the Dockerfile in this directory and executes the instructions sequentially.
As the build progresses, youll see output like:
Sending build context to Docker daemon 4.096kB
Step 1/7 : FROM python:3.11-slim
---> 9a4e4b2c1d7e
Step 2/7 : WORKDIR /app
---> Using cache
---> 5f9a3b1c2d8e
Step 3/7 : COPY . /app
---> 3e7f1d4a5b6c
Step 4/7 : RUN pip install --no-cache-dir -r requirements.txt
---> Running in 4b8f9a3c1d2e
Collecting Flask==3.0.0
Downloading Flask-3.0.0-py3-none-any.whl (96 kB)
Installing collected packages: Flask
Successfully installed Flask-3.0.0
Removing intermediate container 4b8f9a3c1d2e
---> 7a1b2c3d4e5f
Step 5/7 : EXPOSE 5000
---> Running in 6d7e8f9a0b1c
---> 8f9a0b1c2d3e
Step 6/7 : ENV FLASK_APP=app.py
---> Running in 9c8d7e6f5a4b
---> 6e5f4d3c2b1a
Step 7/7 : CMD ["flask", "run", "--host=0.0.0.0"]
---> Running in 5d4c3b2a1f0e
---> 1a2b3c4d5e6f
Successfully built 1a2b3c4d5e6f
Successfully tagged my-flask-app:latest
The final line confirms your image has been built and tagged. You can verify this by listing all local images:
docker images
You should see an entry like:
REPOSITORY TAG IMAGE ID CREATED SIZE
my-flask-app latest 1a2b3c4d5e6f 2 minutes ago 128MB
Step 4: Run the Container
Now that youve built the image, you can launch a container from it. Use the docker run command:
docker run -p 5000:5000 my-flask-app
The -p 5000:5000 flag maps port 5000 on your host machine to port 5000 in the container. This allows you to access the application via your browser at http://localhost:5000.
You should see Flasks development server output in your terminal:
* Running on http://0.0.0.0:5000
Open your browser and navigate to http://localhost:5000. Youll see the message: Hello, Docker World!
Step 5: Stop and Clean Up
To stop the running container, press Ctrl + C in the terminal. To remove the container after stopping it, use:
docker ps -a
This lists all containers, including stopped ones. Note the container ID or name, then remove it:
docker rm
To remove the image entirely (if needed), use:
docker rmi my-flask-app
Always clean up unused containers and images to free up disk space.
Best Practices
Building Docker images is not just about making them work its about making them efficient, secure, and maintainable. Following industry best practices ensures your containers are production-ready and scalable.
Use Specific Base Image Tags
Always avoid using the latest tag in your FROM instruction. For example:
? Avoid this
FROM python:latest
? Use this instead
FROM python:3.11-slim
Using latest introduces unpredictability a new version of Python may break your application. Pinning to a specific version ensures reproducibility and stability across environments.
Minimize Image Size
Smaller images are faster to build, pull, and deploy. Heres how to reduce size:
- Use slim or alpine variants of base images (e.g.,
python:3.11-slimorpython:3.11-alpine). - Avoid installing unnecessary packages or tools inside the container.
- Use
--no-cache-dirwith pip and clean package caches after installation. - Merge multiple
RUNcommands using&&to reduce layers.
Example of optimized RUN command:
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
Use .dockerignore
Just as you use .gitignore to exclude files from version control, use .dockerignore to exclude files from the build context. This improves build speed and reduces image size.
Create a .dockerignore file in your project root:
.git
node_modules
__pycache__
*.log
.env
Dockerfile
.dockerignore
These files are ignored during the build process, preventing accidental inclusion of sensitive or unnecessary data.
Multi-Stage Builds for Production
Multi-stage builds allow you to use multiple FROM statements in a single Dockerfile. Each stage can have its own base image and instructions. You can copy only the necessary artifacts from one stage to another, discarding build-time dependencies.
Example for a Node.js application:
Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
This results in a final image that contains only the runtime and built code no development tools, source files, or npm caches.
Use Non-Root Users
Running containers as root is a security risk. Always create and use a non-root user inside your container:
FROM python:3.11-slim
RUN addgroup -g 1001 -S appuser && adduser -u 1001 -S appuser -g appuser
USER appuser
WORKDIR /app
COPY --chown=appuser:appuser . /app
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
ENV FLASK_APP=app.py
CMD ["flask", "run", "--host=0.0.0.0"]
The USER instruction switches to the non-root user for all subsequent commands. The --chown flag ensures copied files have the correct ownership.
Label Your Images
Add metadata to your images using the LABEL instruction:
LABEL maintainer="dev-team@example.com"
LABEL version="1.0.0"
LABEL description="Flask web application for user authentication"
These labels help with documentation, auditing, and automation. You can view them later using:
docker inspect my-flask-app
Scan for Vulnerabilities
Regularly scan your images for known security vulnerabilities. Docker provides built-in scanning via docker scan (requires Docker Desktop and a Docker Hub account):
docker scan my-flask-app
Alternatively, use tools like Trivy, Snyk, or ClamAV for advanced scanning and CI/CD integration.
Dont Store Secrets in Images
Never hardcode API keys, passwords, or certificates in your Dockerfile or image. Use environment variables and Docker secrets or external secret managers (e.g., HashiCorp Vault, AWS Secrets Manager) at runtime.
Tools and Resources
Building Docker images becomes more efficient with the right tools. Below are essential utilities and platforms to enhance your workflow.
Docker Desktop
Docker Desktop is the most user-friendly way to run Docker on Windows and macOS. It includes:
- Docker Engine
- Docker CLI
- Docker Compose
- Kubernetes integration
- Resource usage monitoring
Download it at https://www.docker.com/products/docker-desktop.
Docker Compose
When your application consists of multiple services (e.g., web server, database, cache), use Docker Compose to define and run multi-container applications. Create a docker-compose.yml file:
version: '3.8'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- FLASK_ENV=development
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Run with:
docker-compose up
BuildKit
BuildKit is Dockers next-generation build backend, offering faster builds, better caching, and improved security. Enable it by setting:
export DOCKER_BUILDKIT=1
Add it to your shell profile (.bashrc, .zshrc) to make it permanent.
Container Registry Services
Once youve built your image, youll need to store and share it. Popular registries include:
- Docker Hub Free public registry with private repositories available.
- GitHub Container Registry (GHCR) Integrated with GitHub Actions and repositories.
- Amazon ECR AWSs managed container registry.
- Google Container Registry (GCR) Google Clouds container registry.
- Azure Container Registry (ACR) Microsofts container registry service.
To push your image to Docker Hub:
docker tag my-flask-app your-dockerhub-username/my-flask-app:1.0.0
docker login
docker push your-dockerhub-username/my-flask-app:1.0.0
CI/CD Integration Tools
Automate image building and deployment with:
- GitHub Actions Automate builds on git push.
- GitLab CI/CD Built-in container registry and pipeline support.
- Jenkins Extensible automation server with Docker plugins.
- CircleCI Cloud-based CI/CD with Docker support.
Example GitHub Actions workflow to build and push on tag:
name: Build and Push Docker Image
on:
push:
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
tags: your-dockerhub-username/my-flask-app:${{ github.ref_name }}
Image Analysis and Optimization Tools
- Trivy Open-source vulnerability scanner for containers.
- Dive Tool to explore each layer in a Docker image and discover space optimization opportunities.
- Container Structure Test Validate image structure and content programmatically.
Install Trivy:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
Scan your image:
trivy image my-flask-app
Real Examples
Lets walk through three real-world scenarios to demonstrate how Docker images are built for different technologies.
Example 1: Node.js Express Application
Project structure:
my-node-app/
??? app.js
??? package.json
??? .dockerignore
??? Dockerfile
app.js:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello from Node.js!');
});
app.listen(port, '0.0.0.0', () => {
console.log(Server running at http://0.0.0.0:${port});
});
package.json:
{
"name": "my-node-app",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Build and run:
docker build -t my-node-app .
docker run -p 3000:3000 my-node-app
Example 2: Go Binary Application
Go applications compile into single binaries, making them ideal for minimal Docker images.
main.go:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
Dockerfile (multi-stage):
Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main .
Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Build and run:
docker build -t my-go-app .
docker run -p 8080:8080 my-go-app
Example 3: React Frontend with Nginx
React apps are static and served via Nginx. Build the app first, then serve it in a lightweight container.
Dockerfile:
Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
Production stage
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Build and run:
docker build -t my-react-app .
docker run -p 80:80 my-react-app
Visit http://localhost to see your React app served by Nginx.
FAQs
What is the difference between a Docker image and a container?
A Docker image is a static, read-only template that contains the application code and dependencies. A container is a runnable instance of an image. You can create, start, stop, move, or delete containers, but images remain unchanged unless rebuilt.
Can I build Docker images on Windows and Linux?
Yes. Docker Desktop supports Windows and macOS, while Docker Engine runs natively on Linux. The Dockerfile syntax is identical across platforms. However, Linux containers on Windows require WSL2 (Windows Subsystem for Linux 2) for full compatibility.
Why is my Docker image so large?
Large images are typically caused by:
- Using non-slim base images (e.g.,
python:3.11instead ofpython:3.11-slim) - Installing unnecessary packages
- Not cleaning caches after installation
- Copying large files or directories into the image
Use multi-stage builds and .dockerignore to reduce size.
How do I update a Docker image after changing the code?
After modifying your application code:
- Rebuild the image:
docker build -t my-app:latest . - Stop and remove the running container:
docker stop my-container && docker rm my-container - Run a new container from the updated image:
docker run -p 5000:5000 my-app:latest
For development, consider using volume mounts to sync code changes without rebuilding.
How do I version my Docker images?
Use semantic versioning in your tags: my-app:v1.2.3. Avoid using latest in production. Always tag your images with version numbers and push them to a registry for traceability.
Can I build Docker images without Docker installed?
Yes. You can use cloud-based builders like:
- GitHub Codespaces
- GitLab CI/CD
- Google Cloud Build
- Buildpacks Tools like Paketo or CNB that build images without a Dockerfile
These platforms provide Docker-like environments in the cloud.
Is it safe to run Docker as root?
No. Running containers as root grants them full access to the host system. Always use non-root users inside containers and avoid running the Docker daemon as root unless absolutely necessary. Use user namespaces and security policies (e.g., SELinux, AppArmor) for added protection.
How do I inspect the layers of a Docker image?
Use the dive tool:
dive my-flask-app
It provides an interactive view of each layer, showing file additions, modifications, and deletions. This helps identify bloat and optimize your Dockerfile.
Conclusion
Building Docker images is a critical skill in modern software development. By following the step-by-step process outlined in this guide from preparing your application and writing a Dockerfile to building, running, and optimizing your container youve gained the foundational knowledge to containerize any application. But mastery comes with practice and adherence to best practices: use minimal base images, avoid secrets in images, leverage multi-stage builds, and scan for vulnerabilities.
The examples provided Python, Node.js, Go, and React demonstrate how Docker adapts to different technologies and deployment models. Whether youre deploying a simple script or a complex microservice, Docker ensures consistency, portability, and scalability.
As you continue your journey, integrate Docker into your CI/CD pipelines, automate image builds, and explore orchestration tools like Kubernetes. The future of software delivery is containerized and now, youre equipped to lead it.