Caddy di Docker #
Menjalankan Caddy di Docker adalah salah satu cara paling populer untuk men-deploy Caddy, terutama di lingkungan yang sudah mengadopsi containerization. Docker memberikan isolasi yang bersih, kemudahan deployment, dan konsistensi antara environment development dan production.
Namun ada satu kesalahan yang sangat umum yang dibuat pengguna baru saat menjalankan Caddy di Docker: tidak menggunakan persistent volume untuk menyimpan sertifikat TLS. Kesalahan ini tidak langsung terlihat — semuanya tampak bekerja normal sampai kamu me-restart atau menghapus container, dan mendapati bahwa Caddy mencoba memperoleh sertifikat baru dari Let’s Encrypt. Let’s Encrypt memiliki rate limit ketat: 5 sertifikat per domain per 7 hari. Jika kamu terus-menerus menghapus dan membuat ulang container, domain kamu bisa diblokir selama beberapa hari.
Artikel ini membahas cara yang benar untuk menjalankan Caddy di Docker, dari deployment paling sederhana hingga konfigurasi production yang lengkap.
Image Resmi Caddy di Docker Hub #
Tim Caddy menyediakan dan memelihara image resmi di Docker Hub. Ada beberapa varian yang perlu kamu ketahui:
# Varian standar — binary Caddy di atas Alpine Linux
# Image ini yang paling umum digunakan
docker pull caddy:latest
docker pull caddy:2 # Track major version 2
docker pull caddy:2.8 # Track minor version 2.8
docker pull caddy:2.8.4 # Pin ke versi spesifik — untuk production
# Varian Alpine — eksplisit menggunakan Alpine (sama dengan standar tapi label berbeda)
docker pull caddy:2.8.4-alpine
# Varian builder — digunakan untuk compile Caddy dengan plugin kustom
# Berisi xcaddy dan toolchain Go, tapi JANGAN dijadikan image final
# Digunakan sebagai build stage dalam Dockerfile multi-stage
docker pull caddy:builder
docker pull caddy:2.8.4-builder
Untuk production, selalu pin ke versi spesifik seperticaddy:2.8.4— bukancaddy:latestataucaddy:2. Ini memastikan behavior yang konsisten dan mencegah upgrade mendadak yang tidak terduga saat container di-pull ulang. Lakukan upgrade secara sadar dengan mengubah tag versi.
Percobaan Cepat — Tanpa Konfigurasi #
Sebelum setup yang proper, berikut cara paling cepat untuk melihat Caddy bekerja:
# Jalankan Caddy yang menjawab "Hello!" di port 8080
docker run --rm -p 8080:80 caddy caddy respond --listen :80 "Hello from Caddy!"
# Test
curl http://localhost:8080
# Output: Hello from Caddy!
Ini berguna untuk verifikasi cepat bahwa Docker dan image bekerja, tapi jangan digunakan untuk deployment nyata karena tidak ada konfigurasi dan tidak ada persistensi apapun.
Deployment yang Benar — Dengan Volume Persisten #
Ini adalah cara yang benar untuk men-deploy Caddy di Docker. Perhatikan setiap detail:
docker run -d \
--name caddy \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
-p 443:443/udp \
-v /path/to/your/Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy_data:/data \
-v caddy_config:/config \
caddy:2.8.4
Penjelasan detail setiap flag:
Flag Penjelasan
──────────────────────────────────────────────────────────────────
--name caddy → Nama container agar mudah dirujuk
(untuk: docker exec caddy ..., docker logs caddy)
--restart unless-stopped → Restart otomatis jika container crash
atau jika Docker daemon restart
Berhenti hanya jika di-stop secara manual
-p 80:80 → Map port 80 host ke port 80 container
Diperlukan untuk ACME HTTP-01 challenge
dan redirect HTTP → HTTPS
-p 443:443 → Map port 443 TCP ke container
Untuk HTTPS (TLS)
-p 443:443/udp → Map port 443 UDP ke container
Untuk HTTP/3 (QUIC) — opsional tapi disarankan
-v /path/Caddyfile:/etc/caddy/Caddyfile:ro
→ Bind mount Caddyfile dari host ke container
":ro" = read-only — container tidak bisa mengubahnya
-v caddy_data:/data → Named volume untuk sertifikat TLS dan data ACME
WAJIB menggunakan named volume (bukan anonymous)
agar data persisten saat container dihapus/dibuat ulang
-v caddy_config:/config → Named volume untuk konfigurasi cache Caddy
Berisi konfigurasi yang ter-adapt dari Caddyfile
Kenapa Named Volume, Bukan Bind Mount untuk /data? #
Ada alasan teknis mengapa /data sebaiknya menggunakan named Docker volume, bukan bind mount ke direktori host:
# ANTI-PATTERN: Bind mount untuk data sertifikat
-v /home/user/caddy-data:/data
# Problem: Permission mismatch — user di container (UID 1000)
# mungkin berbeda dengan user di host
# File permission bisa menjadi masalah
# BENAR: Named volume
-v caddy_data:/data
# Docker mengelola permission secara otomatis
# Data persisten selama named volume ada, terlepas dari lifecycle container
Caddyfile untuk Environment Docker #
Ada satu perbedaan penting antara Caddyfile untuk instalasi langsung dan untuk Docker: cara merujuk ke backend/upstream.
Di instalasi langsung, backend biasanya berjalan di localhost:3000. Di Docker, setiap container memiliki network namespace sendiri — localhost di dalam container Caddy merujuk ke container Caddy itu sendiri, bukan ke container aplikasi lain.
# ANTI-PATTERN: Menggunakan localhost untuk upstream di Docker
example.com {
# Ini tidak akan bekerja! 'localhost:3000' di dalam container Caddy
# merujuk ke Caddy itu sendiri, bukan ke container 'app'
reverse_proxy localhost:3000
}
# BENAR: Gunakan nama service Docker sebagai hostname
example.com {
# Docker DNS internal me-resolve 'app' ke IP container 'app'
# Hanya bekerja jika Caddy dan app di network Docker yang sama
reverse_proxy app:3000
}
Docker menyediakan DNS internal yang otomatis me-resolve nama container/service ke IP yang tepat, tapi hanya dalam network yang sama. Ini berarti kamu perlu memastikan Caddy dan backend berada dalam Docker network yang sama.
# Buat network terlebih dahulu
docker network create web
# Jalankan Caddy dalam network 'web'
docker run -d \
--name caddy \
--network web \
-p 80:80 -p 443:443 \
-v ./Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy_data:/data \
caddy:2.8.4
# Jalankan app dalam network yang sama
docker run -d \
--name app \
--network web \
my-app:latest
# Sekarang Caddy bisa resolve 'app' sebagai hostname
Mengelola Caddy di dalam Container #
Reload Konfigurasi #
# Cara yang benar: reload tanpa restart container
# Ini menerapkan konfigurasi baru secara graceful tanpa downtime
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# Alternatif: kirim SIGHUP signal
docker kill --signal=SIGHUP caddy
# Validasi dulu sebelum reload (best practice)
docker exec caddy caddy validate --config /etc/caddy/Caddyfile \
&& docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Melihat Log #
# Log terbaru dari container Caddy
docker logs caddy
# Follow log secara real-time
docker logs -f caddy
# Log dengan timestamp
docker logs -t caddy
# Hanya 100 baris terakhir
docker logs --tail 100 caddy
# Kombinasi
docker logs -f -t --tail 50 caddy
Mengakses Shell di Container #
# Masuk ke shell interaktif di container Caddy yang berjalan
docker exec -it caddy sh
# Di dalam shell, kamu bisa:
caddy version
caddy list-modules
caddy validate --config /etc/caddy/Caddyfile
curl http://localhost:2019/config/
Memeriksa Status via Admin API #
Admin API Caddy berjalan di port 2019 di dalam container. Untuk mengaksesnya dari luar:
# Akses Admin API dari host melalui docker exec
docker exec caddy curl -s http://localhost:2019/config/ | python3 -m json.tool
# Atau ekspos port 2019 hanya ke localhost saat menjalankan container
docker run -d \
--name caddy \
-p 127.0.0.1:2019:2019 \ # Ekspos ke localhost saja, BUKAN ke semua interface
-p 80:80 -p 443:443 \
-v ./Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy_data:/data \
caddy:2.8.4
# Kemudian akses dari host
curl http://localhost:2019/reverse_proxy/upstreams/
Jangan pernah mengekspos port Admin API (2019) ke0.0.0.0atau interface publik. Admin API memberikan kontrol penuh atas Caddy. Gunakan127.0.0.1:2019:2019untuk membatasi akses hanya dari localhost, atau akses viadocker exec.
Menggunakan Variabel Environment di Caddyfile #
Caddy mendukung variabel environment di Caddyfile, yang sangat berguna untuk membedakan konfigurasi antara environment development dan production:
docker run -d \
--name caddy \
-e SITE_ADDRESS=example.com \
-e BACKEND_HOST=app \
-e BACKEND_PORT=3000 \
-p 80:80 -p 443:443 \
-v ./Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy_data:/data \
caddy:2.8.4
{env.SITE_ADDRESS} {
reverse_proxy {env.BACKEND_HOST}:{env.BACKEND_PORT}
}
Variabel environment juga bisa digunakan untuk secret yang tidak boleh di-hardcode di Caddyfile:
docker run -d \
--name caddy \
-e ADMIN_PASSWORD_HASH="$2a$14$..." \
...
example.com {
basicauth /admin/* {
admin {env.ADMIN_PASSWORD_HASH}
}
reverse_proxy app:3000
}
Caddy dengan Plugin Kustom di Docker #
Jika kamu butuh plugin yang tidak ada di binary standar (misalnya DNS provider Cloudflare untuk wildcard certificate), kamu perlu build image kustom menggunakan multi-stage Dockerfile.
# Stage 1: Build Caddy dengan plugin yang diperlukan
# Gunakan image 'builder' yang sudah berisi xcaddy dan toolchain Go
FROM caddy:2.8.4-builder AS builder
# Tambahkan plugin dengan xcaddy
RUN xcaddy build \
--with github.com/caddy-dns/[email protected] \
--with github.com/mholt/[email protected]
# Stage 2: Image final yang ringan
# Ganti binary standar dengan binary yang baru di-compile
FROM caddy:2.8.4
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
# Build image kustom
docker build -t my-caddy:2.8.4 .
# Verifikasi plugin tersedia
docker run --rm my-caddy:2.8.4 caddy list-modules | grep cloudflare
# Output: dns.providers.cloudflare
# Gunakan image kustom
docker run -d \
--name caddy \
-p 80:80 -p 443:443 \
-v ./Caddyfile:/etc/caddy/Caddyfile:ro \
-v caddy_data:/data \
my-caddy:2.8.4
Rate Limit Let’s Encrypt — Memahami Risikonya #
Ini adalah topik yang sangat penting untuk pengguna Docker karena mudah terjebak dalam situasi yang menguras rate limit.
Let’s Encrypt menerapkan rate limit berikut:
Rate Limit Let's Encrypt:
─────────────────────────────────────────────────────
50 sertifikat per Registered Domain per minggu
→ Untuk example.com, subdomain apapun dihitung bersama
5 Duplicate Certificate per minggu
→ Sertifikat untuk domain set yang identik
5 Failed Validation per hostname per jam
→ Terlalu banyak ACME challenge gagal
300 New Orders per account per 3 jam
→ Total request sertifikat baru
Dalam konteks Docker, situasi berbahaya ini bisa terjadi:
# ANTI-PATTERN: Deployment workflow yang menguras rate limit
# Hapus container dan volume (menghapus sertifikat!)
docker rm -f caddy
docker volume rm caddy_data
# Buat container baru (Caddy request sertifikat BARU dari Let's Encrypt)
docker run -d ... caddy
# Lakukan ini 5 kali dalam seminggu → Rate limit tercapai
# Domain kamu tidak bisa dapat sertifikat baru selama 7 hari
# BENAR: Workflow yang aman
# Hapus container TAPI jangan hapus volume
docker rm -f caddy
# caddy_data volume masih ada!
# Buat container baru — Caddy menggunakan sertifikat yang ada di volume
docker run -d \
-v caddy_data:/data \ # Volume yang sama!
...
caddy
Debugging Masalah Umum di Docker #
Caddy Tidak Bisa Resolve Nama Service #
# Gejala: "dial tcp: lookup app: no such host"
# Pastikan kedua container di network yang sama
docker network inspect web
# Harus menyertakan kedua container 'caddy' dan 'app'
# Jika tidak, tambahkan container ke network
docker network connect web caddy
docker network connect web app
Sertifikat Tidak Diperoleh #
# Lihat log ACME
docker logs caddy 2>&1 | grep -i "acme\|certificate\|tls\|error"
# Kemungkinan masalah:
# 1. Port 80 tidak bisa diakses dari internet
curl -I http://IP-SERVER-KAMU:80
# 2. DNS belum mengarah ke IP server
dig +short domain.com
# 3. Rate limit Let's Encrypt
# Log akan menyertakan pesan seperti:
# "too many certificates already issued for registered domain"
Volume Data Hilang #
# Cek apakah named volume masih ada
docker volume ls | grep caddy
# Jika hilang (karena docker compose down -v atau volume rm)
# Caddy akan request sertifikat baru saat container berikutnya start
# Tidak ada cara recover kecuali request ulang
Ringkasan #
- Gunakan image resmi dengan tag versi spesifik (
caddy:2.8.4) di production — bukanlatest.- Volume
/datauntuk sertifikat wajib menggunakan named volume (caddy_data) — bukan anonymous volume yang hilang saat container dihapus.- Di Docker, gunakan nama service/container sebagai hostname upstream (bukan
localhost).- Port
443/udpuntuk HTTP/3 — sertakan agar Caddy bisa mengaktifkan QUIC.- Reload konfigurasi dengan
docker exec caddy caddy reload— tidak perlu restart container.- Jangan hapus volume
caddy_datasembarangan — Let’s Encrypt rate limit sangat ketat dan bisa memblokir domain selama 7 hari.- Untuk plugin kustom, gunakan Dockerfile multi-stage dengan
caddy:buildersebagai build stage.