Caddy ile Docker ve Docker Compose Entegrasyonu

Docker ile uygulama gelistirip production’a almaya calisirken en cok baş ağrısı yaratan konulardan biri reverse proxy konfigürasyonudur. Nginx veya Apache ile uğraşmak, sertifika yenilemek, her servis için ayrı config dosyası yazmak… Bunların hepsi zaman alır ve hata yapmaya açıktır. Caddy tam da bu noktada devreye girer. Otomatik HTTPS, minimal konfigürasyon ve Docker ile mükemmel uyum. Bu yazıda Caddy’yi Docker ortamında nasıl kullanacağını, Docker Compose ile nasıl entegre edeceğini ve gerçek dünya senaryolarında nasıl yapılandıracağını detaylı olarak ele alacağız.

Neden Caddy ve Docker Birlikte?

Docker ile çalışırken servisler sürekli değişir, yeni container’lar ayağa kalkar, eskiler durur. Geleneksel reverse proxy’lerde her değişiklikte config güncellemek ve reload yapmak gerekir. Caddy bu süreci ciddi ölçüde basitleştirir.

Caddy’nin Docker ortamında öne çıkan avantajları şunlardır:

  • Otomatik TLS: Let’s Encrypt entegrasyonu ile sertifikalar otomatik alınır ve yenilenir
  • Minimal konfigürasyon: Tek satırla bir servisin önüne geçebilirsin
  • API desteği: Caddy’nin admin API’si üzerinden çalışan sistemi yeniden başlatmadan config değiştirebilirsin
  • Docker label desteği: Traefik benzeri label tabanlı konfigürasyon mümkün
  • Düşük kaynak tüketimi: Go ile yazıldığı için bellek ve CPU kullanımı oldukça makul

Temel Kurulum: Caddy Container’ı Ayağa Kaldırmak

İlk adım olarak basit bir Caddy container’ı çalıştıralım ve nasıl davrandığını görelim.

docker run -d 
  --name caddy 
  -p 80:80 
  -p 443:443 
  -p 443:443/udp 
  -v caddy_data:/data 
  -v caddy_config:/config 
  -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile 
  caddy:latest

Burada UDP port 443, HTTP/3 (QUIC protokolü) için gereklidir. caddy_data volume’u sertifikaları saklar, caddy_config ise Caddy’nin çalışma zamanı konfigürasyonunu tutar. Bu volume’ları kalıcı tutmak kritik önem taşır, aksi halde her container yeniden başlatıldığında sertifikalar sıfırdan alınmaya çalışılır ve Let’s Encrypt rate limit’e takılabilirsin.

Temel bir Caddyfile şöyle görünür:

example.com {
    reverse_proxy localhost:8080
}

Bu kadar. HTTP’den HTTPS’e yönlendirme, sertifika alımı, yenileme hepsi otomatik.

Docker Compose ile Temel Entegrasyon

Gerçek dünyada tek container çalıştırmak nadirdir. Çoğunlukla birden fazla servisi bir arada yönetirsin. İşte bu noktada Docker Compose devreye girer.

Aşağıdaki senaryo düşün: Bir Next.js frontend, bir Node.js API backend ve bir de PostgreSQL veritabanın var. Caddy’yi bu servisler için reverse proxy olarak kullanacaksın.

version: '3.8'

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web
    depends_on:
      - frontend
      - api

  frontend:
    image: node:18-alpine
    container_name: frontend
    working_dir: /app
    volumes:
      - ./frontend:/app
    command: npm start
    networks:
      - web
    expose:
      - "3000"

  api:
    image: node:18-alpine
    container_name: api
    working_dir: /app
    volumes:
      - ./api:/app
    command: npm start
    networks:
      - web
    expose:
      - "4000"

  db:
    image: postgres:15-alpine
    container_name: postgres
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: supersecret
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - internal

volumes:
  caddy_data:
  caddy_config:
  postgres_data:

networks:
  web:
    driver: bridge
  internal:
    driver: bridge

Bu yapıda önemli bir nokta: expose ile ports arasındaki fark. expose sadece aynı Docker network içindeki servislerin erişimine açar, dışarıya port açmaz. Veritabanını sadece internal network’e koyarak ekstra bir güvenlik katmanı ekliyoruz.

Buna uygun Caddyfile:

example.com {
    reverse_proxy frontend:3000
}

api.example.com {
    reverse_proxy api:4000
    
    header {
        Access-Control-Allow-Origin "https://example.com"
        Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
        Access-Control-Allow-Headers "Content-Type, Authorization"
    }
}

Docker Compose network’leri sayesinde container isimlerini hostname olarak kullanabiliyorsun. frontend:3000 yazdığında Caddy, frontend container’ına 3000 portundan bağlanıyor.

Özel Caddy Image’ı Oluşturmak

Bazı durumlarda resmi Caddy image’ı ihtiyaçlarını karşılamayabilir. Örneğin caddy-dns eklentisi ile Cloudflare DNS challenge kullanmak istiyorsun veya başka bir eklentiye ihtiyacın var. Bu durumda kendi Caddy image’ını build etmek gerekir.

FROM caddy:2-builder AS builder

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

FROM caddy:2-alpine

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

xcaddy aracı, Caddy’yi istediğin eklentilerle derlemenizi sağlar. Builder pattern kullanarak final image’ı küçük tutuyoruz. Bu Dockerfile’ı kullanacak Docker Compose konfigürasyonu:

services:
  caddy:
    build:
      context: .
      dockerfile: Dockerfile.caddy
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web

Wildcard Sertifika ve DNS Challenge

Shared hosting’den kurtulup kendi sunucuna geçtiğinde wildcard sertifika büyük kolaylık sağlar. Tek sertifika ile *.example.com altındaki tüm subdomain’leri karşılayabilirsin. Wildcard sertifika için DNS challenge zorunludur.

*.example.com, example.com {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }
    
    @frontend host example.com www.example.com
    @api host api.example.com
    @grafana host grafana.example.com
    @portainer host portainer.example.com
    
    handle @frontend {
        reverse_proxy frontend:3000
    }
    
    handle @api {
        reverse_proxy api:4000
    }
    
    handle @grafana {
        reverse_proxy grafana:3000
    }
    
    handle @portainer {
        reverse_proxy portainer:9000
    }
    
    handle {
        abort
    }
}

Bu yapı, tek bir sertifika bloğu içinde tüm subdomain’leri matcher kullanarak yönetiyor. abort direktifi, eşleşmeyen istekleri keser. Güvenlik açısından iyi bir pratik.

Load Balancing ve Health Check

Production ortamında yüksek erişilebilirlik için aynı uygulamayı birden fazla instance olarak çalıştırabilirsin. Caddy bunu neredeyse sıfır konfigürasyonla destekler.

version: '3.8'

services:
  caddy:
    image: caddy:2-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web

  app:
    image: myapp:latest
    deploy:
      replicas: 3
    networks:
      - web
    expose:
      - "8080"
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s

volumes:
  caddy_data:
  caddy_config:

networks:
  web:
    driver: bridge

Bu konfigürasyona uygun Caddyfile:

example.com {
    reverse_proxy app:8080 {
        lb_policy round_robin
        
        health_uri /health
        health_interval 10s
        health_timeout 5s
        health_status 200
        
        fail_duration 30s
        max_fails 3
        unhealthy_latency 500ms
        
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
    
    log {
        output file /var/log/caddy/access.log {
            roll_size 100mb
            roll_keep 10
        }
        format json
    }
}

Caddy Docker Swarm veya Compose replicas ile çalışırken container ismini kullandığında otomatik olarak tüm instance’lara dağıtır. lb_policy ile round_robin, least_conn veya ip_hash gibi farklı yük dengeleme stratejileri kullanabilirsin.

Gerçek Dünya Senaryosu: Monitoring Stack

Pek çok sysadmin’in kurduğu klasik bir monitoring stack’i düşünelim: Prometheus, Grafana ve Alertmanager. Bunları Caddy ile dışarıya güvenli bir şekilde açmak istiyorsun, üstelik bazı endpoint’lere sadece iç ağdan erişilmesini istiyorsun.

version: '3.8'

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - monitoring

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    networks:
      - monitoring
    expose:
      - "9090"

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SERVER_ROOT_URL=https://grafana.example.com
    volumes:
      - grafana_data:/var/lib/grafana
    networks:
      - monitoring
    expose:
      - "3000"

  alertmanager:
    image: prom/alertmanager:latest
    container_name: alertmanager
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
    networks:
      - monitoring
    expose:
      - "9093"

volumes:
  caddy_data:
  caddy_config:
  prometheus_data:
  grafana_data:

networks:
  monitoring:
    driver: bridge

Bu stack için Caddyfile:

grafana.example.com {
    reverse_proxy grafana:3000
    
    encode gzip
    
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

prometheus.example.com {
    @internal {
        remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
    }
    
    handle @internal {
        reverse_proxy prometheus:9090
    }
    
    handle {
        respond "Access denied" 403
    }
}

alertmanager.example.com {
    basicauth {
        admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
    }
    
    reverse_proxy alertmanager:9093
}

basicauth direktifindeki hash değerini caddy hash-password komutuyla üretebilirsin:

docker run --rm caddy:2-alpine caddy hash-password --plaintext "sifreniz"

Bu komut, girdiğin şifrenin bcrypt hash’ini döner. Caddyfile’a bu hash değerini yazarsın, şifrenin düz hali hiçbir zaman config’e girmez.

Caddy API ile Dinamik Konfigürasyon

Caddy’nin admin API’si oldukça güçlüdür. Çalışan sistemi durdurmadan yeni servisler ekleyebilir, var olanları güncelleyebilirsin. Bu özellikle CI/CD pipeline’larında faydalıdır.

Önce admin API’yi dışarıya açmak için Caddyfile’ı güncelleyelim:

{
    admin 0.0.0.0:2019
}

example.com {
    reverse_proxy app:8080
}

Uyarı: Admin API’yi production’da dışarıya açarken dikkatli ol. Güvenli bir ağ içinde kalmasını veya authentication eklenmesini öneririm.

API ile config yönetimi:

# Mevcut konfigürasyonu görüntüle
curl http://localhost:2019/config/

# Yeni bir route ekle
curl -X POST http://localhost:2019/config/apps/http/servers/srv0/routes 
  -H "Content-Type: application/json" 
  -d '{
    "@id": "new_service",
    "match": [{"host": ["newapp.example.com"]}],
    "handle": [{
      "handler": "reverse_proxy",
      "upstreams": [{"dial": "newapp:5000"}]
    }]
  }'

# Caddy'yi graceful reload yap
curl -X POST http://localhost:2019/load 
  -H "Content-Type: text/caddyfile" 
  --data-binary @Caddyfile

Performans Optimizasyonu ve İpuçları

Docker ortamında Caddy kullanırken performansı artırmak için birkaç önemli nokta var.

Buffer boyutlarını ayarlamak: Büyük dosya transferi olan uygulamalar için:

example.com {
    reverse_proxy app:8080 {
        transport http {
            read_buffer_size 4096
            write_buffer_size 4096
            dial_timeout 10s
            response_header_timeout 30s
        }
    }
}

Gzip sıkıştırma: Statik asset’ler için bant genişliğini önemli ölçüde azaltır:

example.com {
    encode {
        gzip 6
        zstd
        minimum_length 1024
    }
    
    reverse_proxy app:8080
}

File server ile statik içerik sunumu: Eğer Caddy’nin içinden statik dosya sunmak istiyorsan:

static.example.com {
    root * /srv/static
    file_server {
        precompressed gzip br
        hide .git
    }
    
    @cached path *.css *.js *.png *.jpg *.webp *.woff2
    header @cached Cache-Control "public, max-age=31536000, immutable"
}

Bu Caddyfile için Docker Compose’da volume mount eklemeyi unutma:

caddy:
  image: caddy:2-alpine
  volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile:ro
    - ./static:/srv/static:ro
    - caddy_data:/data
    - caddy_config:/config

Log Yönetimi ve Hata Ayıklama

Production’da log yönetimi kritik önem taşır. Caddy’nin structured logging özelliği monitoring araçlarıyla entegrasyonu kolaylaştırır.

# Caddy loglarını takip et
docker logs -f caddy

# Belirli bir hata var mı diye kontrol et
docker logs caddy 2>&1 | grep -i error

# Konfigürasyonu validate et (container ayağa kalkmadan önce)
docker run --rm 
  -v $(pwd)/Caddyfile:/etc/caddy/Caddyfile 
  caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile

# Caddy'yi reload et (sertifikaları sıfırlamadan)
docker exec caddy caddy reload --config /etc/caddy/Caddyfile

# Sertifika durumunu kontrol et
docker exec caddy caddy certificates

caddy validate komutu deploy öncesi yapılan konfigürasyon hatalarını yakalar. CI/CD pipeline’ına bu adımı eklemek iyi bir pratik. Yanlış bir Caddyfile ile production’ı açmak ve sitenin 503 vermesi yerine önceden hatayı tespit etmek çok daha iyidir.

Güvenlik Sertleştirme

Son olarak, güvenlik açısından production’da dikkat etmen gereken noktalar:

{
    email [email protected]
    
    servers {
        protocols h1 h2 h3
        
        timeouts {
            read_body 10s
            read_header 10s
            write 30s
            idle 120s
        }
    }
}

example.com {
    @bots {
        header User-Agent *bot*
        header User-Agent *crawl*
        header User-Agent *spider*
    }
    
    respond @bots "Gone" 410
    
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1m
        }
    }
    
    header {
        -Server
        -X-Powered-By
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Content-Security-Policy "default-src 'self'"
        Permissions-Policy "geolocation=(), microphone=(), camera=()"
    }
    
    reverse_proxy app:8080
}

rate_limit direktifi için caddy-ratelimit eklentisine ihtiyacın var, bunu özel image build ederken xcaddy ile ekleyebilirsin. -Server ve -X-Powered-By header’larını kaldırmak, saldırganlara gereksiz bilgi vermemek açısından önemlidir.

Sonuç

Caddy ve Docker kombinasyonu, modern web altyapısı için gerçekten güçlü bir ikili. Nginx’te onlarca satır config yazacağın şeyleri Caddy ile birkaç satırda halledebiliyorsun. Otomatik HTTPS, Docker network entegrasyonu, API tabanlı dinamik konfigürasyon ve iyi performans bir arada geliyor.

Bu yazıda ele aldığımız konuları özetlemek gerekirse:

  • Temel kurulum: Docker run ve Docker Compose ile Caddy çalıştırma
  • Network tasarımı: Servisleri doğru network’lere yerleştirme ve güvenli expose etme
  • Özel image: xcaddy ile eklenti ekleyerek kendi Caddy image’ını oluşturma
  • Wildcard sertifika: DNS challenge ile *.example.com için tek sertifika
  • Load balancing: Çoklu instance ve health check konfigürasyonu
  • Monitoring stack: Gerçek dünya senaryosunda IP kısıtlama ve basicauth
  • Admin API: Çalışan sistemi durdurmadan konfigürasyon güncelleme
  • Güvenlik: Header hardening, rate limiting ve bot koruması

Caddy’yi production’a almadan önce mutlaka caddy validate ile konfigürasyonu doğrula, volume’ları kalıcı yap ve admin API’yi güvenli bir şekilde yönet. Sertifika volume’unun backup’ını almak da akılda bulundurulması gereken önemli bir nokta; Let’s Encrypt rate limit’lerine takılmak istemezsin.

Yorum yapın