CI/CD gibi süreçlerimizi hızlandırmak için Docker Image boyutlarımızı minimize etmek önemlidir. Bu yazımızda basit bir Go uygulaması yazıp, hem Go’yu hem de Docker’ı optimize ederek 4 adımda Image boyutumuzu neredeyse 1GB’dan 1 MB’a kadar düşüreceğiz.


Ön Hazırlık

package main

import (
  "fmt"
  "net/http"
)

func main() {
  http.HandleFunc("/", func (w http.ResponseWriter, _ *http.Request) {
    fmt.Fprintf(w, "Hello World")
  })
  http.ListenAndServe(":80", nil)
}

Burada basit bir HTTP server başlatan 10 satırlık Go programı yazdık. Şimdi bunu adım adım Container haline getirelim.


Adım 1 - Yeni Bir Başlangıç

Öncelikle yazabileceğimiz en kötü Dockerfile’ı yazalım. Burada kısaca golang 1.16 image’ını kullanıyoruz ve uygulamamızı build edip entrypoint olarak belirtiyoruz.

FROM golang:1.16

WORKDIR /app
COPY . .

RUN go build -o /bin/blog-app

ENTRYPOINT ["/bin/blog-app"]

Build Sonucu Image Boyutu: 868 MB

Bu iyi bir image değil, boyutu gereksiz büyük. Bunu optimize etmeye başlayalım.


Adım 2 - Minimalizm Bazen İyidir

Şimdi yazdığımız bu Dockerfile’ı biraz daha optimize edelim ve alpine image’ı kullanalım. Bilmiyorsanız Alpine, image’ın boyutunu düşürmek için biçilmiş bir kaftandır, çünkü içerisinde temel os runtime’ları dışında pek bir binary yoktur. Buradan üstteki yaptıklarımızdan farklı olarak sadece ana image’ımızı Alpine olarak değiştiriyoruz.

FROM golang:1.16-alpine

WORKDIR /app
COPY . .

RUN go build -o /bin/blog-app

ENTRYPOINT ["/bin/blog-app"]

Build Sonucu Image Boyutu: 308 MB ~(-500MB)

Gördüğünüz gibi Alpine image’ına geçerek yaklaşık 500 MB tasarruf sağladık.


Adım 3 - Multi-Stage Build

Go’nun güzel özelliklerinden biride static build alabilmenizdir. Yani Go programınızın çalışması için libc gibi bağımlılıklara ihtiyaç duymadan çalışabilir. Burada artık multi-stage build dediğimiz yapıya geçiyoruz. Yani build’imizi Alpine image’ımızda yapıp, aldığımız build’i scratch dediğimiz boş bir image içerisine kopyalayıp orada çalıştırıyoruz.

### Stage 1
FROM golang:1.16-alpine AS builder

WORKDIR /app
COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -o /app/blog-app

### Stage 2
FROM scratch
COPY --from=builder /app/blog-app /bin/blog-app
ENTRYPOINT ["/bin/blog-app"]

Build Sonucu Image Boyutu: 4.42 MB ~(-300MB)

Burada ise multi-stage build ile boş bir image’a geçerek 300 MB tasarruf sağlamış olduk. Yani sadece build ettiğimiz binary’nin boyutu kalıyor geriye. Hadi bunuda optimize edelim.


Adım 4 - UPX

Artık son adıma geldik. Burada ise UPX kullanarak build ettiğimiz static binary’i daha da minify edelim.

### Stage 1
FROM golang:1.16-alpine AS builder

WORKDIR /app
COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-s -w" -o /app/blog-app

RUN apk add upx
RUN upx --ultra-brute /app/blog-app

### Stage 2
FROM scratch
COPY --from=builder /app/blog-app /bin/blog-app
ENTRYPOINT ["/bin/blog-app"]

Build Sonucu Image Boyutu: 1.32 MB ~(-3MB)

Binary’mizide minify ettik ve 3 MB bir tasarruf sağlayarak image boyutumuzu 1 MB’a kadar indirmiş olduk. Unutmayın bu adımdaki boyut değişimi, sizin programızında ne kadar paket kullandığınıza ve ne kadar kod bulunduğa bağlı olarak değişebilir. Yani go build’i 100 MB çıkıyor ise UPX ile bunu 30 MB’a düşürebilirsiniz.


Sonuç

REPOSITORYTAGIMAGE IDSIZE
blogstep-1d96961d177b7868MB
blogstep-27c874d66fd2d308MB
blogstep-392e68959939e4.42MB
blogstep-40a753fc54c4c1.32MB

Günün sonunda yukarıdaki gibi bir tablo karşımıza çıkıyor. Optimizasyonlar ile birlikte image boyutumuzu 868 MB‘dan 1.32 MB‘a düşürmüş oluyoruz. Tabiki burada unutmamız gereken bazı maddeler var.

  • Image boyutu programınızın performansını etkilemez, ama bir çok süreci hızlandırabilir.
  • Eğer programınızın os veya paket bağımlılıkları varsa bunları Dockerfile’a eklemeniz gerekir ve boyutu küçülteyim derken anlaması zor complex bir Dockerfile üretebilirsiniz.
  • UPX her durumda gerekli olmayabilir, burada dikkat etmeniz gereken nokta sizin ne kadarlık bir boyuta kadar bunu tolere edebileceğiniz.

Umarım bu yazı sizin için faydalı olmuştur, bir sonraki yazılarda görüşmek üzere.