kertenkerem_
· DevOps · 4 dk okuma

Caddy ile Docker Konteynerlerinde Sıfır Konfigürasyonlu Otomatik SSL ve Production Dağıtımı

Docker üzerinde çalışan mikro servisleri ya da monolit uygulamaları canlıya alırken hâlâ yüzlerce satırlık Nginx konfigürasyonu yazıyor, Certbot cronjob’larını debug ediyor veya OpenSSL script’leri arasında kayboluyor musunuz? Caddy bu operasyonel karmaşayı ortadan kaldıran, modern ve bellek güvenliği odaklı (memory-safe) bir web sunucusu. Bu yazıda production ortamında Docker konteynerleriniz için sıfır konfigürasyonlu ve tamamen otomatik bir altyapıyı adım adım kuruyoruz.

Nginx ve Caddy: production gözünden karşılaştırma

Yıllardır süren reverse proxy rekabeti aslında bir felsefe savaşı. Nginx performansı yüksek bir C yazılımı, ama SSL yönetimi, HTTP/3 desteği ve dinamik konfigürasyon gibi modern ihtiyaçlar için ek modüllere, third-party paketlere (certbot, lua, v.b.) ihtiyaç duyar. Bu da production ortamlarında bakım yükünü (maintenance overhead) katlar.

Caddy ise Go diliyle yazılmıştır ve içinde gömülü bir ACME (Let’s Encrypt / ZeroSSL) istemcisiyle gelir. Tek satır yazarak reverse proxy kurulumunu tamamlar, otomatik SSL sürecini de arka planda kendisi başlatır. HTTP/3 protokolünü kutudan çıktığı gibi aktif eder. Performans tarafında Nginx milisaniyeler bazında mikro optimizasyonlarda hâlâ önde olabilir, ama modern CPU’larda ve network mimarilerinde Caddy’nin getirdiği operasyonel hız ve güvenlik bu farkı önemsiz kılıyor.

Production mimarisi: Docker Compose ve Caddyfile

Production ortamında stabil çalışan bir reverse proxy yapılandırması için önce Caddy’nin durum bilgisi (state) içeren dizinlerini host makinede saklamamız gerekir. SSL sertifikalarının kaybolmaması ve rate limit’lere takılmamak için bu adım şart.

İlk olarak projemizin kök dizininde bir Caddyfile oluşturalım. Bu dosya Caddy’nin ana konfigürasyon merkezi ve syntax’ı oldukça temiz.

# Caddyfile
api.kertenkerem.net {
    # Gelişmiş Güvenlik Başlıkları
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-XSS-Protection "1; mode=block"
        X-Frame-Options "DENY"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    # Sıkıştırma Aktif
    encode gzip zstd

    # Docker içindeki microservice-api konteynerine yönlendirme
    reverse_proxy api-service:8080 {
        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
    }
}

Yukarıdaki konfigürasyonda tek bir domain tanımladık. Caddy bu domaini gördüğü anda Let’s Encrypt ve ZeroSSL üzerinden ACME HTTP-01 challenge sürecini otomatik olarak başlatır. Ek bir port açıp certbot çalıştırmanıza ya da private-key üretmenize gerek kalmaz.

Docker Compose ile servisleri ayağa kaldırmak

Şimdi bu yapıyı ayağa kaldıracak production-ready docker-compose.yml dosyamızı hazırlayalım. En çok dikkat etmeniz gereken nokta, Caddy’nin veri depolama hacimleri (volumes).

version: '3.8'

services:
  caddy:
    image: caddy:2.7-alpine
    container_name: caddy_proxy
    restart: always
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp" # HTTP/3 (QUIC) için zorunlu port
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - prod_network
    environment:
      - ACME_AGREE=true

  api-service:
    image: node:20-alpine
    container_name: node_api
    restart: always
    command: node index.js
    # Port map etmiyoruz; dış dünyaya sadece Caddy üzerinden kapalı network ile açıyoruz
    networks:
      - prod_network

networks:
  prod_network:
    driver: bridge

volumes:
  caddy_data:
    external: false
  caddy_config:
    external: false

/data ve /config dizinlerini neden kalıcı hale (persist) getirdik? Çünkü Caddy aldığı SSL sertifikalarını, private key’leri ve ACME hesap bilgilerini /data dizini altında saklar. Bu volume tanımlarını yapmazsanız konteyner her restart olduğunda sıfırdan sertifika talep eder, Let’s Encrypt’in haftalık limitlerine (duplicate certificate limit) takılır ve siteniz erişime kapanabilir.

Gelişmiş senaryo: DNS-01 challenge ile wildcard SSL

API servislerinizi ya da staging ortamlarınızı dış dünyaya tamamen açmadan, yerel veya yarı kapalı networklerde SSL ile çalıştırmak istiyorsanız ya da wildcard (*.kertenkerem.net) sertifikaya ihtiyacınız varsa, varsayılan HTTP-01 challenge işe yaramaz. HTTP-01, Let’s Encrypt sunucularının sizin 80. portunuza erişmesini gerektirir.

Bu durumlarda DNS-01 challenge kullanmalıyız. Caddy bu yöntemde DNS sağlayıcınızın (örn: Cloudflare) API’sini kullanarak geçici bir TXT kaydı açar, doğrulamayı geçer ve kaydı siler. Ancak varsayılan Caddy imajı güvenlik ve hafiflik gerekçesiyle DNS plugin’lerini içermez, bu yüzden kendi imajımızı derlememiz gerekir.

Bunun için Dockerfile ile kolayca bir custom build oluşturabiliriz:

# Dockerfile-caddy
FROM caddy:2.7-builder-alpine AS builder

RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare

FROM caddy:2.7-alpine

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Bu custom imajı oluşturduktan sonra Caddyfile dosyamızı DNS-01 kullanacak şekilde güncelliyoruz:

# DNS-01 Destekli Caddyfile
*.kertenkerem.net {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }

    @api host api.kertenkerem.net
    handle @api {
        reverse_proxy api-service:8080
    }

    # Geri kalan istekleri düşür veya statik dosyaya yönlendir
    handle {
        abort
    }
}

Burada kullandığımız CLOUDFLARE_API_TOKEN değerini Docker Compose dosyasında çevre değişkeni (environment variable) olarak tanımlamanız yeterli. Caddy runtime sırasında bu token’ı okuyarak DNS işlemlerini kendisi halleder.

Sıfır kesintiyle (zero-downtime) konfigürasyon güncelleme

Production sistemlerinde en büyük kabuslardan biri, proxy konfigürasyonunu değiştirdikten sonra servisi restart ederken yaşanan saniyelik kesintilerdir. Nginx’teki nginx -s reload komutunun karşılığı Caddy’de çok daha zarif çözülmüş.

Caddyfile üzerinde yaptığınız değişiklikleri canlıya almak için konteyneri restart etmeyin. Bunun yerine şu komutu çalıştırın:

docker exec -w /etc/caddy caddy_proxy caddy reload

Bu komut, Caddy’nin arka plandaki Go thread’leri aracılığıyla yeni konfigürasyonu belleğe yüklemesini sağlar. Eski bağlantılar (active connections) koparılmaz, yeni gelen istekler anında yeni kurallara göre yönlendirilir. İşlem milisaniyeler içinde tamamlanır ve hata payı sıfırdır.

Production ortamında dikkat edilmesi gereken SRE pratikleri

  • Log yönetimi: Caddy logları varsayılan olarak JSON formatında stdout’a basar. Bu, ELK, Grafana Loki veya Datadog gibi log toplayıcılarla çok iyi uyumludur. Caddyfile içine log { output stdout } ekleyerek log formatınızı standartlaştırın.
  • Resource limits: Go runtime varsayılan olarak sistemdeki tüm CPU çekirdeklerini kullanmaya meyillidir. Docker Compose üzerinde Caddy servisine makul CPU ve Memory limitleri koyarak (örn: 0.5 CPU, 512MB RAM) olası bellek sızıntısı ve CPU spike durumlarında host makinenin çökmesini engelleyin.
  • Keep-alive ve timeout ayarları: Production ortamında yavaş istemci saldırılarını (Slowloris v.b.) engellemek için Caddy’nin global ayarlarından network timeout limitlerini projenizin doğasına göre optimize edin.

Caddy’nin modern mimarisi ve sıfır operasyonel efor felsefesi, onu Docker tabanlı production dağıtımlarında her DevOps mühendisinin cephaneliğinde bulunması gereken bir araç yapıyor. Altyapınızı sadeleştirin, SSL sertifikalarını ve yönlendirmeleri Go tabanlı bu modern dostumuz yönetsin.