Go: Docker Multistage Build

February 08, 2025
Go: Docker Multistage Build

Hello Gophers! Nowadays, using containers like Docker for application development is very popular because it helps manage environments and libraries consistently, no matter where you are. However, the issue of "bloating" (Docker image bloating), where the image size becomes unnecessarily large, can occur if not managed properly. Some solutions to this problem include:

  • Using .dockerignore to exclude unnecessary files.
  • Choosing Distroless/Minimal base images to minimize size.

It's important to understand that the tools used during the build process are often not needed when the application is running. Including everything in the final image can lead to bloating. This article introduces another method to separate build tools, leaving only what is necessary for running the application. This method is called Multi-stage builds.

Using Multi-stage builds helps reduce the size of the container by breaking the process into multiple smaller stages, where each stage passes its output to the next.

Demo

We can write a Go application using the Gin framework.

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}

To clearly demonstrate the difference, we will test building the application using two approaches.

Building with a Regular Dockerfile

FROM golang:1.23-alpine

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY  *.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /api

EXPOSE 8080

CMD [ "/api" ]

Then, build the image using the command docker build -t my-go-app ..

Building with Multi-stage Builds

We will separate the application build process into two stages: the build stage and the release stage, as follows:

# Build stage
FROM golang:1.23-alpine AS build

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY  *.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /api

# Final stage
FROM gcr.io/distroless/base-debian11 AS release

WORKDIR /

COPY --from=build /api /api

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT [ "/api" ]

Now, build the image using command docker build -t my-go-app:multistage -f Dockerfile.multistage .. Once both images are built, let's compare the results:

$ docker images                                              
REPOSITORY          TAG             IMAGE ID       CREATED          SIZE
my-go-app           multistage      918a32cac56d   51 seconds ago   31.8MB
my-go-app           latest          528bb389b257   5 minutes ago    622MB

As you can see, using Multi-stage builds significantly reduces the image size compared to the regular approach. A smaller image is easier to use and takes up less space.

Reference:

https://nattrio.dev/posts/feed.xml