Caddy dengan Docker Compose #
Docker Compose adalah cara paling umum dan paling terorganisir untuk menjalankan Caddy bersama stack aplikasi lengkap. Dengan satu file docker-compose.yml, kamu mendefinisikan Caddy sebagai reverse proxy, aplikasi backend, database, dan semua service yang dibutuhkan — berikut network isolation, volume management, dan dependency ordering — semuanya dalam satu deklarasi yang bisa di-version control.
Artikel ini membahas pola-pola deployment yang umum, dari setup paling sederhana hingga stack production yang lengkap dengan isolasi jaringan yang proper.
Mengapa Docker Compose untuk Caddy? #
Sebelum masuk ke konfigurasi, penting memahami keunggulan menggunakan Compose:
Tanpa Compose (docker run manual): Dengan Compose:
────────────────────────────────── ────────────────────────────
docker run ... caddy docker compose up -d
docker run ... app
docker run ... db
docker network create ...
docker network connect ...
docker network connect ...
(Puluhan perintah, mudah salah) (Satu perintah, deklaratif)
Compose juga memudahkan:
- Reproducibility — siapa pun yang punya file Compose bisa menjalankan stack yang identik
- Network isolation — service yang tidak perlu berkomunikasi tidak bisa berkomunikasi
- Version control — konfigurasi seluruh stack ada di satu file yang bisa di-commit ke Git
- Environment management — berbeda
.envuntuk dev/staging/production
Struktur Direktori yang Direkomendasikan #
project/
├── docker-compose.yml ← Definisi stack
├── docker-compose.dev.yml ← Override untuk development
├── .env ← Environment variables (jangan commit ke Git)
├── .env.example ← Template .env (commit ke Git)
├── Caddyfile ← Konfigurasi Caddy
└── app/
├── Dockerfile
└── src/
Setup Minimal #
Untuk pemula, ini adalah setup paling sederhana yang berfungsi:
docker-compose.yml:
services:
caddy:
image: caddy:2.8.4
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
app:
image: node:20-alpine
working_dir: /app
volumes:
- ./app:/app
command: node server.js
# Tidak perlu expose port ke host — hanya Caddy yang perlu akses
volumes:
caddy_data: # WAJIB — menyimpan sertifikat TLS
caddy_config: # Menyimpan konfigurasi cache Caddy
Caddyfile:
example.com {
# 'app' adalah nama service di docker-compose.yml
# Docker DNS internal me-resolve ini ke IP container 'app'
reverse_proxy app:3000
}
# Jalankan stack
docker compose up -d
# Cek status semua service
docker compose ps
# Lihat log Caddy
docker compose logs caddy
docker compose logs -f caddy # Follow mode
Stack Production: Caddy + Node.js + PostgreSQL #
Ini adalah contoh realistis dengan isolasi jaringan yang benar — pola yang harus digunakan di production:
services:
caddy:
image: caddy:2.8.4
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- frontend
depends_on:
- app
app:
build:
context: ./app
dockerfile: Dockerfile
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
PORT: 3000
networks:
- frontend # Agar bisa diakses Caddy
- backend # Agar bisa akses database
depends_on:
db:
condition: service_healthy # Tunggu sampai DB siap
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend # HANYA di backend — tidak bisa diakses dari internet
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
frontend:
# Caddy dan App berkomunikasi di sini
# Internet → Caddy → App (via frontend network)
backend:
# App dan Database berkomunikasi di sini
# Tidak ada koneksi langsung dari Caddy ke DB
# Tidak ada akses dari internet ke DB
volumes:
caddy_data:
caddy_config:
postgres_data:
Mengapa Isolasi Network Ini Penting? #
Tanpa isolasi (semua di default network):
Internet → Caddy ─┬─→ App
└─→ DB (BERBAHAYA! Caddy bisa akses langsung ke DB)
Dengan isolasi yang benar:
Internet → Caddy (frontend) → App (frontend + backend) → DB (backend only)
✓ Caddy tidak bisa akses DB
✓ Internet tidak bisa akses DB langsung
✓ App bisa akses keduanya karena ada di kedua network
.env file (jangan commit ke Git):
DB_PASSWORD=supersecretpassword123
Stack PHP dengan PHP-FPM #
PHP-FPM di Docker memiliki tantangan khusus: Caddy dan PHP-FPM perlu berbagi Unix socket untuk komunikasi yang efisien. Ini dilakukan via shared volume.
services:
caddy:
image: caddy:2.8.4
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./public:/var/www/html:ro # Web root — read-only untuk Caddy
- caddy_data:/data
- caddy_config:/config
- php_socket:/run/php # Shared volume untuk Unix socket
depends_on:
- php-fpm
php-fpm:
image: php:8.3-fpm-alpine
restart: unless-stopped
volumes:
- ./public:/var/www/html:ro # Web root — read-only untuk PHP juga
- php_socket:/run/php # Shared volume untuk Unix socket
# Konfigurasi PHP-FPM untuk listen di Unix socket
command: >
sh -c "
sed -i 's|listen = 127.0.0.1:9000|listen = /run/php/php-fpm.sock|'
/usr/local/etc/php-fpm.d/www.conf &&
sed -i 's|;listen.owner = www-data|listen.owner = root|'
/usr/local/etc/php-fpm.d/www.conf &&
sed -i 's|;listen.group = www-data|listen.group = root|'
/usr/local/etc/php-fpm.d/www.conf &&
sed -i 's|;listen.mode = 0660|listen.mode = 0666|'
/usr/local/etc/php-fpm.d/www.conf &&
php-fpm
"
volumes:
caddy_data:
caddy_config:
php_socket: # Named volume untuk berbagi Unix socket
example.com {
root * /var/www/html
# Gunakan Unix socket dari volume yang dibagi
php_fastcgi unix//run/php/php-fpm.sock
encode gzip
file_server
}
Multi-Domain dalam Satu Stack #
Untuk aplikasi yang memiliki beberapa subdomain atau domain terpisah:
services:
caddy:
image: caddy:2.8.4
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
api:
build: ./api
restart: unless-stopped
networks: [web]
frontend:
build: ./frontend
restart: unless-stopped
networks: [web]
admin:
build: ./admin
restart: unless-stopped
networks: [web]
docs:
image: nginx:alpine
volumes:
- ./dist:/usr/share/nginx/html:ro
networks: [web]
networks:
web:
volumes:
caddy_data:
caddy_config:
{
email [email protected]
}
api.example.com {
reverse_proxy api:8080
header {
Access-Control-Allow-Origin "https://app.example.com"
}
}
app.example.com {
reverse_proxy frontend:3000
}
admin.example.com {
# Batasi akses ke IP tertentu
@blocked {
not remote_ip 10.0.0.0/8 192.168.0.0/16
}
respond @blocked "Access Denied" 403
reverse_proxy admin:4000
}
docs.example.com {
reverse_proxy docs:80
}
Development dengan HTTPS Lokal #
Caddy sangat berguna untuk development karena bisa membuat HTTPS yang valid untuk domain lokal menggunakan internal CA. Browser akan trust sertifikat ini setelah kamu install root CA ke sistem.
docker-compose.dev.yml:
services:
caddy:
image: caddy:2.8.4
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile.dev:/etc/caddy/Caddyfile:ro
- caddy_data_dev:/data
- caddy_config_dev:/config
# Mount direktori untuk export root CA
- ./caddy-certs:/root/.local/share/caddy/pki/authorities/local
app:
build: .
environment:
NODE_ENV: development
volumes:
caddy_data_dev:
caddy_config_dev:
Caddyfile.dev:
{
# Aktifkan HTTPS internal untuk localhost
# Tidak perlu koneksi internet, tidak butuh domain
local_certs
}
localhost {
reverse_proxy app:3000
}
# Jika app kamu perlu akses via nama service
app.localhost {
reverse_proxy app:3000
}
# Jalankan stack development
docker compose -f docker-compose.dev.yml up -d
# Install root CA Caddy ke sistem (sekali saja)
# Caddy akan export root CA ke direktori yang di-mount di atas
caddy trust
# Sekarang https://localhost bekerja tanpa warning di browser
Perintah Docker Compose yang Sering Digunakan #
# ═══ START/STOP ═══
# Jalankan semua service di background
docker compose up -d
# Jalankan hanya service tertentu
docker compose up -d caddy app
# Stop semua service (container dihapus, volume tetap)
docker compose down
# Stop dan hapus volume — HATI-HATI: sertifikat hilang!
docker compose down -v
# Restart service tertentu
docker compose restart caddy
# ═══ LOG ═══
# Log dari semua service
docker compose logs
# Log real-time dari semua service
docker compose logs -f
# Log dari service tertentu saja
docker compose logs -f caddy
# 100 baris terakhir
docker compose logs --tail 100 caddy
# ═══ EXEC ═══
# Reload Caddyfile tanpa restart container
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# Validasi Caddyfile
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
# Masuk ke shell container Caddy
docker compose exec caddy sh
# Jalankan perintah di container app
docker compose exec app npm run migrate
# ═══ STATUS ═══
# Lihat status semua service
docker compose ps
# Lihat resource usage
docker compose stats
# ═══ BUILD ═══
# Build ulang image (setelah mengubah Dockerfile)
docker compose build
# Build dan langsung jalankan
docker compose up -d --build
# ═══ SCALE ═══
# Jalankan 3 instance service 'app' (untuk load balancing)
docker compose up -d --scale app=3
Pitfall yang Harus Dihindari #
1. Menghapus Volume Data Tanpa Sadar #
# BERBAHAYA — menghapus semua volume termasuk sertifikat TLS
docker compose down -v
# AMAN — hanya stop dan hapus container
docker compose down
2. Hot-reload Caddyfile yang Tidak Terupdate #
# Setelah edit Caddyfile di host, container tidak otomatis tahu
# Kamu perlu reload secara eksplisit
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# Atau restart hanya Caddy (ada downtime singkat)
docker compose restart caddy
3. Service Depends On tanpa Health Check #
# ANTI-PATTERN: depends_on tanpa health check
# App mungkin start sebelum DB benar-benar siap
app:
depends_on:
- db # Hanya tunggu container start, tidak tunggu DB ready
# BENAR: Gunakan condition: service_healthy
app:
depends_on:
db:
condition: service_healthy
4. Port Database Terekspos ke Host #
# ANTI-PATTERN: Port DB terekspos ke host (dan berpotensi ke internet)
db:
ports:
- "5432:5432" # JANGAN ini di production!
# BENAR: Tidak perlu expose port — hanya app yang perlu akses DB
db:
# Tidak ada 'ports' — hanya bisa diakses dari backend network
networks:
- backend
Ringkasan #
- Gunakan named volumes
caddy_datadancaddy_config— jangan bind mount untuk ini, dan jangan pernah jalankandocker compose down -vdi production.- Buat network terpisah (
frontend/backend) untuk isolasi — database tidak perlu bisa diakses Caddy secara langsung.- Gunakan nama service sebagai hostname di Caddyfile (
app:3000, bukanlocalhost:3000).- Untuk PHP, gunakan
php_socketsebagai shared volume untuk berbagi Unix socket antara Caddy dan PHP-FPM.- Gunakan
condition: service_healthydidepends_onuntuk memastikan service bergantung menunggu service lain benar-benar siap.docker compose exec caddy caddy reloaduntuk reload konfigurasi tanpa restart container.- Untuk development dengan HTTPS lokal, gunakan
local_certsdi Caddyfile dancaddy trustuntuk install CA ke browser.