Docker ile Python Uygulaması Containerize Etme

Production ortamında “bende çalışıyor ama sunucuda çalışmıyor” problemini yaşamayan sysadmin neredeyse yoktur. Python uygulamaları özellikle bağımlılık yönetimi konusunda bu sorunu sık yaşatır. Bir uygulamada Python 3.8, diğerinde Python 3.11 lazım; birinde NumPy’ın eski versiyonu, diğerinde yenisi gerekiyor. Docker tam olarak bu kaosa çözüm getiriyor. Uygulamayı tüm bağımlılıklarıyla birlikte paketleyip her ortamda aynı şekilde çalışmasını sağlıyor.

Bu yazıda gerçek bir Python web uygulamasını sıfırdan Docker container’ına taşıyacağız. Flask API’den başlayıp production-ready bir yapıya kadar adım adım gideceğiz.

Docker ve Python: Temel Kavramlar

Docker’ı bir nakliye konteyneri gibi düşünebilirsiniz. İçine ne koyarsanız koyun, nereye gönderirseniz gönderin, aynı şekilde ulaşır. Python uygulamanız için bu şu anlama geliyor: geliştirme ortamınızda çalışan uygulama, staging’de ve production’da birebir aynı şekilde çalışır.

Python özelinde Docker kullanmanın getirdiği avantajlar somuttur:

  • İzolasyon: Her uygulama kendi Python versiyonu ve bağımlılıklarıyla çalışır, sistem Python’ına dokunmaz
  • Tekrarlanabilirlik: requirements.txt artık yeterli değil, tüm sistem bağımlılıkları da paketleniyor
  • Ölçeklenebilirlik: Kubernetes veya Docker Swarm ile kolayca yatay ölçekleme yapılabiliyor
  • CI/CD entegrasyonu: Pipeline’larda build, test ve deploy süreçleri standardize oluyor

Örnek Proje Yapısı

Gerçekçi bir senaryo için basit bir Flask REST API kullanalım. Bu API bir SQLite veritabanıyla çalışıyor ve birkaç endpoint sunuyor. Proje yapısı şu şekilde:

flask-api/
├── app/
│   ├── __init__.py
│   ├── routes.py
│   └── models.py
├── requirements.txt
├── config.py
├── run.py
├── Dockerfile
├── docker-compose.yml
└── .dockerignore

Önce basit Flask uygulamamızı yazalım:

# run.py
cat > run.py << 'EOF'
from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)
EOF
# app/__init__.py
cat > app/__init__.py << 'EOF'
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.db'
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SECRET_KEY'] = 'dev-secret-key'
    
    db.init_app(app)
    
    from .routes import main
    app.register_blueprint(main)
    
    with app.app_context():
        db.create_all()
    
    return app
EOF

requirements.txt dosyamız da şu şekilde:

cat > requirements.txt << 'EOF'
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
SQLAlchemy==2.0.23
gunicorn==21.2.0
python-dotenv==1.0.0
EOF

İlk Dockerfile: Temel Yapı

Dockerfile yazarken en çok yapılan hata, her şeyi tek bir RUN komutuna yığmak ya da tam tersi, gereksiz yere katman çoğaltmak. Katman optimizasyonu hem image boyutunu küçültür hem de build sürelerini kısaltır.

cat > Dockerfile << 'EOF'
# Python 3.11 slim imajı kullanıyoruz, alpine daha küçük ama derleme sorunları çıkarabilir
FROM python:3.11-slim

# Çalışma dizinini belirle
WORKDIR /app

# Sistem bağımlılıklarını kur (varsa)
RUN apt-get update && apt-get install -y 
    gcc 
    && rm -rf /var/lib/apt/lists/*

# Önce sadece requirements.txt kopyala
# Bu sayede bağımlılıklar değişmediğinde cache kullanılır
COPY requirements.txt .

# Python bağımlılıklarını yükle
RUN pip install --no-cache-dir --upgrade pip && 
    pip install --no-cache-dir -r requirements.txt

# Uygulama kodunu kopyala
COPY . .

# Port tanımla (dokümantasyon amaçlı)
EXPOSE 5000

# Uygulamayı gunicorn ile başlat
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "run:app"]
EOF

Bu Dockerfile’da dikkat edilmesi gereken birkaç önemli nokta var. requirements.txt‘i uygulama kodundan önce kopyalamak kritik bir optimizasyon. Docker build sırasında her katman cache’lenir. Kod değiştiğinde sadece COPY . . adımından sonraki katmanlar yeniden build edilir, bağımlılıklar tekrar indirilmez. Büyük bir projede bu onlarca dakika kazandırabilir.

.dockerignore Dosyası

.gitignore ne kadar önemliyse .dockerignore da o kadar önemli. Bu dosya olmadan node_modules, __pycache__, .git gibi gereksiz dosyalar image’a dahil olur ve boyutu şişirir:

cat > .dockerignore << 'EOF'
# Python cache dosyaları
__pycache__/
*.pyc
*.pyo
*.pyd
.Python

# Sanal ortam
venv/
env/
.venv/

# Test dosyaları
.pytest_cache/
.coverage
htmlcov/

# IDE dosyaları
.vscode/
.idea/
*.swp

# Git
.git/
.gitignore

# Docker dosyaları (recursive build'i önle)
Dockerfile
docker-compose*.yml

# Ortam dosyaları (güvenlik!)
.env
.env.local
.env.production

# Log dosyaları
*.log
logs/

# Veritabanı dosyaları
*.db
*.sqlite3

# OS dosyaları
.DS_Store
Thumbs.db
EOF

.env dosyasını .dockerignore‘a eklemek güvenlik açısından zorunlu. Aksi takdirde API key’leriniz, veritabanı şifreleriniz image’a gömülür ve image’ı paylaşırsanız bu bilgiler sızar.

Image Build ve Çalıştırma

# Image oluştur
docker build -t flask-api:latest .

# Build sürecini verbose olarak takip et
docker build --progress=plain -t flask-api:latest .

# Container çalıştır
docker run -d 
    --name flask-api 
    -p 5000:5000 
    -e SECRET_KEY="production-secret-key" 
    flask-api:latest

# Logları takip et
docker logs -f flask-api

# Container içine gir (debug için)
docker exec -it flask-api /bin/bash

Uygulamayı test etmek için:

# Health check
curl http://localhost:5000/health

# API endpoint testi
curl -X POST http://localhost:5000/api/items 
    -H "Content-Type: application/json" 
    -d '{"name": "test item", "value": 42}'

Multi-Stage Build: Production için Küçük Image

Geliştirme ortamında derleme araçları, test kütüphaneleri gibi şeyler gerekiyor ama production image’ında bunlar gereksiz ve güvenlik riski oluşturuyor. Multi-stage build burada devreye giriyor:

cat > Dockerfile.production << 'EOF'
# === Build Stage ===
FROM python:3.11-slim AS builder

WORKDIR /app

# Derleme için gerekli sistem araçları
RUN apt-get update && apt-get install -y 
    gcc 
    g++ 
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

# Wheel dosyalarını oluştur (binary paketler için önemli)
RUN pip install --no-cache-dir --upgrade pip && 
    pip wheel --no-cache-dir --wheel-dir=/app/wheels -r requirements.txt

# === Production Stage ===
FROM python:3.11-slim AS production

# Güvenlik: root olmayan kullanıcı oluştur
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app

# Build stage'den sadece wheel dosyalarını al
COPY --from=builder /app/wheels /wheels

# Wheel'lardan kur (ağ bağlantısı gerekmez, daha hızlı)
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* 
    && rm -rf /wheels

# Uygulama kodunu kopyala
COPY --chown=appuser:appuser . .

# Root olmayan kullanıcıya geç
USER appuser

EXPOSE 5000

# Healthcheck ekle
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')"

CMD ["gunicorn", 
     "--bind", "0.0.0.0:5000", 
     "--workers", "4", 
     "--worker-class", "sync", 
     "--timeout", "120", 
     "--access-logfile", "-", 
     "--error-logfile", "-", 
     "run:app"]
EOF

Bu yaklaşımın faydaları somut. Tek stage ile oluşturulan image genellikle 400-500 MB civarında olurken, multi-stage build ile bu 150-200 MB’a kadar düşebiliyor. Daha küçük image, daha hızlı pull, daha az saldırı yüzeyi.

Docker Compose ile Tam Ortam Kurulumu

Gerçek dünya senaryolarında uygulama tek başına çalışmaz. Veritabanı, cache, mesaj kuyruğu gibi servislerle birlikte çalışır. Docker Compose bu servisleri bir arada yönetmeyi sağlar:

cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: flask-api
    restart: unless-stopped
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=production
      - DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb
      - REDIS_URL=redis://redis:6379/0
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - app-logs:/app/logs
    networks:
      - app-network

  db:
    image: postgres:15-alpine
    container_name: flask-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=apppass
      - POSTGRES_DB=appdb
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: flask-redis
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: flask-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - web
    networks:
      - app-network

volumes:
  postgres-data:
  redis-data:
  app-logs:

networks:
  app-network:
    driver: bridge
EOF

Compose ortamını yönetmek için kullanılan temel komutlar:

# Tüm servisleri başlat (arka planda)
docker-compose up -d

# Sadece belirli bir servisi rebuild edip başlat
docker-compose up -d --build web

# Tüm logları takip et
docker-compose logs -f

# Sadece web servisinin loglarını izle
docker-compose logs -f web

# Servis durumlarını kontrol et
docker-compose ps

# Veritabanı migration çalıştır (örnek)
docker-compose exec web flask db upgrade

# Tüm servisleri durdur ve sil
docker-compose down

# Volume'ları da sil (dikkat: veriler silinir!)
docker-compose down -v

Geliştirme Ortamı için Ayrı Compose Dosyası

Production ve development ortamları farklı ihtiyaçlara sahip. Development’ta hot-reload, debug modu, volume mount gibi şeyler lazım:

cat > docker-compose.dev.yml << 'EOF'
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      target: builder  # multi-stage build kullanıyorsak
    environment:
      - FLASK_ENV=development
      - FLASK_DEBUG=1
      - DATABASE_URL=postgresql://appuser:apppass@db:5432/appdb_dev
    volumes:
      # Kaynak kodu mount et - hot reload için
      - .:/app
      # Virtual env'i override etme
      - /app/venv
    ports:
      - "5000:5000"
      - "5678:5678"  # debugpy portu
    command: python -m debugpy --listen 0.0.0.0:5678 -m flask run --host=0.0.0.0 --port=5000

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=appuser
      - POSTGRES_PASSWORD=apppass
      - POSTGRES_DB=appdb_dev
    ports:
      - "5432:5432"  # Development'ta dışarıdan erişim için

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"
EOF

# Development compose'u çalıştır
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

Yaygın Sorunlar ve Çözümleri

Gerçek dünyada karşılaşılan problemler ve çözümleri sysadmin’in işini kolaylaştırır. İşte en sık karşılaşılan durumlar:

Bağımlılık çakışması: requirements.txt‘de versiyon sabitlemeyin demek modası geçti, sabitlemek şart. Ama sadece ana paketleri sabitleyin, pip-compile ile tam bir lock dosyası oluşturun:

# pip-tools kurulumu
pip install pip-tools

# requirements.in dosyasından lock dosyası oluştur
pip-compile requirements.in --output-file=requirements.txt

# Bağımlılıkları güncelle
pip-compile --upgrade requirements.in

# Container içinde sync yap
pip-sync requirements.txt

Image boyutu şişmesi: Hangi katmanın ne kadar yer kapladığını görmek için:

# Image katmanlarını analiz et
docker history flask-api:latest

# Detaylı boyut analizi için dive aracı
docker run --rm -it 
    -v /var/run/docker.sock:/var/run/docker.sock 
    wagoodman/dive:latest flask-api:latest

Container başlamıyor: Debug sürecinde sistematik yaklaşım:

# Container'ı interaktif modda başlat, CMD'yi override et
docker run -it --rm flask-api:latest /bin/bash

# Çıkış kodunu kontrol et
docker inspect flask-api --format='{{.State.ExitCode}}'

# Son çalışan container'ın loglarına bak
docker logs flask-api --tail=50

# Container kaynak kullanımı
docker stats flask-api --no-stream

Güvenlik Kontrol Listesi

Production’a almadan önce container güvenliği için yapılması gerekenler:

  • Root olmayan kullanıcı: Dockerfile’da USER appuser ile non-root kullanıcıya geç
  • Gizli bilgiler: .env dosyalarını asla image’a dahil etme, Docker secrets veya environment variable kullan
  • Image tarama: Trivy veya Snyk ile zafiyet taraması yap
  • Read-only filesystem: Mümkünse container’ı read-only modda çalıştır
  • Capabilities: --cap-drop=ALL ile gereksiz Linux capabilities’leri kaldır
  • Ağ izolasyonu: Servisleri sadece gerekli ağlara bağla, her şeyi default bridge’e koyma
# Trivy ile image tarama
docker run --rm 
    -v /var/run/docker.sock:/var/run/docker.sock 
    aquasec/trivy:latest image flask-api:latest

# Güvenli container başlatma örneği
docker run -d 
    --name flask-api 
    --read-only 
    --cap-drop=ALL 
    --security-opt=no-new-privileges:true 
    --tmpfs /tmp 
    -p 5000:5000 
    flask-api:latest

Sonuç

Python uygulamasını Docker ile containerize etmek, deployment süreçlerini kökten değiştiriyor. “Bende çalışıyor” probleminin üstüne kapanıyor, versiyon yönetimi basitleşiyor, ölçekleme kolaylaşıyor.

Özetle dikkat edilmesi gereken kritik noktalar:

  • Katman optimizasyonu: requirements.txt‘i her zaman uygulama kodundan önce kopyala
  • Multi-stage build: Production image’larını küçük ve temiz tut
  • Non-root kullanıcı: Güvenlik için zorunlu, ihmal etme
  • .dockerignore: Gereksiz dosyaları image’dan dışla, özellikle .env dosyalarını
  • Health check: Container orchestration sistemlerinin ihtiyaç duyduğu, ihmal edilmemeli
  • Compose kullanımı: Development ve production için ayrı compose dosyaları yönetimi kolaylaştırır
  • Gizli bilgi yönetimi: Environment variable veya Docker secrets, asla hardcode etme

Docker öğrenme eğrisi başta dik görünebilir ama bir kez oturduğunda geri dönmek istemiyorsunuz. Özellikle birden fazla Python versiyonu veya çakışan bağımlılıkları olan projeler varsa, Docker olmadan nasıl idare ettiğinizi merak edeceksiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir