Merhaba arkadaşlar, bugün sizlere özellikle Go, Java veya Node.js gibi derlenen/bağımlılık yönetimi yapılan uygulamalarınız için Docker imaj boyutlarınızı ciddi anlamda küçültecek, güvenliği artıracak ve en önemlisi "best practice" olan bir yöntemi anlatacağım: Multi-Stage Build.
Benim de sunucularda sıkça kullandığım bu yöntem, tek bir Dockerfile içinde birden fazla `FROM` ifadesi kullanarak, derleme işlemlerini bir "stage" (aşama) içinde yapıp, sadece çalıştırılabilir dosyayı veya runtime'ı içeren temiz bir son stage'e kopyalamamızı sağlıyor. Böylece derleme araçları, geçici dosyalar ve gereksiz bağımlılıklar nihai imajımızda yer almıyor. Hadi adım adım inceleyelim.
Multi-Stage Build'in Avantajları Neler?
Öncelikle neden böyle bir şeye ihtiyaç duyuyoruz onu konuşalım. Klasik bir Dockerfile ile uygulamanızı derleyip imaj oluşturduğunuzda, derleyici (gcc, go compiler, maven), tüm kütüphaneler ve ara dosyalar da imajın içinde kalır. Bu da;
İmaj boyutunun gereksiz yere büyümesine (bazen 1GB+),
Daha yavaş image pull/push sürelerine,
Potansiyel güvenlik açıklarına (derleyicideki bir zafiyet saldırı yüzeyini genişletir),
Daha fazla disk ve bellek kullanımına sebep olur.
Multi-Stage ile bu sorunların hepsini minimize ediyoruz. İmaj boyutlarında %80-90'a varan küçülmeler görmek mümkün.
Temel Multi-Stage Build Yapısı
En basit haliyle iki aşamalı bir Dockerfile şöyle görünür. İlk aşama (genelde `builder` olarak isimlendirilir) derleme işini yapar. İkinci aşama ise nihai, temiz çalıştırma ortamımızdır.
Gördüğünüz gibi, ilk aşamada `golang:1.21-alpine` gibi büyük bir imaj kullandık. Ancak nihai imajımız sadece `alpine:latest` ve içine kopyaladığımız binary dosyası. `COPY --from=builder` komutu bu sihrin anahtarı. Bir önceki stage'den sadece ihtiyacımız olan dosyayı alıyoruz.
Farklı Senaryolar ve Optimizasyonlar
Teknik sadece Go için değil, tüm dillerde işe yarar. İşte bir Node.js örneği:
Burada dikkat etmemiz gereken nokta, `npm ci --only=production` ile sadece production bağımlılıklarını kuruyoruz. `devDependencies` nihai imaja girmiyor.
Dikkat Edilmesi Gerekenler
Stage İsimlendirme: `AS builder` gibi isimler verirseniz, `--from=builder` ile referans vermek daha kolay ve okunabilir olur. Sayısal referans (`--from=0`) da kullanabilirsiniz ama isimlendirmek daha iyidir.
Docker Build Cache: Stage'ler birbirinden bağımsız cache'lenir. `COPY . .` gibi bir komutu mümkün olduğunca geç yapın ki dependency'ler değişmediğinde cache kırılmasın. Örnekte önce `go.mod` ve `go.sum`'ı kopyalayıp modülleri indirmemiz bu yüzden.
Çoklu Stage Kullanımı: İki stage ile sınırlı değilsiniz. Test aşaması, farklı binary'leri birleştirme gibi kompleks işler için üç veya daha fazla stage kullanabilirsiniz.
COPY --from ile Harici İmajlar: Sadece kendi stage'lerinizden değil, harici bir Docker imajından da dosya kopyalayabilirsiniz. Örneğin: `COPY --from=nginx:alpine /etc/nginx/nginx.conf /nginx.conf`
İmaj Boyutu Karşılaştırması
İşin somut faydasını görelim. Yukarıdaki Go örneğinde:
Tek Stage (geleneksel): `golang:1.21-alpine` imajı + binary ≈ 350MB
Multi-Stage: `alpine:latest` + binary ≈ 10-15MB
Aradaki fark devasa! Bu, container'ınızın daha hızlı başlaması, registry'de daha az depolama alanı ve network bandı kullanması demek.
Sonuç olarak arkadaşlar, eğer Docker imajlarınız şişmeye başladıysa, güvenlik taramasında gereksiz paketler çıkıyorsa veya deploy süreleriniz uzuyorsa, ilk bakmanız gereken noktalardan biri Multi-Stage Build'e geçiş yapmak olmalı.
Peki siz bu konfigürasyonu kendi sunucularınızda nasıl yapıyorsunuz? Özellikle Python veya Java gibi dillerde farklı püf noktalarınız var mı? Deneyimlerinizi ve sorularınızı aşağıya yazmaktan çekinmeyin. Hep birlikte öğrenelim.
Benim de sunucularda sıkça kullandığım bu yöntem, tek bir Dockerfile içinde birden fazla `FROM` ifadesi kullanarak, derleme işlemlerini bir "stage" (aşama) içinde yapıp, sadece çalıştırılabilir dosyayı veya runtime'ı içeren temiz bir son stage'e kopyalamamızı sağlıyor. Böylece derleme araçları, geçici dosyalar ve gereksiz bağımlılıklar nihai imajımızda yer almıyor. Hadi adım adım inceleyelim.
Öncelikle neden böyle bir şeye ihtiyaç duyuyoruz onu konuşalım. Klasik bir Dockerfile ile uygulamanızı derleyip imaj oluşturduğunuzda, derleyici (gcc, go compiler, maven), tüm kütüphaneler ve ara dosyalar da imajın içinde kalır. Bu da;
İmaj boyutunun gereksiz yere büyümesine (bazen 1GB+),
Daha yavaş image pull/push sürelerine,
Potansiyel güvenlik açıklarına (derleyicideki bir zafiyet saldırı yüzeyini genişletir),
Daha fazla disk ve bellek kullanımına sebep olur.
Multi-Stage ile bu sorunların hepsini minimize ediyoruz. İmaj boyutlarında %80-90'a varan küçülmeler görmek mümkün.
En basit haliyle iki aşamalı bir Dockerfile şöyle görünür. İlk aşama (genelde `builder` olarak isimlendirilir) derleme işini yapar. İkinci aşama ise nihai, temiz çalıştırma ortamımızdır.
Kod:
# STAGE 1: Derleme Aşaması (Builder)
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /uygulamam
# STAGE 2: Çalıştırma Aşaması (Nihai İmaj)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Sadece derlenmiş binary'yi bir önceki aşamadan kopyala
COPY --from=builder /uygulamam .
EXPOSE 8080
CMD ["./uygulamam"]
Gördüğünüz gibi, ilk aşamada `golang:1.21-alpine` gibi büyük bir imaj kullandık. Ancak nihai imajımız sadece `alpine:latest` ve içine kopyaladığımız binary dosyası. `COPY --from=builder` komutu bu sihrin anahtarı. Bir önceki stage'den sadece ihtiyacımız olan dosyayı alıyoruz.
Teknik sadece Go için değil, tüm dillerde işe yarar. İşte bir Node.js örneği:
Kod:
# STAGE 1: Bağımlılıkları Kur ve Derle
FROM node:20 AS builder
WORKDIR /app
COPY package.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# STAGE 2: Sadece Production Gereksinimleri
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
Burada dikkat etmemiz gereken nokta, `npm ci --only=production` ile sadece production bağımlılıklarını kuruyoruz. `devDependencies` nihai imaja girmiyor.
Stage İsimlendirme: `AS builder` gibi isimler verirseniz, `--from=builder` ile referans vermek daha kolay ve okunabilir olur. Sayısal referans (`--from=0`) da kullanabilirsiniz ama isimlendirmek daha iyidir.
Docker Build Cache: Stage'ler birbirinden bağımsız cache'lenir. `COPY . .` gibi bir komutu mümkün olduğunca geç yapın ki dependency'ler değişmediğinde cache kırılmasın. Örnekte önce `go.mod` ve `go.sum`'ı kopyalayıp modülleri indirmemiz bu yüzden.
Çoklu Stage Kullanımı: İki stage ile sınırlı değilsiniz. Test aşaması, farklı binary'leri birleştirme gibi kompleks işler için üç veya daha fazla stage kullanabilirsiniz.
COPY --from ile Harici İmajlar: Sadece kendi stage'lerinizden değil, harici bir Docker imajından da dosya kopyalayabilirsiniz. Örneğin: `COPY --from=nginx:alpine /etc/nginx/nginx.conf /nginx.conf`
İşin somut faydasını görelim. Yukarıdaki Go örneğinde:
Tek Stage (geleneksel): `golang:1.21-alpine` imajı + binary ≈ 350MB
Multi-Stage: `alpine:latest` + binary ≈ 10-15MB
Aradaki fark devasa! Bu, container'ınızın daha hızlı başlaması, registry'de daha az depolama alanı ve network bandı kullanması demek.
Sonuç olarak arkadaşlar, eğer Docker imajlarınız şişmeye başladıysa, güvenlik taramasında gereksiz paketler çıkıyorsa veya deploy süreleriniz uzuyorsa, ilk bakmanız gereken noktalardan biri Multi-Stage Build'e geçiş yapmak olmalı.
Peki siz bu konfigürasyonu kendi sunucularınızda nasıl yapıyorsunuz? Özellikle Python veya Java gibi dillerde farklı püf noktalarınız var mı? Deneyimlerinizi ve sorularınızı aşağıya yazmaktan çekinmeyin. Hep birlikte öğrenelim.