Merhaba! Önceki dersimizde, PySpark ETL uygulamamız için tüm bağımlılıkları ve kodları içeren fonksiyonel bir Dockerfile oluşturmuştuk. Bu, uygulamamızı taşınabilir hale getirmek için attığımız ilk ve önemli bir adımdı.
Ancak bu yaklaşımın bir dezavantajı vardır: Oluşturduğumuz imaj, uygulamanın sadece çalışması için gerekenlerden çok daha fazlasını içerebilir. Derleme araçları, geçici dosyalar ve sadece geliştirme sırasında ihtiyaç duyulan paketler, son imajın boyutunu gereksiz yere artırır. Bu durum, depolama maliyetlerini yükseltir, CI/CD süreçlerinde imajların transferini (push/pull) yavaşlatır ve potansiyel güvenlik zafiyetleri için daha geniş bir yüzey alanı oluşturur.
Bugünkü Öğrenme Hedefimiz: Çok aşamalı Docker yapılarını (multi-stage builds) kullanarak daha küçük, daha güvenli ve üretim için optimize edilmiş imajlar oluşturmak.
Bu ders, CI/CD işlem hattınızın verimliliği için kritik bir optimizasyon tekniğini ele alacak. Daha küçük imajlar, daha hızlı dağıtım anlamına gelir, bu da çevik geliştirme döngüleri için hayati önem taşır.
Çok Aşamalı Yapılar (Multi-stage Builds) Nedir?
Çok aşamalı yapılar, tek bir Dockerfile içinde birden fazla FROM talimatı kullanarak imaj oluşturma sürecini farklı aşamalara ayırmanıza olanak tanır. Her FROM talimatı yeni bir aşama başlatır ve önceki aşamaları atarak temiz bir başlangıç yapar.
Bu tekniğin arkasındaki ana fikir şudur:
- "İnşaatçı" (Builder) Aşaması: Bu ilk aşamada, uygulamanızı ve bağımlılıklarınızı derlemek/kurmak için gereken tüm araçları (örneğin,
build-essential,python3-dev,maven) içeren daha büyük bir temel imaj kullanırsınız. Python paketlerini bir sanal ortam (virtual environment) içine kurarsınız. - "Üretim" (Production) Aşaması: Bu son aşamada, çok daha küçük ve minimal bir temel imajla (örneğin,
python-slimveya bizim durumumuzdaapache/spark-py) başlarsınız. Ardından, "inşaatçı" aşamasından sadece ve sadece uygulamanın çalışması için gerekli olan dosyaları (örneğin, oluşturulmuş sanal ortam ve uygulama kodunuz) kopyalarsınız.
Bu, tüm derleme araçlarını, geçici dosyaları ve gereksiz "artıkları" son imajın dışında bırakmanızı sağlar.

Pratikte Çok Aşamalı Yapılar: Python Örneği
Bu konseptin gücünü anlamak için, genel bir Python uygulaması üzerindeki etkisini görelim.
This Is How You Write an Efficient Python Dockerfile
ArjanCodes'un bu videosu, çok aşamalı yapıların arkasındaki mantığı ve pratikteki faydasını mükemmel bir şekilde gösteriyor. Standart bir Python uygulamasının imaj boyutunu önemli ölçüde nasıl küçülttüğüne odaklanacağız.
Lütfen videonun 09:13 - 11:42 arasındaki bölümünü izleyin. Şu noktalara odaklanın: Build aşaması (builder): Bağımlılıkları kurmak için kullanılan geçici aşama. Production aşaması: Sadece uygulamanın çalışması için gerekenleri içeren son, temiz aşama. COPY --from=builder ... komutunun iki aşama arasında dosya (bu örnekte sanal ortam - venv) kopyalamak için nasıl kullanıldığı. Bu tekniğin imaj boyutunu ne kadar dramatik bir şekilde azalttığı.
Gördüğünüz gibi, imaj boyutundaki azalma oldukça etkileyici. Bu sadece bir başlangıç. Modern araçlar ve daha modüler tasarımlarla bu süreci daha da optimize edebiliriz.
Aşağıdaki video, uv gibi modern bir paket yöneticisi kullanarak hem geliştirme hem de üretim için optimize edilmiş aşamalar içeren daha gelişmiş bir Dockerfile yapısını gösteriyor.
Stop Building 2GB Python Containers
Timnology'nin bu videosu, uv gibi modern bir araç kullanarak daha da modüler ve optimize edilmiş çok aşamalı bir yapı sunuyor. Bu yaklaşım, geliştirme (dev) ve üretim (prod) için ayrı hedefler oluşturmanıza olanak tanır.
Lütfen videonun 01:38 - 08:14 arasındaki bölümünü izleyin. Şu konulara odaklanın: python-base aşaması: Temel Python ortamını ve optimizasyonları tanımlar. builder aşaması: Sadece üretim (runtime) bağımlılıklarını kurar. production aşaması: builder'dan sanal ortamı kopyalar, uygulama kodunu ekler ve root olmayan bir kullanıcı ayarlar. Geliştirme araçlarının son imaja dahil edilmediğine dikkat edin. docker build --target <stage_name> komutuyla belirli bir aşamanın nasıl hedeflenebildiği ve bu sayede geliştirme ve üretim imajlarının aynı Dockerfile'dan nasıl oluşturulduğu.
PySpark Projemiz için Çok Aşamalı Dockerfile
Şimdi bu öğrendiklerimizi kendi PySpark projemize uygulayalım. Önceki dersteki Dockerfile'ımızı çok aşamalı bir yapıya dönüştüreceğiz.
Stratejimiz:
builderAşaması: Genel birpython:3.11-slimimajı kullanarak başlayacağız. Bu aşamada,requirements.txtdosyasındaki tüm Python bağımlılıklarını/opt/venvadında bir sanal ortama kuracağız.- Son Aşama: Önceki derste kullandığımız
apache/spark-pyimajı ile başlayacağız çünkü bu imaj Java ve Spark'ı bizim için hazır olarak sunuyor. Daha sonra,builderaşamasından/opt/venvsanal ortamını ve yerel makinemizden uygulama kodumuzu (srcklasörü) bu son imajın içine kopyalayacağız.
Projenizdeki Dockerfile'ı açın ve içeriğini aşağıdakiyle değiştirin:
# ==============================================================================
# AŞAMA 1: Builder - Python bağımlılıklarını kurmak için.
# ==============================================================================
FROM python:3.11-slim as builder
# Çalışma dizinini ayarla
WORKDIR /app
# Sanal ortam için ortam değişkeni
ENV VENV_PATH=/opt/venv
# Sanal ortamı oluştur
RUN python3 -m venv $VENV_PATH
# PATH'i güncelle, böylece venv'deki `pip` kullanılır
ENV PATH="$VENV_PATH/bin:$PATH"
# requirements.txt dosyasını kopyala
COPY requirements.txt .
# Bağımlılıkları kur.
# --no-cache-dir imaj boyutunu küçültür.
RUN pip install --no-cache-dir -r requirements.txt
# ==============================================================================
# AŞAMA 2: Final - Üretim imajı.
# ==============================================================================
# Java ve Spark'ın kurulu olduğu resmi PySpark imajını kullan
FROM apache/spark-py:v3.5.1
# Çalışma dizinini ayarla
WORKDIR /app
# Builder aşamasından sanal ortamı kopyala
COPY --from=builder /opt/venv /opt/venv
# Uygulama kaynak kodunu kopyala
COPY src/ /app/src/
# (İsteğe Bağlı) Harici JAR dosyalarını ekle
# Örneğin bir PostgreSQL sürücüsü eklemek için:
# RUN wget https://jdbc.postgresql.org/download/postgresql-42.7.3.jar -P /opt/spark/jars/
# Ortam değişkenlerini ayarla
# SPARK_HOME, temel imajda zaten ayarlanmıştır.
# PATH'e venv'i ekleyerek, konteyner başladığında
# kurduğumuz kütüphanelerin bulunmasını sağla.
ENV PATH="/opt/venv/bin:$PATH"
# Güvenlik için Spark'ın varsayılan non-root kullanıcısına geç
USER 185
Bu yeni Dockerfile ile docker build komutunu çalıştırdığınızda, Docker önce builder aşamasını çalıştıracak, sanal ortamı oluşturacak ve ardından apache/spark-py temel imajı üzerine bu sanal ortamı ve kodunuzu kopyalayarak son, daha küçük bir imaj üretecektir. Derleme için kullanılan python:3.11-slim imajının kendisi ve içindeki hiçbir araç (sanal ortam hariç) son imaja dahil edilmez.
Daha derinlemesine ve üretim odaklı bir bakış açısı için, aşağıdaki makale, gördüğümüz konseptleri pekiştiren harika bir kaynaktır.
Production-ready Python Docker Containers with uv
Bu makale, uv kullanarak üretim ortamı için hazır, sağlam ve optimize edilmiş Docker imajları oluşturmanın arkasındaki ilkeleri ve en iyi pratikleri derinlemesine inceliyor. Gördüğümüz video örneklerindeki konseptleri pekiştirmek için harika bir kaynaktır.
Makalenin başından itibaren "runtime" aşamasını tanımlayan kod bloğunun sonuna kadar okuyun. Şu temel ilkelere odaklanın: Multi-stage builds: Derleme araçlarını son imajdan ayırmanın önemi. Judicious layering: Bağımlılıkların ve uygulama kodunun ayrı katmanlarda tutularak Docker'ın önbellek mekanizmasından nasıl en iyi şekilde yararlanıldığı. build aşamasında, uv sync --no-install-project kullanarak önce sadece bağımlılıkların kurulup, daha sonra ayrı bir RUN adımında uygulama kodunun kurulmasının katman önbelleğini nasıl optimize ettiği. runtime aşamasının ne kadar minimal olduğu ve build aşamasından sadece önceden oluşturulmuş sanal ortamı (/app) nasıl kopyaladığı.
Sonuç
Bu derste, Docker imajlarımızı optimize etmek için güçlü bir teknik olan çok aşamalı yapıları öğrendik. Bu yöntem, son imajlarımızdan gereksiz derleme araçlarını ve bağımlılıkları çıkararak onları daha küçük, daha güvenli ve daha hızlı hale getirmemizi sağlar.
Temel Çıkarımlar:
- Çok aşamalı yapılar, tek bir
Dockerfileiçinde birden fazlaFROMkullanarak imaj oluşturma sürecini mantıksal aşamalara ayırır. - Bir
builderaşaması, bağımlılıkları kurmak için kullanılırken, sonproductionaşaması sadece çalışma zamanı için gerekli olanları içerir. COPY --from=<stage_name>komutu, aşamalar arasında dosya ve dizinleri kopyalamanın anahtarıdır.- Bu optimizasyon, CI/CD işlem hatlarında imaj transfer sürelerini kısaltır ve depolama maliyetlerini düşürür.
Gelecek Dersin Önizlemesi:
Artık optimize edilmiş bir Docker imajı oluşturmak için bir Dockerfile'ımız var. Ancak bu imajın gerçekten beklendiği gibi çalışıp çalışmadığını nasıl anlarız? Bir sonraki dersimizde, oluşturduğumuz bu konteynerleştirilmiş PySpark işini docker run komutunu kullanarak yerel olarak nasıl çalıştıracağımızı ve doğrulayacağımızı öğreneceğiz. Bu, imajı bir konteyner kayıt defterine (container registry) göndermeden önce yapacağımız önemli bir "sağlamlık kontrolü" olacaktır.