Docker Compose

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 .env untuk 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_data dan caddy_config — jangan bind mount untuk ini, dan jangan pernah jalankan docker compose down -v di 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, bukan localhost:3000).
  • Untuk PHP, gunakan php_socket sebagai shared volume untuk berbagi Unix socket antara Caddy dan PHP-FPM.
  • Gunakan condition: service_healthy di depends_on untuk memastikan service bergantung menunggu service lain benar-benar siap.
  • docker compose exec caddy caddy reload untuk reload konfigurasi tanpa restart container.
  • Untuk development dengan HTTPS lokal, gunakan local_certs di Caddyfile dan caddy trust untuk install CA ke browser.

← Sebelumnya: Docker   Berikutnya: Compile dari Source →

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