Python uygulamaları için Docker imaj boyutunu küçültmek

Selamlar, bu yazımda Python uygulamalarının Docker imaj boyutunu nasıl küçülteceğimize bakacağız. Konu basit görünüyor ama detaya inince hangi tekniğin gerçekten kazandırdığını, hangisinin sadece kâğıt üstünde iyi durduğunu ayırt etmek lazım. Lafı çok uzatmadan başlayalım.

Bir gün baktığınızda küçücük bir Flask API'nin imajı 1.1GB olmuş oluyor. İşin tuhafı bu boyutun büyük kısmı uygulamanızın çalışırken hiç dokunmadığı şeyler: C derleyicisi, header dosyaları, pip cache'leri, kullanılmayan sistem paketleri. Küçük imaj demek hızlı deploy, autoscaling sırasında hızlı pull ve saldırgana daha az yüzey demek. Yani sadece estetik değil.

Şişkin başlangıç noktası

Çoğumuzun ilk yazdığı Dockerfile aşağı yukarı şudur:

FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

python:3.12 base image'ı tek başına yaklaşık 1GB. İçinde tam Debian, gcc, Python development header'ları, bir sürü utility var. Bunların büyük kısmına ihtiyacınız yok.

Adım 1: python:slim'e geçin

Slim varyantı derleyiciyi ve dev paketlerini atar. Tek satır değişiklikle ciddi kazanç:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

--no-cache-dir pip'in indirdiği paketleri cache'lemesini engelliyor; bu da imaj içinde gereksiz kopya bırakmıyor. Sırf bu adımla 1GB civarından 280MB'a inebilirsiniz.

Adım 2: Multi-stage build

Bazı paketler (cryptography, pandas, lxml, psycopg2) kurulurken C derleyicisi ister. Derleyiciyi production imajında tutmaya gerek yok. Builder stage'de derleyip, sadece kurulan paketleri production'a kopyalıyoruz:

FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc g++ libpq-dev libffi-dev && rm -rf /var/lib/apt/lists/*

WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 && rm -rf /var/lib/apt/lists/*

COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY . .

RUN useradd --create-home appuser
USER appuser

CMD ["python", "app.py"]

Buradaki kritik nokta /opt/venv numarası. Builder stage'de virtualenv'e kuruyoruz, sonra tek COPY --from=builder ile bütün ortamı production'a taşıyoruz. Production tarafında pip, setuptools, wheel zaten yok.

Adım 3: Alpine - en küçük imaj ama dikkat

Alpine inanılmaz küçük base image üretir. Ama glibc yerine musl libc kullanıyor; numpy, pandas, scipy gibi C extension'lı paketlerle bazen sıkıntı çıkar. Tecrübeyle söyleyebilirim ki saf Python'a yakın projelerde Alpine harika, ama ağır bilim/veri kütüphaneleri varsa slim'de kalmak daha az baş ağrıtır.

FROM python:3.12-alpine AS builder
RUN apk add --no-cache gcc musl-dev libffi-dev postgresql-dev

WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-alpine
RUN apk add --no-cache libpq libffi
WORKDIR /app

COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .

USER nobody
CMD ["python", "app.py"]

Adım 4: .dockerignore unutulmasın

Build context'e gereksiz dosya gönderirseniz hem build yavaşlar hem de yanlışlıkla .env, .git gibi şeyler imaja sızabilir. Şu kadarcık bir dosya çok iş görür:

__pycache__
*.pyc
.git
.env
.env.*
.venv
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
dist
build
*.egg-info
tests
docs
*.md
.vscode
.idea
docker-compose*.yml
Dockerfile*

Sık karşılaşılan hatalar

  • Install ve temizliği ayrı RUN'larda yapmak: Docker layer'ları katmanlı çalışır, yani önceki layer'da yazılan dosyayı sonraki layer'da silseniz bile imaj boyutu küçülmez. apt-get install ile rm -rf /var/lib/apt/lists/* aynı RUN içinde olmalı.
  • Builder'da pip cache mount'u atlamak: BuildKit ile --mount=type=cache,target=/root/.cache/pip kullanırsanız tekrar build'lerde paketler yeniden indirilmez; imaja da sızmaz.
  • Slim üstünde -dev paketlerini bırakmak: Production'da libpq-dev değil libpq5 istiyorsunuz. -dev paketleri sadece compile zamanı için.
  • COPY . . ile her şeyi atmak: .dockerignore yoksa .git klasörü bile imaja girer. Bence en sık atlanan adım bu.
  • root kullanıcı bırakmak: Boyutla ilgili değil ama production'a gidecek imajda USER appuser veya USER nobody olsun. Bir alışkanlık meselesi.

Tam production Dockerfile

Hepsini birleştiren, sahada kullanabileceğiniz hal:

# syntax=docker/dockerfile:1

FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc libpq-dev && rm -rf /var/lib/apt/lists/*

WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

FROM python:3.12-slim
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 curl && \
    rm -rf /var/lib/apt/lists/* && \
    useradd --create-home appuser

COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

COPY . .

USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:create_app()", "--bind", "0.0.0.0:8000", "--workers", "4"]

Kapanış

Bu yazıda Python imajını küçültmek için slim base, multi-stage build, Alpine ve .dockerignore gibi tekniklere baktık. Şahsi kanaatim, çoğu proje için sweet spot slim + multi-stage; tek bu değişiklikle imaj boyutu yüzde sekseni rahat düşer. Alpine'ı sadece bağımlılıklarınızın musl'da temiz derlendiğinden eminseniz seçin, yoksa kazandığınız megabayttan çok zaman kaybedersiniz. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.