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

Go: Docker Multistage Build

February 08, 2025

สวัสดีชาว Gopher ทุกท่าน ในปัจจุบันการใช้ Container สำหรับพัฒนาแอปอย่าง Docker เป็นที่นิยมอย่างมาก เนื่องจากช่วยในการจัดการ environment และ library ให้เหมือนกันได้ไม่ว่าจะอยู่ที่ไหนก็ตาม

อย่างไรก็ดี ปัญหาการ "บวม" (Docker image bloating) หรือก็คือ image มีขนาดใหญ่เกินความจำเป็นนั้น สามารถเกิดขึ้นได้ถ้าไม่จัดการให้ดี ทางออกอื่นๆ ที่ช่วยได้ เช่น

  • ใช้ .dockerignore เพื่อไม่ให้เพิ่มไฟล์ที่ไม่ต้องการ
  • เลือกใช้ Distroless/Minimal base image เพื่อลดขนาดให้ได้มากที่สุด

ต้องเข้าใจว่าส่วนที่ใช้ทำในตอน build แอปพลิเคชันนั้น พอเสร็จแล้วมักจะไม่ถูกนำมาใช้ต่อตอนที่แอปกำลังรัน ถ้าเรานำทั้งหมดมาใช้ก็จะทำให้บวมได้ ในบทความนี้จะนำเสนออีกวิธีที่จะแยกส่วนที่เป็น build tool ออก ให้เหลือเฉพาะส่วนที่จำเป็นต่อการรันแอปนั่นเอง เรียกว่า Multi-stage builds

การใช้ Multi-stage builds จะช่วยลดขนาดของ container ให้เล็กลงได้ ด้วยการแตกขั้นตอนออกเป็นหลายๆ stage ย่อย แต่ละ stage จะส่งผลลัพธ์ต่อไปยัง stage ถัดไป

Demo

เราสามารถเขียน Go application ด้วย Gin framework ดังนี้

main.go
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
}

ในการเปรียบเทียบเพื่อให้เห็นภาพชัด จะทดสอบการ build ทั้งสองรูปแบบ

ใช้ Dockerfile ในการ build แบบปกติ

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" ]

จากนั้นลอง build ด้วยคำสั่ง docker build -t my-go-app .

ใช้ Multi-stage builds

เราจะแยกการ build แอปพลิเคชันออกเป็นสอง stage คือ build stage และ release stage ดังนี้

Dockerfile.multistage
# 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" ]

คราวนี้ลอง build ด้วยคำสั่ง docker build -t my-go-app:multistage -f Dockerfile.multistage . เมื่อ build เรียบร้อยทั้งสองรูปแบบ มาดูผลลัพธ์กันดีกว่า

$ 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

จะเห็นว่าการใช้ Multi-stage builds ช่วยลดขนาดให้ image ของเราอย่างมหาศาลเทียบกับแบบปกติ พอ image เล็กแล้วก็สามารถนำไปใช้ต่อได้ง่ายมากขึ้นและใช้พื้นที่ลดลงนั่นเอง

Reference: