How to Build Docker Image
How to Build Docker Image Docker has revolutionized the way software is developed, deployed, and scaled. 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 the foundational st
How to Build Docker Image
Docker has revolutionized the way software is developed, deployed, and scaled. 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 the foundational step in containerizing applications, enabling consistent behavior across development, testing, and production environments. Whether you're a developer, DevOps engineer, or system administrator, mastering how to build Docker images is essential for modern software delivery.
This guide provides a comprehensive, step-by-step walkthrough on how to build Docker images from scratch. Well cover everything from writing a Dockerfile to optimizing images for production, along with industry best practices, essential tools, real-world examples, and answers to common questions. By the end of this tutorial, youll have the knowledge and confidence to create efficient, secure, and scalable Docker images tailored to your applications needs.
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, or Vim)
- Basic familiarity with the command line
- A sample application or codebase you wish to containerize
To verify Docker is installed and running, open your terminal and run:
docker --version
You should see output similar to:
Docker version 24.0.7, build afdd53b
If Docker is not installed, visit https://docs.docker.com/get-docker/ to download and install the appropriate version for your operating system.
Step 1: Prepare Your Application
Before creating a Docker image, organize your applications files in a clean directory structure. For example, if youre containerizing a Python web application, your project folder might look like this:
my-app/
??? app.py
??? requirements.txt
??? README.md
??? .dockerignore
The app.py file contains your application code. The requirements.txt file lists Python dependencies. The .dockerignore file (which well create next) helps exclude unnecessary files from the build context.
Step 2: Create a Dockerfile
The Dockerfile is a text file that contains a series of instructions used to build a Docker image. It must be named exactly Dockerfile (no extension) and placed in the root of your project directory.
Open your terminal and navigate to your project folder. Create the Dockerfile:
touch Dockerfile
Now open the file in your editor and add the following content for a Python Flask application:
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 Specifies the base image. We use
python:3.11-slimto reduce image size while retaining functionality. - WORKDIR Sets the working directory inside the container. All subsequent commands run relative to this path.
- COPY Copies files from your local machine into the container. Avoid using
COPY *to prevent copying unnecessary files. - RUN Executes commands during the build process. Here, we install Python dependencies.
- EXPOSE Informs Docker that the container listens on port 5000 at runtime. This is documentation; it doesnt publish the port.
- ENV Sets environment variables used by the application.
- CMD Defines the default command to run when the container starts. Only one CMD instruction is allowed per Dockerfile.
Step 3: Create a .dockerignore File
Just as a .gitignore file excludes files from version control, a .dockerignore file excludes files from the Docker build context. This improves build speed and reduces image size by preventing unnecessary files from being copied into the image.
Create a .dockerignore file in your project root:
touch .dockerignore
Add the following content:
.git
node_modules
__pycache__
*.log
.DS_Store
.env
venv/
This ensures that Git metadata, virtual environments, logs, and OS-specific files are excluded from the build context.
Step 4: Build the Docker Image
With your Dockerfile and .dockerignore in place, youre ready to build the image. In your terminal, navigate to the project directory and run:
docker build -t my-flask-app .
Lets examine the command:
- docker build Tells Docker to build an image.
- -t my-flask-app Tags the image with a name (
my-flask-app). You can also include a version tag likemy-flask-app:1.0. - . Specifies the build context (the current directory). Docker looks for the Dockerfile here.
Docker will now execute each instruction in the Dockerfile sequentially. Youll see output similar to:
Sending build context to Docker daemon 3.584kB
Step 1/7 : FROM python:3.11-slim
---> 1a2b3c4d5e6f
Step 2/7 : WORKDIR /app
---> Using cache
---> 2b3c4d5e6f7a
Step 3/7 : COPY . /app
---> 3c4d5e6f7a8b
Step 4/7 : RUN pip install --no-cache-dir -r requirements.txt
---> Running in 9f8e7d6c5b4a
Collecting Flask==2.3.3
Downloading Flask-2.3.3-py3-none-any.whl (101 kB)
Installing collected packages: Flask
Successfully installed Flask-2.3.3
Removing intermediate container 9f8e7d6c5b4a
---> 4d5e6f7a8b9c
Step 5/7 : EXPOSE 5000
---> Running in 5e6f7a8b9c0d
---> 6f7a8b9c0d1e
Step 6/7 : ENV FLASK_APP=app.py
---> Running in 7a8b9c0d1e2f
---> 8b9c0d1e2f3a
Step 7/7 : CMD ["flask", "run", "--host=0.0.0.0"]
---> Running in 9c0d1e2f3a4b
---> a0b1c2d3e4f5
Successfully built a0b1c2d3e4f5
Successfully tagged my-flask-app:latest
Once complete, verify the image was created by listing all local images:
docker images
You should see your image listed:
REPOSITORY TAG IMAGE ID CREATED SIZE
my-flask-app latest a0b1c2d3e4f5 2 minutes ago 120MB
Step 5: Run the Container
Now that youve built the image, you can run it as a container. Use the following command:
docker run -p 5000:5000 my-flask-app
- -p 5000:5000 Maps port 5000 on your host machine to port 5000 in the container.
- my-flask-app The image name to run.
Open your browser and navigate to http://localhost:5000. If your Flask app is correctly configured, you should see your application running.
Step 6: Stop and Clean Up
To stop the running container, press Ctrl + C in the terminal. To list all containers (including stopped ones), run:
docker ps -a
To remove a stopped container, use:
docker rm <container_id>
To remove the image (if you no longer need it), use:
docker rmi my-flask-app
Best Practices
Use Official Base Images
Always prefer official images from Docker Hub (e.g., python, node, nginx, alpine) over unofficial or custom images. Official images are maintained by the software vendors and undergo regular security audits. For example, use python:3.11-slim instead of python:latest to avoid unexpected breaking changes.
Minimize Image Layers
Each instruction in a Dockerfile creates a new layer. Too many layers increase image size and slow down builds. Combine related commands using && and line continuations (\). For example:
RUN apt-get update && apt-get install -y \
curl \
vim \
&& rm -rf /var/lib/apt/lists/*
This reduces the number of layers and removes temporary files in the same step.
Use .dockerignore to Exclude Unnecessary Files
As previously mentioned, the .dockerignore file prevents irrelevant files from being copied into the build context. This not only speeds up builds but also reduces the attack surface by excluding sensitive files like .env or config.json.
Avoid Running as Root
By default, Docker containers run as the root user, which poses a security risk. Create a non-root user inside the container:
FROM python:3.11-slim
RUN addgroup --system appuser && adduser --system --group appuser
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
EXPOSE 5000
CMD ["flask", "run", "--host=0.0.0.0"]
This ensures that even if an attacker compromises the container, they cannot escalate privileges to root.
Use Multi-Stage Builds for Production Images
Multi-stage builds allow you to use multiple FROM statements in a single Dockerfile. Each stage can have its own base image and instructions. The final stage contains only the artifacts needed for production, drastically reducing image size.
Example for a Node.js application:
Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
The final image is only ~100MB instead of over 1GB, because it doesnt include build tools, source code, or development dependencies.
Pin Dependency Versions
Never use floating tags like latest in production. Always pin versions in your Dockerfile and package manifests:
FROM python:3.11.6-sliminstead ofFROM python:3.11flask==2.3.3instead offlask>=2.0
This ensures reproducible builds and prevents unexpected behavior due to dependency updates.
Scan Images for Vulnerabilities
Regularly scan your images for known security vulnerabilities using tools like Docker Scout, Trivy, or Clair. For example, with Trivy:
trivy image my-flask-app
Integrate scanning into your CI/CD pipeline to block deployments of vulnerable images.
Optimize Build Cache
Docker caches each layer to speed up subsequent builds. Order your Dockerfile instructions from least to most frequently changing:
- FROM rarely changes
- COPY requirements.txt changes infrequently
- RUN pip install changes when dependencies change
- COPY . changes with every code update
This way, only the final layer needs to be rebuilt when your source code changes.
Tools and Resources
Essential Docker Tools
- Docker Desktop The official GUI for macOS and Windows, simplifying Docker setup and management.
- Docker CLI Command-line interface for building, running, and managing containers.
- Docker Compose Used to define and run multi-container applications using a YAML file. Ideal for local development with databases, caches, and microservices.
- Docker Buildx An extended build client that supports multi-platform builds (e.g., building ARM images on x86 machines).
- Docker Scout A security and compliance tool for analyzing images, detecting vulnerabilities, and tracking software bills of materials (SBOMs).
- Trivy An open-source vulnerability scanner for containers and other artifacts.
- Hadolint A linter for Dockerfiles that detects common mistakes and enforces best practices.
Image Registries
Once youve built an image, youll need to store it in a registry for sharing and deployment:
- Docker Hub Free public registry with unlimited public repositories and limited private repos.
- GitHub Container Registry (GHCR) Integrated with GitHub Actions and repositories. Ideal for open-source projects.
- Amazon ECR AWS-managed container registry with tight integration for EC2, ECS, and EKS.
- Google Container Registry (GCR) Google Clouds container registry, optimized for GKE.
- GitLab Container Registry Built into GitLab CI/CD pipelines.
To push an image to Docker Hub:
docker tag my-flask-app your-dockerhub-username/my-flask-app:1.0
docker login
docker push your-dockerhub-username/my-flask-app:1.0
Online Resources
- Docker Documentation Official and comprehensive guides.
- Docker Official Images GitHub Source for all official images.
- Multi-Stage Builds Blog Deep dive into optimizing image size.
- What is a Container? Beginner-friendly introduction to containerization.
- Dive A tool for exploring each layer in a Docker image and discovering ways to reduce size.
CI/CD Integration
Automate Docker image builds using CI/CD platforms:
- GitHub Actions Use
docker/build-push-actionto build and push images on push or pull request. - GitLab CI Leverage built-in Docker-in-Docker (DinD) support.
- CircleCI Use the Docker executor and orb for streamlined builds.
- Jenkins Install the Docker Pipeline plugin to integrate Docker commands into pipelines.
Example GitHub Actions workflow for building and pushing a Docker image:
name: Build and Push Docker Image
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: your-dockerhub-username/my-app:latest
Real Examples
Example 1: Node.js Express Application
Lets containerize a simple Express server.
Project structure:
node-app/
??? server.js
??? package.json
??? .dockerignore
??? Dockerfile
server.js:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello from Dockerized Node.js!');
});
app.listen(port, '0.0.0.0', () => {
console.log(Server running at http://0.0.0.0:${port});
});
package.json:
{
"name": "node-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.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"]
.dockerignore:
node_modules
npm-debug.log
.git
Build and run:
docker build -t node-app .
docker run -p 3000:3000 node-app
Example 2: Nginx Static Website
Deploy a static HTML site using Nginx.
Project structure:
static-site/
??? index.html
??? styles.css
??? script.js
??? Dockerfile
index.html:
<!DOCTYPE html>
<html>
<head>
<title>My Static Site</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Welcome to My Dockerized Site</h1>
<script src="script.js"></script>
</body>
</html>
Dockerfile:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
COPY styles.css /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
EXPOSE 80
Build and run:
docker build -t static-site .
docker run -p 8080:80 static-site
Visit http://localhost:8080 to see your site.
Example 3: Multi-Stage Python + Pandas Data App
Build a lightweight image for a data processing script using pandas.
data-app.py:
import pandas as pd
df = pd.read_csv('data.csv')
print(df.head())
Dockerfile:
Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
Final stage
FROM python:3.11-slim
WORKDIR /app
Copy only the installed packages from builder stage
COPY --from=builder /root/.local /root/.local
COPY data.csv .
COPY data-app.py .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "data-app.py"]
This approach avoids installing pandas and its heavy dependencies (like NumPy) in the final image, reducing size from ~1.2GB to ~150MB.
FAQs
What is the difference between a Docker image and a container?
A Docker image is a read-only template with instructions for creating a container. A container is a runnable instance of an image. Think of the image as a class in object-oriented programming and the container as an instance of that class.
Can I build Docker images on Windows and Linux?
Yes. Docker Desktop for Windows and macOS includes a Linux VM to run containers. On Linux, Docker runs natively. The build process is identical across platforms.
Why is my Docker image so large?
Large images are often caused by:
- Using non-slim base images (e.g.,
python:3.11instead ofpython:3.11-slim) - Installing development tools or unnecessary packages
- Not cleaning up cache files (e.g.,
apt-get clean) - Not using multi-stage builds
Use docker history <image> to inspect layer sizes and identify bloat.
How do I update a Docker image after changing the code?
Rebuild the image with the same tag:
docker build -t my-app .
Then stop and remove the old container, and run a new one:
docker stop my-container
docker rm my-container
docker run -p 5000:5000 my-app
Can I build images without a Dockerfile?
No. A Dockerfile is required to define the build steps. However, you can use tools like docker commit to create an image from a running container but this is discouraged for production use as it lacks reproducibility and version control.
How do I share my Docker image with others?
Push it to a registry like Docker Hub or GitHub Container Registry. Others can then pull it using:
docker pull your-username/your-image:tag
Is it safe to use the latest tag in production?
No. The latest tag is mutable and can change without notice. Always use specific version tags (e.g., python:3.11.6) for reproducible, stable deployments.
How do I debug a failing Docker build?
Run the build with verbose output:
docker build --no-cache -t my-app .
The --no-cache flag forces Docker to rebuild every layer, helping you identify where the failure occurs. You can also run an interactive container from a previous successful layer to inspect its state:
docker run -it <image-id> /bin/bash
Conclusion
Building Docker images is a critical skill in modern software development. By following the steps outlined in this guide from writing a clean Dockerfile and using .dockerignore to implementing multi-stage builds and scanning for vulnerabilities you can create lightweight, secure, and reproducible containers that work consistently across environments.
Remember that Docker is not just a tool for deployment its a philosophy of encapsulation, portability, and automation. When done right, Docker images become the foundation of scalable microservices, CI/CD pipelines, and cloud-native applications.
Start small: containerize a simple script or web app. Then gradually adopt best practices pin versions, use non-root users, optimize layers, and automate builds. As you gain experience, youll discover how Docker transforms not just how you deploy code, but how you think about software architecture.
Now that you know how to build Docker images, the next step is to orchestrate them using Docker Compose for local development and Kubernetes for production. But thats a topic for another guide. For now, build, test, and iterate. Your containerized future is waiting.