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.txtartı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 appuserile non-root kullanıcıya geç - Gizli bilgiler:
.envdosyaları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=ALLile 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.envdosyaları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.
