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

Go: Docker Multistage Build

February 08, 2025
Go: Docker Multistage Build

สวัสดีชาว 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: