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.comiç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.