Caddy di Docker #

Menjalankan Caddy di dalam container Docker adalah salah satu pendekatan paling populer untuk mengadopsi infrastruktur modern berbasis containerization. Dengan menjalankan Caddy di Docker, kita mendapatkan isolasi proses yang bersih, kemudahan dalam melakukan pengujian (development), konsistensi konfigurasi antara komputer lokal dan server produksi, serta kemudahan proses deployment menggunakan alur kerja CI/CD.

Namun, di balik kepopulerannya, terdapat satu kesalahan fatal yang sangat sering terjadi pada pengguna baru Caddy di Docker: melupakan konfigurasi volume persisten (persistent volume) untuk penyimpanan sertifikat TLS. Pada sistem bare-metal, Caddy menulis sertifikat otomatis ke disk dan membacanya kembali saat restart. Di dalam container Docker, jika kita tidak mendefinisikan volume secara eksplisit, seluruh file sertifikat TLS yang diterbitkan oleh Let’s Encrypt akan disimpan dalam writable layer container yang bersifat sementara (ephemeral). Saat container kita dihapus atau diperbarui, sertifikat tersebut akan hilang selamanya, memaksa Caddy meminta sertifikat baru. Let’s Encrypt menerapkan batas ketat (Rate Limit), sehingga pengajuan berulang dalam waktu singkat dapat menyebabkan domain kita diblokir selama beberapa hari.

Artikel ini akan mengupas tuntas cara men-deploy Caddy di Docker secara benar sesuai dengan praktik terbaik industri.


Arsitektur Container Caddy #

Sebelum kita masuk ke perintah instalasi, mari kita pahami bagaimana data konfigurasi dan sertifikat dipetakan antara sistem host Linux kita dengan lingkungan internal container Caddy melalui diagram di bawah ini:

flowchart TD
    subgraph Host ["Sistem Host Linux"]
        Caddyfile["Caddyfile (Host File)"]
        DataVol[("caddy_data (Named Volume)")]
        ConfigVol[("caddy_config (Named Volume)")]
        
        subgraph Container ["Container Caddy (caddy:2.8.4)"]
            Core["Caddy Runtime Engine"]
            CertStorage["/data/caddy/ (SSL Certs)"]
            ConfigStorage["/config/caddy/"]
            ConfigMapping["/etc/caddy/Caddyfile"]
        end
    end

    Caddyfile -. Bind Mount .-> ConfigMapping
    DataVol -. Volume Mount .-> CertStorage
    ConfigVol -. Volume Mount .-> ConfigStorage

    style Host stroke:#0288d1,stroke-width:2px
    style Container stroke:#43a047,stroke-width:2px

Melalui pemetaan volume di atas, kita memastikan bahwa konfigurasi Caddyfile di host dapat langsung dibaca oleh Caddy, dan seluruh sertifikat SSL yang diunduh dari Let’s Encrypt tetap aman di dalam named volume host meskipun container Caddy dihancurkan dan dibuat ulang.


Image Resmi Caddy di Docker Hub #

Tim inti developer Caddy memelihara image resmi di Docker Hub. Kita perlu memahami perbedaan varian tag yang disediakan untuk memilih image yang paling cocok untuk server kita:

# Varian Standar (Berbasis Alpine Linux)
# Ini adalah image default yang direkomendasikan karena ukurannya kecil dan aman.
docker pull caddy:latest
docker pull caddy:2          # Mengikuti pembaruan rilis Mayor versi 2
docker pull caddy:2.8        # Mengikuti pembaruan rilis Minor versi 2.8
docker pull caddy:2.8.4      # Mengunci ke versi spesifik (sangat disarankan untuk produksi!)

# Varian Alpine Eksplisit
docker pull caddy:2.8.4-alpine

# Varian Builder
# Image khusus yang dilengkapi compiler Go dan utilitas xcaddy.
# Kita menggunakannya hanya untuk kompilasi plugin kustom di Dockerfile multi-stage.
docker pull caddy:builder
docker pull caddy:2.8.4-builder

[!TIP] Selalu Kunci Versi di Produksi! Hindari penggunaan tag caddy:latest atau caddy:2 pada server produksi kita. Penggunaan tag dinamis tersebut berisiko memicu pembaruan otomatis yang tidak terduga saat server melakukan pull ulang image, yang berpotensi memicu ketidakcocokan konfigurasi (breaking changes). Gunakan tag spesifik seperti caddy:2.8.4 dan lakukan pembaruan versi secara sadar melalui revisi kode.


Percobaan Awal — Tanpa Konfigurasi (Uji Coba) #

Jika kita hanya ingin memverifikasi bahwa Docker dan image Caddy berjalan dengan baik di server kita tanpa menulis file konfigurasi apa pun terlebih dahulu, kita dapat menjalankan perintah sekali pakai berikut:

# Menjalankan container Caddy sementara untuk merespon teks sederhana di port 8080
docker run --rm -p 8080:80 caddy caddy respond --listen :80 "Halo dari Caddy di Docker!"

Kita dapat membuka terminal baru dan melakukan pengujian menggunakan curl:

curl http://localhost:8080
# Output yang diharapkan: Halo dari Caddy di Docker!

Flag --rm memastikan Docker akan menghapus container ini secara otomatis saat kita menghentikan prosesnya (dengan menekan tombol Ctrl+C). Skenario ini hanya untuk uji coba cepat dan tidak boleh digunakan untuk server asli karena tidak memiliki persistensi data.


Langkah Deployment yang Benar #

Mari kita buat container Caddy produksi dengan konfigurasi volume persisten, port mapping yang lengkap, dan kebijakan restart otomatis.

Berikut adalah perintah standar untuk menjalankan container Caddy secara aman:

docker run -d \
  --name caddy \
  --restart unless-stopped \
  -p 80:80 \
  -p 443:443 \
  -p 443:443/udp \
  -v /var/www/Caddyfile:/etc/caddy/Caddyfile:ro \
  -v caddy_data:/data \
  -v caddy_config:/config \
  caddy:2.8.4

Penjelasan Detail Parameter Perintah #

Kita harus memahami peran dari setiap flag yang kita gunakan di atas:

  • -d — Menjalankan container di background (detached mode) agar terminal kita tetap bebas.
  • --name caddy — Memberikan nama container yang konsisten untuk mempermudah eksekusi perintah berikutnya (seperti melihat log atau melakukan reload).
  • --restart unless-stopped — Mengonfigurasi Docker daemon untuk memulai ulang container Caddy secara otomatis jika ia mengalami crash, server mati, atau Docker di-restart. Docker tidak akan merestart jika kita menghentikannya secara manual.
  • -p 80:80 — Memetakan port 80 TCP host ke container. Port ini wajib dibuka karena digunakan oleh Caddy untuk melayani pengalihan otomatis HTTP ke HTTPS dan merespon validasi Let’s Encrypt HTTP-01 challenge.
  • -p 443:443 — Memetakan port 443 TCP untuk mengamankan lalu lintas terenkripsi HTTPS (TLS).
  • -p 443:443/udp — Memetakan port 443 UDP. Langkah ini opsional namun sangat direkomendasikan agar Caddy dapat melayani protokol HTTP/3 (QUIC) yang memberikan performa jauh lebih cepat.
  • -v /var/www/Caddyfile:/etc/caddy/Caddyfile:ro — Melakukan bind mount file Caddyfile fisik dari host ke dalam container. Aturan :ro (read-only) memastikan container Caddy tidak diizinkan mengubah isi Caddyfile demi keamanan.
  • -v caddy_data:/data — Membuat named volume bernama caddy_data yang dipetakan ke direktori internal /data. Di sinilah Caddy menyimpan seluruh sertifikat SSL dan file konfigurasi ACME yang diterbitkan oleh CA.
  • -v caddy_config:/config — Membuat named volume bernama caddy_config untuk menyimpan file konfigurasi internal hasil adaptasi dari Caddyfile.

Mengapa Memilih Named Volume daripada Bind Mount untuk /data? #

Kita disarankan menggunakan named volume (seperti caddy_data:/data) dibandingkan melakukan bind mount langsung ke folder host (seperti /home/kita/data:/data). Hal ini dikarenakan:

# ANTI-PATTERN: Menggunakan bind mount langsung untuk direktori data sensitif
-v /home/kita/caddy_data:/data
# Masalah: Container Caddy berjalan dengan UID/GID non-root internal. 
# Jika folder host dimiliki oleh user root dengan permission ketat, 
# Caddy di dalam container akan mengalami crash akibat "Permission Denied" 
# saat mencoba menulis sertifikat TLS baru ke folder tersebut.

# BENAR: Menggunakan named volume
-v caddy_data:/data
# Solusi: Docker secara otomatis mengatur izin kepemilikan (ownership) dan 
# perizinan (permissions) yang tepat pada filesystem internal, sehingga Caddy 
# dapat menulis dan membaca data tanpa terhambat masalah izin sistem host.

Caddyfile dalam Lingkungan Docker #

Ada satu aturan emas yang wajib kita patuhi saat menyusun Caddyfile untuk dijalankan di dalam Docker: Jangan pernah menggunakan localhost atau 127.0.0.1 sebagai upstream server web proxy kita.

Di dalam ekosistem Docker, setiap container memiliki lingkungan jaringan terisolasi (network namespace) sendiri. Alamat localhost di dalam container Caddy merujuk pada container Caddy itu sendiri, bukan komputer host kita atau container aplikasi backend lainnya.

# ANTI-PATTERN: Menggunakan localhost untuk mendefinisikan backend di Docker
situsku.com {
    # Konfigurasi ini AKAN GAGAL! Caddy akan mencoba mencari port 3000 
    # di dalam dirinya sendiri, bukan di container aplikasi kita.
    reverse_proxy localhost:3000
}

# BENAR: Gunakan nama container atau nama service jaringan Docker
situsku.com {
    # DNS internal Docker secara otomatis menerjemahkan nama container 
    # 'app_container' menjadi alamat IP internal yang tepat.
    reverse_proxy app_container:3000
}

Agar hal ini berfungsi dengan benar, container Caddy dan container backend aplikasi kita harus berada di dalam jaringan Docker (Docker network) yang sama.

# Langkah 1: Buat jaringan Docker baru
docker network create jaringan_web

# Langkah 2: Jalankan container backend aplikasi kita di dalam jaringan tersebut
docker run -d --name app_container --network jaringan_web my-node-app:latest

# Langkah 3: Jalankan Caddy di jaringan yang sama
docker run -d \
  --name caddy \
  --network jaringan_web \
  -p 80:80 -p 443:443 -p 443:443/udp \
  -v /var/www/Caddyfile:/etc/caddy/Caddyfile:ro \
  -v caddy_data:/data \
  caddy:2.8.4

Mengelola Lifecycle Caddy di Docker #

Kita dapat melakukan pemeliharaan runtime Caddy secara langsung menggunakan perintah CLI Docker.

1. Reload Konfigurasi Tanpa Downtime #

Sama seperti di instalasi bare-metal, kita tidak perlu mematikan container (docker restart) hanya untuk menerapkan perubahan Caddyfile baru. Kita dapat memicu proses graceful reload di dalam container:

# Cara 1: Menggunakan sub-perintah reload resmi Caddy (Sangat Direkomendasikan)
docker exec -w /etc/caddy caddy caddy reload

# Cara 2: Mengirimkan sinyal SIGHUP ke proses Caddy di container
docker kill --signal=SIGHUP caddy

Sebelum kita melakukan reload, kita juga dapat melakukan validasi file konfigurasi terlebih dahulu:

# Memvalidasi konfigurasi di dalam container
docker exec -w /etc/caddy caddy caddy validate

# Alur kerja aman: validasi di container, jika sukses lakukan reload
docker exec -w /etc/caddy caddy caddy validate && docker exec -w /etc/caddy caddy caddy reload

2. Memeriksa Log Container Caddy #

# Memantau log runtime kontainer Caddy secara real-time
docker logs -f caddy

# Menampilkan 50 baris log terakhir disertai timestamp lengkap
docker logs -t --tail 50 caddy

3. Masuk ke Shell Container #

Jika kita membutuhkan investigasi mendalam terhadap lingkungan filesystem internal Caddy:

# Membuka terminal interaktif shell (sh) di container Caddy
docker exec -it caddy sh

# Di dalam container, kita dapat memeriksa modul yang aktif:
# caddy list-modules

Caddy dengan Plugin Kustom di Docker #

Jika kita membutuhkan modul tambahan yang tidak disertakan pada binary resmi — contohnya modul DNS provider Cloudflare untuk mendukung wildcard SSL via DNS-01 challenge — kita harus mengompilasi binary Caddy kustom.

Cara paling efisien dan bersih adalah menggunakan Dockerfile Multi-Stage. Pendekatan ini menggunakan image caddy:builder untuk melakukan proses kompilasi, namun pada tahap akhir hanya menyalin file binary hasil kompilasi ke image caddy standar yang bersih dan ringan.

Buat berkas bernama Dockerfile:

# Tahap 1: Kompilasi binary menggunakan image builder resmi
FROM caddy:2.8.4-builder AS builder

# Jalankan kompilasi menggunakan xcaddy dengan menentukan plugin yang kita butuhkan
# Kita dapat menambahkan beberapa bendera --with untuk menyertakan lebih dari satu plugin
RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/mholt/caddy-ratelimit

# Tahap 2: Buat image akhir yang bersih dan ringan
FROM caddy:2.8.4

# Ganti binary bawaan dengan binary kustom yang telah selesai dikompilasi
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Kita dapat membangun image kustom tersebut menggunakan perintah:

# Membangun image Docker kustom dengan nama tag 'caddy-kustom:2.8.4'
docker build -t caddy-kustom:2.8.4 .

# Memverifikasi bahwa plugin Cloudflare telah berhasil terpasang di binary baru
docker run --rm caddy-kustom:2.8.4 caddy list-modules | grep cloudflare
# Output: dns.providers.cloudflare

Sekarang kita dapat menjalankan container menggunakan image baru kita caddy-kustom:2.8.4.


Rate Limit Let’s Encrypt — Risiko Kehilangan Sertifikat #

Kita harus memahami dampak dari batas permintaan (rate limits) Let’s Encrypt agar tidak merusak reputasi domain kita di server produksi. Let’s Encrypt membatasi penerbitan sertifikat duplikat sebanyak 5 sertifikat per domain per minggu.

Berikut adalah contoh skenario berbahaya yang sering merugikan pengguna baru di Docker:

# ANTI-PATTERN: Skenario penghapusan container dan volume secara tidak sengaja
docker stop caddy
docker rm caddy
docker volume rm caddy_data   # DATA SERTIFIKAT DI HAPUS TOTAL!

# Setelah data dihapus, kita membuat ulang container:
docker run -d --name caddy -v caddy_data:/data ... caddy:2.8.4
# Caddy mendeteksi folder /data kosong, ia meminta sertifikat baru ke Let's Encrypt.
# Jika alur kerja di atas kita ulangi sebanyak 5 kali berturut-turut dalam satu minggu,
# domain kita akan diblokir oleh Let's Encrypt karena mencapai batas duplikasi.
# Akibatnya, pada pembuatan kontainer ke-6, HTTPS akan gagal dan situs kita offline.
# BENAR: Alur kerja pemeliharaan container yang aman
docker stop caddy
docker rm caddy

# JANGAN PERNAH menghapus volume 'caddy_data'.
# Ketika kita membuat ulang container, kita tetap memetakan volume yang sama:
docker run -d --name caddy -v caddy_data:/data ... caddy:2.8.4
# Caddy mendeteksi sertifikat SSL yang valid masih ada di folder /data host,
# ia langsung menggunakannya kembali tanpa perlu menghubungi Let's Encrypt. Aman!

Troubleshooting Kasus Docker #

1. Pesan Kesalahan: “dial tcp: lookup app_container: no such host” #

Caddy tidak dapat mendeteksi keberadaan server backend kita menggunakan nama container yang didefinisikan di Caddyfile. Solusi:

  • Pastikan kedua container telah berjalan.
  • Pastikan kedua container berada dalam satu jaringan Docker yang sama. Kita dapat memverifikasi jaringan aktif dengan perintah: docker network inspect jaringan_web.
  • Pastikan tidak ada salah penulisan nama container di Caddyfile.

2. Sertifikat TLS Tidak Kunjung Terbit #

Caddy tidak berhasil mengamankan koneksi HTTPS dan log menampilkan proses negosiasi ACME berulang kali. Solusi:

  • Periksa apakah domain benar-benar telah diarahkan ke IP host server kita.
  • Verifikasi bahwa port 80 dan 443 host kita dapat dijangkau dari luar (firewall terbuka).
  • Gunakan perintah docker logs caddy 2>&1 | grep -i acme untuk melihat pesan kesalahan spesifik dari Let’s Encrypt/ZeroSSL.

3. Masalah Izin File pada Volume Statis #

Caddy mengembalikan respon HTTP 403 Forbidden untuk dokumen statis yang dipetakan dari host. Solusi: Hal ini terjadi karena user default di dalam container Caddy (biasanya user caddy dengan UID 1000 atau root tergantung image) tidak memiliki izin baca pada file host kita. Jalankan perintah berikut pada host Linux untuk memastikan izin baca tersedia bagi semua user:

sudo chmod -R o+r /path/ke/direktori/statis/host

Kapan Beralih ke Alternatif / Tidak Menggunakan Ini #

Tetap gunakan deployment Docker Standar jika:
  ✓ Kita ingin mengisolasi proses server web dari filesystem host secara ketat.
  ✓ Kita terbiasa men-deploy container tunggal dengan siklus hidup mandiri.
  ✓ Aplikasi backend kita juga dijalankan di dalam container Docker.
  ✓ Kita membutuhkan kemudahan replikasi environment server web di komputer lokal kita.

Pertimbangkan metode lain jika:
  ✗ Kita mengelola arsitektur kompleks dengan puluhan service yang saling bergantung (Gunakan Docker Compose).
  ✗ Kita ingin integrasi package manager bawaan OS dan mengelola pembaruan via apt-get rutin (Gunakan APT).
  ✗ Server kita memiliki spesifikasi memori sangat minim yang tidak mampu menahan overhead Docker Daemon.

Ringkasan #

  • Gunakan Tag Spesifik — Selalu gunakan versi spesifik (contoh: caddy:2.8.4) pada server produksi untuk memastikan stabilitas runtime.
  • Wajib Volume Persisten — Selalu petakan direktori /data container ke named volume host (caddy_data:/data) guna mencegah terhapusnya sertifikat TLS.
  • Hindari Localhost — Gunakan nama container atau nama service Docker sebagai alamat upstream reverse proxy di Caddyfile, bukan localhost atau 127.0.0.1.
  • Protokol HTTP/3 — Buka port 443/udp pada pemetaan port container (-p 443:443/udp) agar Caddy dapat melayani HTTP/3.
  • Graceful Reload — Gunakan perintah docker exec caddy caddy reload untuk memuat ulang konfigurasi tanpa perlu merestart container.
  • Multi-Stage Build — Manfaatkan image caddy:builder untuk merakit binary Caddy kustom secara aman dan tetap menghasilkan image final yang ringan.

← Sebelumnya: CentOS / RHEL   Berikutnya: Docker Compose →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact