Docker Compose ile Uygulama Güncellemesi ve Geri Alma Stratejileri

Prodüksiyonda bir uygulama güncellemesi yapıyorsunuz, her şey yolunda gidiyor gibi görünüyor, sonra telefon çalmaya başlıyor. “Site çalışmıyor.” Bu anı yaşayan her sysadmin bilir, o an için hazırlıklı olmak ile olmamak arasındaki fark saatler hatta günler süren bir kesinti demektir. Docker Compose ile çalışıyorsanız, iyi haber şu: doğru stratejiyle bu tür felaket senaryolarını çok daha kontrollü bir şekilde yönetebilirsiniz.

Neden Güncelleme Stratejisine İhtiyaç Duyarız?

Docker Compose, geliştirme ortamları için mükemmel bir araç olarak başlar ama zamanla prodüksiyon sistemlerine de taşınır. Bu geçiş beraberinde ciddi sorumluluklar getirir. Bir servis güncellemesinde yanlış giden her şey kullanıcıları etkiler, veri tutarsızlıklarına yol açabilir ve geri dönmek sandığınızdan çok daha karmaşık olabilir.

Temel problem şu: Docker Compose’un kendisi Kubernetes gibi gelişmiş rolling update veya canary deployment mekanizmalarına sahip değildir. Ancak bu, iyi bir strateji geliştiremeyeceğiniz anlamına gelmez. Biraz planlama ve doğru araçlarla oldukça sağlam bir güncelleme ve geri alma süreci kurabilirsiniz.

Temel Hazırlık: Image Tag Yönetimi

Her şeyden önce, latest tag kullanmayı bırakın. Bu sysadmin dünyasındaki en klasik hatalardan biridir. latest size hangi versiyonda olduğunuzu söylemez ve geri dönmeyi imkânsız hale getirir.

# Kötü pratik - asla bunu yapmayın
image: myapp:latest

# İyi pratik - her zaman spesifik versiyon kullanın
image: myapp:1.4.2
# veya commit hash ile
image: myapp:a3f8c1d

docker-compose.yml dosyanızda versiyonları açıkça belirtin ve bu dosyayı Git ile takip edin. Her değişiklik commit edilmeli, commit mesajları anlamlı olmalıdır. Bu, ileride “hangi versiyona dönecektik?” sorusunun cevabını otomatik olarak verir.

Uygulamanızı build ederken semantic versioning kullanın ve birden fazla tag ile işaretleyin:

# Build ve birden fazla tag ile işaretle
docker build -t myapp:1.4.2 -t myapp:stable .

# Registry'ye push et
docker push myapp:1.4.2
docker push myapp:stable

# Şu anki production versiyonunu kaydet
echo "CURRENT_VERSION=1.4.2" > /etc/myapp/version.env

Docker Compose Dosya Yapısı ve Override Mantığı

Güncelleme stratejisinin temel taşlarından biri, docker-compose.yml dosyalarını ortama göre ayırmaktır. Base bir dosya ve üzerine eklenen override dosyaları kullanmak, hem güncelleme hem de geri alma işlemlerini kolaylaştırır.

# docker-compose.yml - Base konfigürasyon
version: '3.8'

services:
  web:
    image: myapp:${APP_VERSION:-1.4.2}
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./ssl:/etc/ssl/certs:ro
    depends_on:
      web:
        condition: service_healthy
    networks:
      - app-network

  redis:
    image: redis:7.2-alpine
    volumes:
      - redis-data:/data
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  redis-data:

Ortam değişkenlerini .env dosyasında tutun ve versiyon bilgisini buradan yönetin:

# .env dosyası
APP_VERSION=1.4.2
DATABASE_URL=postgresql://user:pass@db:5432/myapp
REDIS_URL=redis://redis:6379/0

Güncelleme Öncesi Kontrol Listesi

Herhangi bir güncelleme yapmadan önce çalıştırmanız gereken bir kontrol scripti hazırlayın. Bu scripti cron’a bağlamayın, manuel olarak çalıştırın ve çıktısını okuyun:

#!/bin/bash
# pre-update-check.sh

set -e

echo "=== Güncelleme Öncesi Kontroller ==="

# Mevcut servislerin durumunu kontrol et
echo "[1/5] Servis durumları kontrol ediliyor..."
docker compose ps

# Disk kullanımını kontrol et
echo "[2/5] Disk kullanımı kontrol ediliyor..."
df -h | grep -E "^/dev|^Filesystem"
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$DISK_USAGE" -gt 85 ]; then
    echo "UYARI: Disk kullanımı %$DISK_USAGE - güncelleme riskli!"
    exit 1
fi

# Aktif bağlantıları kontrol et
echo "[3/5] Aktif bağlantılar kontrol ediliyor..."
ss -tn | grep :80 | wc -l

# Mevcut image'ları kaydet
echo "[4/5] Mevcut versiyon bilgileri kaydediliyor..."
docker compose images > /tmp/pre-update-images.txt
cat /tmp/pre-update-images.txt

# Database backup al
echo "[5/5] Database backup alınıyor..."
BACKUP_FILE="/backup/db-$(date +%Y%m%d-%H%M%S).sql"
docker compose exec -T db pg_dump -U myapp > "$BACKUP_FILE"
echo "Backup alındı: $BACKUP_FILE"

echo "=== Kontroller tamamlandı, güncellemeye hazır ==="

Temel Güncelleme Stratejisi: Blue-Green Deployment

Docker Compose ile blue-green deployment yapmak, Kubernetes kadar otomatik olmasa da oldukça etkilidir. İki ortam paralel çalıştırırsınız ve trafiği birinden diğerine geçirirsiniz.

Önce iki ayrı compose dosyası hazırlayın:

# Dizin yapısı
/opt/myapp/
├── docker-compose.yml        # Aktif environment
├── docker-compose.blue.yml   # Blue environment
├── docker-compose.green.yml  # Green environment
├── .env.blue
├── .env.green
└── nginx/
    ├── blue.conf
    └── green.conf
# docker-compose.blue.yml
version: '3.8'

services:
  web-blue:
    image: myapp:${APP_VERSION}
    container_name: myapp-web-blue
    environment:
      - DATABASE_URL=${DATABASE_URL}
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 30s

networks:
  app-network:
    external: true
    name: myapp_app-network

Trafik geçiş scripti:

#!/bin/bash
# blue-green-switch.sh

CURRENT=$1  # "blue" veya "green"
NEW_VERSION=$2

if [ "$CURRENT" = "blue" ]; then
    TARGET="green"
else
    TARGET="blue"
fi

echo "Aktif ortam: $CURRENT"
echo "Hedef ortam: $TARGET"
echo "Yeni versiyon: $NEW_VERSION"

# Yeni ortamı başlat
APP_VERSION=$NEW_VERSION docker compose -f docker-compose.${TARGET}.yml --env-file .env.${TARGET} up -d

# Health check bekle
echo "Health check bekleniyor..."
MAX_WAIT=120
WAITED=0
while [ $WAITED -lt $MAX_WAIT ]; do
    STATUS=$(docker inspect --format='{{.State.Health.Status}}' myapp-web-${TARGET} 2>/dev/null)
    if [ "$STATUS" = "healthy" ]; then
        echo "Servis healthy!"
        break
    fi
    echo "Bekleniyor... ($WAITED/$MAX_WAIT saniye) - Status: $STATUS"
    sleep 5
    WAITED=$((WAITED + 5))
done

if [ $WAITED -ge $MAX_WAIT ]; then
    echo "HATA: Servis $MAX_WAIT saniye içinde healthy olmadı!"
    echo "Yeni ortam kapatılıyor..."
    docker compose -f docker-compose.${TARGET}.yml down
    exit 1
fi

# Nginx konfigürasyonunu güncelle
echo "Nginx konfigürasyonu güncelleniyor..."
sed -i "s/web-${CURRENT}/web-${TARGET}/g" ./nginx/conf.d/app.conf
docker compose exec nginx nginx -s reload

echo "Trafik $TARGET ortamına yönlendirildi."
echo "Eski ortam ($CURRENT) 5 dakika sonra kapatılacak..."
sleep 300
docker compose -f docker-compose.${CURRENT}.yml down
echo "Eski ortam kapatıldı."

# Aktif ortamı kaydet
echo $TARGET > /opt/myapp/.active-env

Hızlı In-Place Güncelleme

Blue-green her zaman uygun olmayabilir, kaynaklar yetmeyebilir. Bu durumda in-place güncelleme yaparsınız. Bu yaklaşımda kısa bir downtime kabul edilir ama dikkatli yapılırsa bu süre saniyelerle ölçülür:

#!/bin/bash
# quick-update.sh

set -e

NEW_VERSION=$1
COMPOSE_FILE="docker-compose.yml"
ENV_FILE=".env"

if [ -z "$NEW_VERSION" ]; then
    echo "Kullanım: $0 <yeni-versiyon>"
    echo "Örnek: $0 1.5.0"
    exit 1
fi

# Mevcut versiyonu kaydet
CURRENT_VERSION=$(grep APP_VERSION $ENV_FILE | cut -d'=' -f2)
echo "Mevcut versiyon: $CURRENT_VERSION"
echo "Yeni versiyon: $NEW_VERSION"

# Rollback için state kaydet
echo "APP_VERSION=$CURRENT_VERSION" > /tmp/rollback.env
cp $COMPOSE_FILE /tmp/docker-compose.rollback.yml

# Yeni image'ı pull et (servis çalışırken)
echo "Yeni image indiriliyor..."
APP_VERSION=$NEW_VERSION docker compose pull web

# .env güncelle
sed -i "s/APP_VERSION=.*/APP_VERSION=$NEW_VERSION/" $ENV_FILE

# Sadece web servisini yeniden başlat
echo "Web servisi yeniden başlatılıyor..."
docker compose up -d --no-deps --force-recreate web

# Health check
echo "Health check bekleniyor..."
sleep 10
HEALTH=$(docker compose ps --format json | python3 -c "
import json, sys
data = json.load(sys.stdin)
if isinstance(data, list):
    for s in data:
        if 'web' in s.get('Service', ''):
            print(s.get('Health', 'N/A'))
")

echo "Servis durumu: $HEALTH"

if docker compose ps | grep -E "web.*unhealthy"; then
    echo "HATA: Servis unhealthy, rollback yapılıyor..."
    sed -i "s/APP_VERSION=.*/APP_VERSION=$CURRENT_VERSION/" $ENV_FILE
    docker compose up -d --no-deps --force-recreate web
    echo "Rollback tamamlandı, versiyon: $CURRENT_VERSION"
    exit 1
fi

echo "Güncelleme başarılı: $NEW_VERSION"

Geri Alma Stratejileri

Geri alma, güncelleme kadar önemlidir. Hatta daha önemlidir çünkü bunu genellikle panik anında yaparsınız. Bu nedenle önceden test edilmiş, tek komutla çalışan bir rollback mekanizmanız olmalıdır.

Versiyon Tabanlı Rollback

#!/bin/bash
# rollback.sh

set -e

ROLLBACK_VERSION=$1
ENV_FILE=".env"

if [ -z "$ROLLBACK_VERSION" ]; then
    # Versiyon belirtilmemişse önceki versiyona dön
    if [ -f "/tmp/rollback.env" ]; then
        ROLLBACK_VERSION=$(grep APP_VERSION /tmp/rollback.env | cut -d'=' -f2)
        echo "Önceki versiyon bulundu: $ROLLBACK_VERSION"
    else
        echo "Rollback versiyonu bulunamadı!"
        echo "Kullanım: $0 <versiyon>"
        exit 1
    fi
fi

echo "Rollback başlıyor: $ROLLBACK_VERSION"

# Image'ın mevcut olup olmadığını kontrol et
if ! docker image inspect myapp:$ROLLBACK_VERSION > /dev/null 2>&1; then
    echo "Image bulunamadı, registry'den çekiliyor..."
    docker pull myapp:$ROLLBACK_VERSION
fi

# Versiyonu güncelle ve servisi yeniden başlat
sed -i "s/APP_VERSION=.*/APP_VERSION=$ROLLBACK_VERSION/" $ENV_FILE
docker compose up -d --no-deps --force-recreate web

# Doğrulama
sleep 15
if docker compose ps | grep -E "web.*Up"; then
    echo "Rollback başarılı! Aktif versiyon: $ROLLBACK_VERSION"
    # Log'a yaz
    echo "$(date): Rollback to $ROLLBACK_VERSION completed" >> /var/log/myapp/deployments.log
else
    echo "KRITIK HATA: Rollback başarısız!"
    echo "Manuel müdahale gerekli!"
    exit 1
fi

Git Tabanlı Rollback

Eğer docker-compose.yml dosyanız Git’te ise bu en güvenli yöntemdir:

#!/bin/bash
# git-rollback.sh

STEPS_BACK=${1:-1}  # Kaç commit geriye gidilecek, varsayılan 1

echo "Git geçmişi kontrol ediliyor..."
git log --oneline -10

echo ""
echo "$STEPS_BACK adım geriye gidiliyor..."

# Hedef commit'i bul
TARGET_COMMIT=$(git log --oneline -$((STEPS_BACK + 1)) | tail -1 | awk '{print $1}')
echo "Hedef commit: $TARGET_COMMIT"

# Compose dosyasını o commit'teki haline getir
git checkout $TARGET_COMMIT -- docker-compose.yml .env

# Çalıştır
docker compose up -d

echo "Rollback tamamlandı."
git log --oneline -1

Database Migration ve Rollback

En kritik konu budur. Uygulama versiyonunu geri almak genellikle kolaydır ama database şemasını geri almak zordur. Bu nedenle her migration’ı geri alınabilir yapın:

#!/bin/bash
# db-migration-safe.sh

set -e

ACTION=$1  # "upgrade" veya "downgrade"
NEW_VERSION=$2

echo "Database migration: $ACTION"

# Migration öncesi backup
BACKUP_FILE="/backup/db-pre-migration-$(date +%Y%m%d-%H%M%S).sql"
echo "Backup alınıyor: $BACKUP_FILE"
docker compose exec -T db pg_dump -U myapp myapp > $BACKUP_FILE
echo "Backup tamamlandı: $(du -sh $BACKUP_FILE | cut -f1)"

if [ "$ACTION" = "upgrade" ]; then
    echo "Migration uygulanıyor..."
    docker compose run --rm web python manage.py migrate
    echo "Migration tamamlandı"
elif [ "$ACTION" = "downgrade" ]; then
    echo "Migration geri alınıyor..."
    docker compose run --rm web python manage.py migrate --fake-initial
    echo "Downgrade tamamlandı"
fi

# Migration durumunu kontrol et
echo "Migration durumu:"
docker compose run --rm web python manage.py showmigrations | tail -20

Eğer migration geri alınamıyorsa son çare database restore’dur:

#!/bin/bash
# db-restore.sh

BACKUP_FILE=$1

if [ -z "$BACKUP_FILE" ]; then
    echo "Son backup dosyası seçiliyor..."
    BACKUP_FILE=$(ls -t /backup/db-*.sql | head -1)
    echo "Seçilen: $BACKUP_FILE"
fi

echo "UYARI: Bu işlem veritabanını $BACKUP_FILE ile değiştirecek!"
echo "Devam etmek için 'EVET' yazın:"
read CONFIRM

if [ "$CONFIRM" != "EVET" ]; then
    echo "İptal edildi."
    exit 0
fi

# Web servisini durdur
docker compose stop web

# Restore
echo "Restore başlıyor..."
docker compose exec -T db psql -U myapp -c "DROP DATABASE IF EXISTS myapp_old; ALTER DATABASE myapp RENAME TO myapp_old;"
docker compose exec -T db psql -U postgres -c "CREATE DATABASE myapp OWNER myapp;"
docker compose exec -T db psql -U myapp myapp < $BACKUP_FILE

# Web servisini başlat
docker compose start web
echo "Restore tamamlandı."

Smoke Test ve Doğrulama

Güncelleme sonrası mutlaka otomatik smoke test çalıştırın. Bu testler hem güncellemenin başarılı olduğunu doğrular hem de rollback kararını otomatikleştirir:

#!/bin/bash
# smoke-test.sh

set -e

BASE_URL=${1:-"http://localhost"}
FAILED=0

echo "=== Smoke Tests Başlıyor ==="

# Fonksiyon: endpoint test et
test_endpoint() {
    local name=$1
    local url=$2
    local expected_status=$3
    local timeout=${4:-10}

    STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time $timeout "$url")
    if [ "$STATUS" = "$expected_status" ]; then
        echo "[OK] $name - HTTP $STATUS"
    else
        echo "[FAIL] $name - Beklenen: $expected_status, Alınan: $STATUS"
        FAILED=$((FAILED + 1))
    fi
}

# Temel endpoint testleri
test_endpoint "Ana sayfa" "$BASE_URL/" "200"
test_endpoint "Health check" "$BASE_URL/health" "200"
test_endpoint "API status" "$BASE_URL/api/v1/status" "200"
test_endpoint "404 kontrolü" "$BASE_URL/bu-sayfa-yok" "404"

# Response time kontrolü
RESPONSE_TIME=$(curl -s -o /dev/null -w "%{time_total}" "$BASE_URL/health")
echo "Response time: ${RESPONSE_TIME}s"

if (( $(echo "$RESPONSE_TIME > 3.0" | bc -l) )); then
    echo "[WARN] Response time 3 saniyeden fazla!"
fi

# Sonuç
echo "=== Sonuç: $FAILED test başarısız ==="
exit $FAILED

İzleme ve Alarm

Güncelleme sırasında ve sonrasında yakın izleme yapmanız gerekir. Basit bir izleme döngüsü:

#!/bin/bash
# watch-deployment.sh

DURATION=${1:-300}  # 5 dakika izle
INTERVAL=15
ELAPSED=0
APP_URL="http://localhost/health"

echo "Deployment izleniyor ($DURATION saniye)..."

while [ $ELAPSED -lt $DURATION ]; do
    TIMESTAMP=$(date '+%H:%M:%S')

    # HTTP status
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 $APP_URL 2>/dev/null || echo "000")

    # Container durumu
    CONTAINER_STATUS=$(docker compose ps --format "{{.Service}}: {{.Status}}" 2>/dev/null | tr 'n' '|')

    # CPU ve Memory
    STATS=$(docker stats --no-stream --format "{{.Name}}: CPU={{.CPUPerc}} MEM={{.MemUsage}}" 2>/dev/null | head -3 | tr 'n' '|')

    echo "[$TIMESTAMP] HTTP:$HTTP_STATUS | $CONTAINER_STATUS"

    if [ "$HTTP_STATUS" != "200" ]; then
        echo "ALARM: HTTP $HTTP_STATUS - Rollback düşünülmeli!"
    fi

    sleep $INTERVAL
    ELAPSED=$((ELAPSED + INTERVAL))
done

echo "İzleme tamamlandı."

Pratik Senaryo: Gerçek Bir Güncelleme Akışı

Tüm bu araçları bir araya getiren tam bir güncelleme akışı şöyle görünür:

#!/bin/bash
# deploy.sh - Tam güncelleme scripti

NEW_VERSION=$1
DRY_RUN=${2:-"false"}

if [ -z "$NEW_VERSION" ]; then
    echo "Kullanım: $0 <versiyon> [dry-run]"
    exit 1
fi

echo "========================================="
echo "Deployment: $NEW_VERSION"
echo "Tarih: $(date)"
echo "========================================="

# 1. Ön kontroller
bash pre-update-check.sh || exit 1

# 2. Yeni image pull
echo "Image indiriliyor..."
docker pull myapp:$NEW_VERSION

if [ "$DRY_RUN" = "dry-run" ]; then
    echo "DRY RUN: Gerçek deployment yapılmadı."
    exit 0
fi

# 3. Smoke test mevcut ortamda
echo "Mevcut ortam test ediliyor..."
bash smoke-test.sh http://localhost || {
    echo "HATA: Mevcut ortam zaten sorunlu!"
    exit 1
}

# 4. Güncellemeyi uygula
bash quick-update.sh $NEW_VERSION || {
    echo "Güncelleme başarısız, rollback yapılıyor..."
    bash rollback.sh
    exit 1
}

# 5. Smoke test yeni versiyonda
echo "Yeni versiyon test ediliyor..."
sleep 20
bash smoke-test.sh http://localhost || {
    echo "Smoke test başarısız, rollback yapılıyor..."
    bash rollback.sh
    exit 1
}

# 6. Deployment log
echo "$(date): Deployment $NEW_VERSION başarılı" >> /var/log/myapp/deployments.log
echo "========================================="
echo "Deployment başarılı: $NEW_VERSION"
echo "========================================="

Backup Rotasyonu ve Temizlik

Düzenli güncelleme yapıyorsanız eski image’lar ve backuplar disk doldurmaya başlar. Bunu otomatikleştirin:

#!/bin/bash
# cleanup.sh

echo "Kullanılmayan Docker kaynakları temizleniyor..."

# Dangling image'ları sil
docker image prune -f

# 7 günden eski, kullanılmayan image'ları sil
docker image prune -a --filter "until=168h" -f

# Durmuş container'ları sil
docker container prune -f

# Eski backupları temizle (30 günden eski)
find /backup -name "db-*.sql" -mtime +30 -delete
echo "30 günden eski backuplar silindi."

# Disk kullanım raporu
echo "Disk kullanımı:"
df -h /
docker system df

Sonuç

Docker Compose ile sağlam bir güncelleme ve geri alma stratejisi kurmak Kubernetes kadar karmaşık değil ama aynı prensiplere dayanıyor: her şeyi versiyon kontrolünde tutun, geri alma yolunuzu her zaman açık bırakın, otomatik sağlık kontrolleri yapın ve asla prodüksiyonda test etmeyin.

Özetlemek gerekirse:

  • Versiyon yönetimi: latest tag kullanmayın, her zaman spesifik versiyon numaraları kullanın
  • Pre-deployment kontrolleri: Disk, bağlantı ve backup kontrollerini otomatize edin
  • Güncelleme stratejisi: Kaynağınız varsa blue-green, yoksa in-place ama her ikisinde de health check şart
  • Rollback hazırlığı: Rollback scripti her zaman test edilmiş ve hazır olmalı
  • Database dikkatı: Migration’ları geri alınabilir yazın, her güncelleme öncesi backup alın
  • Smoke testler: Güncelleme sonrası otomatik test çalıştırın, başarısızlıkta otomatik rollback yapın
  • İzleme: Güncelleme sonrası en az 5-10 dakika aktif izleme yapın

Bu scriptlerin tamamını bir Git reposunda saklayın, düzenli olarak güncelleyin ve en önemlisi prodüksiyon öncesinde staging ortamında test edin. Panik anında çalışan scriptler, panik anında yazılan scriptlerden çok daha güvenilirdir.

Yorum yapın