# Building a Docker Image

Docker is an open-source containerization platform that enables developers to package applications and all their dependencies into lightweight, portable containers that run consistently across different environments. Docker images are the blueprint for containers — they package your application code, dependencies, environment variables, and runtime configurations into a portable, reproducible format. This guide explains how to build Docker images efficiently and create an optimized Dockerfile. By the end, you'll have a tested, size-optimized Docker image built with `docker buildx` and ready to push to a registry, following container image build practices documented in [**Vultr Docs**](https://docs.vultr.com/how-to-build-a-docker-image).

---

## The Short Answer

If you're already familiar with Docker and just need a quick reference, use the following commands:

```console
# Build a Docker image from a Dockerfile in the current directory
docker buildx build -t <image-name>:<tag> .

# Run a container from the image and map a port
docker container run -d -p <host-port>:<container-port> <image-name>:<tag>

# Tag the image for a container registry
docker image tag <image-name>:<tag> <registry>/<image-name>:<tag>

# Push your image to a registry (e.g., Docker Hub or another container registry)
docker image push <registry>/<image-name>:<tag>
```

Continue reading for detailed explanations and examples of each command.

## 1\. Create a Dockerfile

To build a Docker image, start by creating a `Dockerfile`. This file defines the environment, dependencies, and commands needed to build your application into a container image. Follow the steps below to create a `Dockerfile` for a Node.js web application.

**1\. Create a file named** `Dockerfile` **in your project's root directory:**

```console
vim Dockerfile
```

**2\. Add the following content to the file, then save and exit:**

```dockerfile
# Use Node.js as the base image
FROM node:22-alpine

# Set the working directory
WORKDIR /app

# Copy dependency files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy application source code
COPY . .

# Expose application port
EXPOSE 3000

# Define the startup command
CMD ["node", "app.js"]
```

### Breakdown of the Dockerfile

- **FROM node:22-alpine**: Defines `node:22-alpine` as the base image.
- **WORKDIR /app**: Sets `/app` as the working directory inside the container.
- *COPY package.json ./*\*: Copies dependency definition files to the working directory.
- **RUN npm install**: Installs project dependencies using npm.
- **COPY . .**: Copies the entire project directory into the container.
- **EXPOSE 3000**: Declares port 3000 as the container's exposed port.
- **CMD \["node", "app.js"\]**: Defines the startup command to run the application.

The above `Dockerfile` follows several best practices:

- Uses a specific version of the base image.
- Copies dependency files first to take advantage of Docker's build cache.
- Uses a minimal Alpine-based image to reduce image size.
- Defines the default command to run the application.

## 2\. Build the Docker Image

With your `Dockerfile` ready, use the `docker buildx build` command to create your image. Docker Buildx enables advanced build features such as multi-platform support, build caching, and pushing to registries.

**1\. Basic build with a tag:**

```console
docker buildx build -t my-app:latest .
```

**2\. Build with multiple tags and build arguments:**

```console
docker buildx build -t my-app:1.0 -t my-app:latest --build-arg ENV=production .
```

**3\. For a multi-platform build and push to a registry, first create and bootstrap a builder (one-time setup):**

```console
docker buildx create \
  --name container-builder \
  --driver docker-container \
  --bootstrap --use
```

**4\. After the** `container-builder` **is running, run the multi-platform build:**

```console
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t my-app:latest \
  --push .
```

### Command Breakdown

Explanation of key options used with `docker buildx build`:

- `docker buildx build`: Uses Docker BuildKit's extended build command for advanced features like multi-platform builds and better caching.
- `-t my-app:latest`: Tags the image with a name (`my-app`) and version (`latest`).
- `-t my-app:1.0`: Adds another tag (`1.0`) to the image.
- `--build-arg ENV=production`: Passes build-time variable `ENV` with the value `production` into the Dockerfile.
- `--platform linux/amd64,linux/arm64`: Builds images for multiple CPU architectures (for example, x86_64 and ARM64).
- `--push`: Pushes the built image and manifest list to the specified container registry.
- `--no-cache`: Forces the build to skip caching and build each layer from scratch.
- `--pull`: Always pulls the latest version of the base image instead of using the cached one.
- `-f Dockerfile.prod`: Specifies a custom Dockerfile (`Dockerfile.prod`) instead of the default Dockerfile.
- `.`: Sets the build context to the present directory.

### Example Scenario

Follow this example to build and tag a Docker image for a Node.js application.

**1\. Clone an example Docker Node.js application repository:**

```console
git clone https://github.com/docker/docker-nodejs-sample
```

**2\. Navigate to the cloned project directory:**

```console
cd docker-nodejs-sample
```

**3\. Create a** `Dockerfile` **inside the project root directory:**

```console
vim Dockerfile
```

**4\. Add the following content to the file:**

```dockerfile
FROM node:22-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
USER node
EXPOSE 3000
CMD ["node", "src/index.js"]
```

**5\. Build the Docker image:**

```console
docker buildx build -t my-app:latest .
```

**6\. View the newly built Docker image:**

```console
docker image ls
```

The above command lists all the Docker images available on your system.

**How the Docker Build Works:**

- Docker processes the `Dockerfile` in a layered fashion.
- Starts with the base image (`node:22-alpine` in this case).
- Creates a temporary container for each instruction (for example, `COPY`, `RUN`, `CMD`).
- Executes the instruction inside the container and commits the result as a new image layer.
- Removes the temporary container.
- Repeats the process for each instruction in the Dockerfile.
- Tags the final image with the names and versions you specified.

## 3\. Test the Docker Image

After building your Docker image, test it by running a container and verifying that your application behaves as intended.

**1\. Run a container from your image:**

```console
docker container run -d -p 3000:3000 --name check-list-app my-app:latest
```

In the above command:

- `docker container run`: Starts a container from an image.
- `-d`: Runs the container in detached mode (in the background).
- `-p 3000:3000`: Maps port 3000 of the host to port 3000 of the container.
- `--name check-list-app`: Assigns a custom name to the container.
- `my-app:latest`: Specifies the image and tag to use.

**2\. Verify the container is running:**

```console
docker container ls
```

This command lists all running containers. After confirming your container is listed and running, you can access your application to ensure it behaves as expected.

**3\. Check the application logs for debugging purposes:**

```console
docker container logs check-list-app
```

This command displays the container's logs, allowing you to verify application startup messages or troubleshoot any issues.

**4\. Stop and remove the container:**

```console
docker container stop check-list-app
docker container rm check-list-app
```

The above commands stop the running container and then remove it entirely from your system.

## 4\. Optimize Docker Image Size

Optimizing Docker image size leads to faster deployments, lower storage costs, and improved security. Follow these best practices to create smaller, more efficient Docker images.

### Use Multi-Stage Builds

Multi-stage builds let you create lean final images by separating build and runtime environments. Each `FROM` starts a new build stage, allowing you to copy only necessary artifacts into the final image.

```dockerfile
# Build stage
FROM golang:1.17 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]
```

The final image includes only the compiled binary and runtime dependencies, excluding source code and build tools.

### Choose Smaller Base Images

Using smaller base images helps reduce Docker image size, improve security, and speed up build and deployment times. Choose a base image that includes only the essential components needed to run your application.

```dockerfile
# Builder stage: install dependencies
FROM python:3.9-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt --target /app/packages

# Final image: minimal runtime using distroless
FROM gcr.io/distroless/python3
COPY --from=builder /app/packages /packages
ENV PYTHONPATH=/packages
COPY app.py /
CMD ["app.py"]
```

**Best practices:**

- Use `alpine` or `slim` based images as a minimal base for building dependencies.
- Use `distroless` for a secure, production-ready runtime image.
- Consider scratch images for statically compiled binaries in languages like Go or Rust.
- Apply multi-stage builds to separate build and runtime environments.
- Exclude package managers and shells from the final image.
- Set environment variables like `PYTHONPATH` to locate dependencies at runtime.

### Reduce Layer Count and Image Size

Each instruction in a Dockerfile creates a new layer. Fewer, optimized layers reduce image size and speed up builds.

**Example of an inefficient Dockerfile (creates many layers):**

```dockerfile
# Bad practice - many layers
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
RUN pip install flask
```

In the above Dockerfile:

- Each `RUN` instruction creates a new layer.
- No cleanup of APT cache or temporary files.
- Slower builds and larger images.

**Example of an optimized Dockerfile (fewer layers and smaller image):**

```dockerfile
# Better practice - fewer layers
FROM ubuntu:20.04
RUN apt-get update && \
 apt-get install -y python3 python3-pip && \
 pip install flask && \
 apt-get clean && \
 rm -rf /var/lib/apt/lists/*
```

In the above Dockerfile:

- Related commands are combined using `&&` to reduce layers.
- Package caches and temporary files are cleaned up during installation.

### Use a `.dockerignore` File

Exclude unnecessary files and directories from the Docker build context to reduce image size and speed up build times.

```ini
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
README.md
.env
*.pyc
__pycache__
```

In the above `.dockerignore` file:

- Prevents unneeded files from being copied into the image.
- Keeps the image clean and lightweight.
- Speeds up build time by minimizing the context sent to the Docker daemon.

### Leverage Docker Build Cache

Docker caches image layers to avoid rebuilding unchanged steps. Structure your Dockerfile to take full advantage of this.

Best practices:

- Place less frequently changing instructions at the top of your Dockerfile.
- Copy dependency files first, install packages, then copy the application source last.

**Example of an efficient caching structure:**

```dockerfile
FROM node:16-alpine

WORKDIR /app

# Copy dependency files first
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the source code last
COPY . .

CMD ["node", "app.js"]
```

In the above Dockerfile:

- Dependency layers are reused unless `package*.json` changes.
- Source code changes won't invalidate the cached dependency layers.
- Results in faster builds and smaller diffs in CI/CD pipelines.

## Next Steps

- Wire `docker buildx build --push` into a CI/CD pipeline so images rebuild and publish automatically on every commit.
- Scan your images for vulnerabilities with a tool like Trivy or Docker Scout before pushing to production.
- Add a multi-stage build to any existing Dockerfile that still ships build tools in the final image.
- Set up `docker buildx` multi-platform builds if you need to support both x86_64 and ARM64 deployments.

For the full guide with additional tips, visit the original article on [**Vultr Docs**](https://docs.vultr.com/how-to-build-a-docker-image).
