DEV Community

Atsushi Suzuki
Atsushi Suzuki

Posted on

Optimizing Docker Images with Multi-Stage Builds and Distroless Approach

When we transitioned our application from Node.js (Express) to Go, we managed to reduce the Docker image size from 2.8 GB to 400 MB, which initially satisfied us. However, aiming for more efficient use of resources and faster deployment, we realized there was still room for further reduction in the image size, so we decided to try a new approach.

This might be familiar territory for engineers with DevOps experience, but for beginners, I'll share the improvements we made.

Original Dockerfile

Our original Dockerfile used a golang:1.21.0 base image and included steps for downloading Go modules, building the application, and executing it. However, this approach meant that all files needed for the development environment were included in the image, resulting in a larger final image size.

# Base the image on the official Go image
FROM golang:1.21.0

# Create the application directory
WORKDIR /app

# Enable Go modules
ENV GO111MODULE=on

# Copy and download dependencies
COPY go.mod .
COPY go.sum .
RUN go mod download

# Copy the application source
COPY . .

# Build the application
RUN go build -o main .

# Expose the port
EXPOSE 8081

# Execute the application command
CMD ["./main"]
Enter fullscreen mode Exit fullscreen mode

Improved Dockerfile

In the improved Dockerfile, we introduced multi-stage builds. Multi-stage builds define multiple build stages in a single Dockerfile, and only the necessary files are included in the final image. In the first stage, the application is built, and in the next stage, only the built executable file is copied to a lightweight alpine image. This reduces the final image size significantly by including only the essential files.

# Base the image on the official Go image
FROM golang:1.21.0-alpine as builder

# Create the application directory
WORKDIR /app

# Enable Go modules
ENV GO111MODULE=on

# Copy and download dependencies
COPY go.mod .
COPY go.sum .
RUN go mod download

# Copy the application source
COPY . .

# Build the application
RUN go build -o main .

# Execution stage
FROM alpine:3.19

WORKDIR /root/

# Copy the built binary
COPY --from=builder /app/main .

# Expose the port
EXPOSE 8081

# Execute the application command
CMD ["./main"]
Enter fullscreen mode Exit fullscreen mode

Results Comparison

Using the original Dockerfile, the image size was about 400 MB. However, after applying the multi-stage build, the image size was reduced to just 10 MB. Without multi-stage build but using golang:1.21.0-alpine as the base image, the size was about 160 MB.

About Distroless Images

For this project, we chose the Alpine image with a shell for debugging purposes, but we also considered using Google's Distroless images. Distroless images are known for being very lightweight and secure environments, containing only the minimal files required, with shells and unnecessary packages removed.

When using Distroless images, applications written in Go need statically linked binaries that do not depend on external C libraries. This is because Distroless provides a minimal environment without external libraries and shells. Therefore, it's necessary to set CGO_ENABLED=0 to disable CGO (the interface between C and Go languages), creating a standalone binary that includes all dependencies.

The Dockerfile using Distroless would look like this:

# Build stage
FROM golang:1.21.0 as builder

# Set the application directory
WORKDIR /app

# Enable Go modules
ENV GO111MODULE=on

# Copy and download dependencies
COPY go.mod .
COPY go.sum .
RUN go mod download

# Copy the application source
COPY . .

# Build the application
RUN CGO_ENABLED=0 go build -o main .

# Execution stage
FROM gcr.io/distroless/base-debian10

# Copy the built binary
COPY --from=builder /app/main /

# Execute the application
CMD ["/main"]
Enter fullscreen mode Exit fullscreen mode

Conclusion

In summary, our journey in optimizing Docker images for a Go application taught us valuable lessons about the efficiency and security in containerized environments. By implementing multi-stage builds, we were able to drastically reduce the image size, thus enhancing deployment speed and minimizing resource usage. This approach not only contributes to a more streamlined and cost-effective deployment process but also aligns well with the principles of modern DevOps practices.

Furthermore, exploring options like the Distroless images opened our eyes to the importance of security in Docker environments. While we opted for Alpine due to our immediate need for a shell for debugging, Distroless presents a compelling alternative for production environments, where security and minimalism are paramount.

This exercise stands as a testament to the continuous evolution in the field of software development and deployment. It highlights the importance of staying adaptable and always being on the lookout for better, more efficient solutions. As technology progresses, so do the tools and practices, enabling us to build and deploy applications that are not only powerful but also efficient and secure.

For both seasoned DevOps professionals and beginners, these findings underscore the significance of Docker image optimization and the impact it can have on the overall software delivery lifecycle. As we move forward, it's clear that embracing such optimizations will be key in driving operational excellence and delivering software at the pace and scale demanded by modern digital landscapes.

Top comments (1)

Collapse
 
syxaxis profile image
George Johnson

Something I only learned about a few weeks back and multi-stage really does reduce sizes and makes a huge difference if size if a consideration. Great article.