Self-Host Banyak Website di 1 Server + Cloudflare
Back to articles

Self-Host Banyak Website di 1 Server + Cloudflare

Bangun infrastruktur self-hosting untuk puluhan situs di satu server: Cloudflare Tunnel, nginx reverse proxy, container dan database terpisah per situs.

Self-hosting website di rumah punya satu masalah klasik: bagaimana cara mengekspos banyak situs ke internet tanpa membuka port router dan mengekspos IP publik? Jawaban saya: Cloudflare Tunnel + nginx reverse proxy. Dengan kombinasi ini, biaya menambah situs baru turun jadi hampir nol — cukup satu folder baru, satu rule nginx, satu database. Tidak sentuh DNS, tidak sentuh router, tidak buka port.

Artikel ini membongkar bagaimana saya self-host puluhan website di satu server dengan infrastruktur ini.

TL;DR:

  • Cloudflare Tunnel + nginx reverse proxy: tidak buka port router, IP rumah tidak terekspos, TLS otomatis
  • Satu wildcard *.jayax.dev di tunnel — menambah subdomain baru tidak butuh konfigurasi DNS terpisah
  • Isolasi container + database per situs; konvensi <nama> / db_<nama> / <nama>.jayax.dev
  • Batas nyata: RAM host 16GB → 2-3 situs WordPress aktif; ketergantungan pada Cloudflare adalah trade-off yang harus diterima

Kenapa Cloudflare Tunnel, bukan port forwarding

Empat opsi untuk mengekspos situs dari rumah:

Port forwarding + DDNS. IP rumah terekspos: ya. Butuh IP statis: tidak (DDNS, tapi tidak reliable). TLS: harus setup sendiri (Let's Encrypt dengan DNS challenge). Biaya: gratis. Batasan: ISP bisa block port, IP bisa berubah, expose IP rumah ke publik.

VPS reverse proxy. IP rumah terekspos: tidak (VPS yang terekspos). Butuh IP statis: VPS punya. TLS: bisa di VPS. Biaya: $5-10/bulan untuk VPS. Batasan: traffic melalui VPS (bandwidth limit), latency tambahan.

Cloudflare Tunnel. IP rumah terekspos: tidak. Butuh IP statis: tidak. TLS: otomatis (Cloudflare edge). Biaya: gratis (untuk personal use). Batasan: bergantung pihak ketiga (Cloudflare), batas ukuran upload (100MB free tier), TOS Cloudflare.

Tailscale / WireGuard mesh. IP rumah terekspos: tidak. Butuh IP statis: tidak. TLS: tidak otomatis (perlu sertifikat sendiri). Biaya: gratis (personal). Batasan: hanya accessible dari dalam mesh network (tidak publik), kecuali dengan Funnel.

Saya pilih Cloudflare Tunnel karena: gratis, TLS otomatis, IP rumah tidak terekspos, dan yang paling penting — wildcard subdomain. Satu tunnel menangani *.jayax.dev. Menambah subdomain baru tidak butuh konfigurasi DNS terpisah.

Batasan yang jujur: tunnel bergantung pada Cloudflare. Kalau Cloudflare down (rare tapi terjadi), semua situs tidak accessible meskipun container jalan. Batas upload 100MB di free tier cukup membatasi untuk upload file besar (video, backup). Dan TOS Cloudflare melarang serving konten tertentu (non-HTML heavy media) di free tier.

Peta lalu lintas satu request

Browser user
  ↓ HTTPS request ke kopi-surgent.jayax.dev
  ↓
DNS Cloudflare (CNAME *.jayax.dev → tunnel)
  ↓
Cloudflare Edge (terminates TLS, cache static)
  ↓
Cloudflare Tunnel (encrypted connection outbound)
  ↓
cloudflared container (di Docker network)
  ↓
nginx site-server (reverse proxy, port 80)
  ↓
Container situs (kopi-surgent, port 80 internal)
  ↓
HTTP 200 (atau 302, 404, dll)

Satu kalimat per hop: DNS resolve subdomain ke tunnel. Edge Cloudflare terminates TLS (HTTPS ke HTTP di internal). cloudflared meneruskan request ke nginx. Nginx site-server mem-proxy ke container situs berdasarkan Host header. Container situs merespons.

Wildcard *.jayax.dev dan aturan ingress

Config Cloudflare Tunnel mendefinisikan ingress rules — urutan rule menentukan matching. Berikut konfigurasi (dengan credential disamarkan):

# config Cloudflare Tunnel (cloudflared)
tunnel: <TUNNEL_UUID>
credentials-file: /root/.cloudflared/<TUNNEL_UUID>.json

ingress:
  # Rule spesifik untuk subdomain tertentu
  - hostname: status.jayax.dev
    service: http://homepage:3000

  # Wildcard catch-all: semua *.jayax.dev → nginx
  - hostname: "*.jayax.dev"
    service: http://site-server:80

  # Catch-all terakhir (WAJIB paling bawah)
  - service: http_status:404

Urutan matching: rule pertama yang match dipakai. status.jayax.dev match rule pertama (lebih spesifik), langsung ke homepage container. kopi-surgent.jayax.dev match rule kedua (wildcard), ke nginx site-server. Rule catch-all http_status:404 harus paling bawah — kalau tidak, akan menangkap semua request dan menampilkan 404.

Kenapa wildcard cukup? Karena nginx site-server yang menentukan routing lebih lanjut berdasarkan Host header. Cloudflare Tunnel hanya perlu meneruskan semua traffic *.jayax.dev ke satu pintu (nginx). Nginx yang tahu: "ah, Host header-nya kopi-surgent.jayax.dev, proxy ke container kopi-surgent".

nginx site-server sebagai satu-satunya pintu masuk

Satu instance nginx menjadi reverse proxy untuk semua situs. Setiap situs baru mendapat server block sendiri. Pola yang dipakai: satu config file per situs, di-include oleh nginx main config:

# /etc/nginx/sites/kopi-surgent.conf
server {
    listen 80;
    server_name kopi-surgent.jayax.dev;

    location / {
        proxy_pass http://kopi-surgent:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 64M;
    }
}

nginx main config meng-include semua file di sites/:

# /etc/nginx/nginx.conf (di dalam container site-server)
http {
    # ... config lain ...
    include /etc/nginx/sites/*.conf;
}

Kenapa satu nginx pusat, bukan nginx per container? Karena: (1) satu instance nginx makan ~20MB RAM — puluhan instance makan ratusan MB; (2) konfigurasi terpusat lebih mudah di-audit; (3) reload nginx satu kali menerapkan semua perubahan, tidak perlu reload per-container.

Isolasi saat self-host banyak website di satu server: container sendiri, database sendiri

Setiap situs dapat container sendiri dan database sendiri. Ini isolasi level terbaik tanpa overhead VPS terpisah.

Trade-off isolasi vs overhead. Setiap container WordPress makan ~300-512MB RAM. Dengan 16GB host (~11GB untuk WSL2, ~6GB reserved Hermes), kapasitas praktis adalah 2-3 situs WordPress aktif bersamaan. Situs Next.js lebih ringan (~100-200MB) karena stateless.

Konvensi penamaan. Container: <nama-situs> (misal: kopi-surgent). Database: db_<nama-situs>. Subdomain: <nama-situs>.jayax.dev. Konvensi ini membuat troubleshooting mudah — satu nama cukup untuk identifikasi container, database, subdomain, nginx config, dan app-stack directory.

Yang menggigit di produksi. Konsumsi resource nyata di mesin saya: baseline Docker + Hermes + site-server + cloudflared + MariaDB = ~3.5GB. Tiap situs WordPress aktif menambah ~400MB. Dengan 2 situs aktif, total ~4.3GB dari 11GB available — masih ada ruang. Tapi 3+ situs aktif mulai menekan batas.

Status monitor dan homepage

Saya pakai container homepage (open-source dashboard) yang menampilkan daftar semua situs yang sedang jalan. Homepage membaca Docker API dan menampilkan status container (running/stopped), port, dan URL.

Daftar situs juga bisa didapat dengan:

# List semua container situs (bukan infra)
docker ps --format "{{.Names}}" | grep -vE "hermes|cloudflared|mariadb|site-server|homepage|netdata"

Atau via deploy script:

./scripts/deploy.sh list

Output menampilkan: nama situs, framework, container status, HTTP status code, URL.

Yang menggigit saya di produksi

DNS propagation. Cloudflare Tunnel membuat CNAME otomatis saat pertama kali subdomain diakses. Tapi propagation DNS bisa butuh 1-5 menit. Selama window ini, situs tidak accessible dari publik (502 dari Cloudflare edge). Fix: tunggu, atau purges DNS cache.

Rule bentrok. Dua situs dengan nama mirip (misal: kopi dan kopi-surgent) tidak bentrok di nginx (server_name exact match). Tapi kalau tidak sengaja menulis dua config dengan server_name yang sama, nginx reload akan gagal dengan "duplicate server name". Fix: skrip deploy cek nama unik sebelum tulis config.

Port habis. Setiap container yang publish port ke host mengambil satu port. Walaupun nginx proxy menggunakan nama container (bukan port host), beberapa aplikasi tetap publish port untuk debugging. Fix: jangan publish port ke host kecuali benar-benar perlu. Gunakan network Docker internal.

Container mati diam-diam. Container bisa crash tanpa notification kalau tidak ada health check. restart: unless-stopped membantu auto-restart, tapi tidak alert. Fix: homepage dashboard + periodic health check via cron.

Tunnel putus saat reboot. cloudflared container butuh Docker untuk jalan. Kalau Docker Desktop mati (misal setelah Windows update), tunnel putus. Semua situs tidak accessible. Fix: Docker Desktop di-set auto-start, dan watchdog bridge bisa restart Docker Desktop (docker-desktop-restart command di whitelist).

Cert / host header. Cloudflare Tunnel handles TLS di edge. Tapi kalau SSL mode Cloudflare di-set "Full (Strict)", Cloudflare mengharapkan cert valid di origin. Karena origin adalah HTTP (nginx tanpa cert), mode yang benar adalah "Flexible" atau "Full" (tidak strict). Setting yang salah menghasilkan 526 error.

Biaya dan batas skala

Biaya operasional:

  • Cloudflare Tunnel: gratis (free tier)
  • Domain .dev: ~$15/tahun
  • Listrik host (PC idle 24/7): ~$10-15/bulan
  • Internet (sudah ada): tidak tambahan

Kapan pendekatan ini berhenti masuk akal:

  • RAM habis. 16GB host membatasi 2-3 situs WordPress aktif. Situs ke-4 atau ke-5 butuh RAM upgrade atau VPS tambahan.
  • Uptime requirement tinggi. Host rumah tidak punya UPS/SLA. Power outage = semua situs down. Kalau butuh 99.9% uptime, VPS lebih tepat.
  • Traffic tinggi. Cloudflare Tunnel free tier tidak publish bandwidth limit eksplisit, tapi traffic ekstrem bisa di-throttle. Kalau situs dapat traffic besar, VPS atau Cloudflare Workers lebih sesuai.
  • Compliance. Beberapa regulasi (PII data, PCI-DSS) melarang hosting data di server rumah tanpa sertifikasi. VPS dari provider certified lebih tepat.

Langkah berikutnya

Sistem self-hosting ini butuh backup database yang rutin supaya aman. Cara deploy situs baru ke infrastruktur ini dibahas di auto-deploy WordPress dengan Docker. Konteks lengkap bagaimana AI agent mengelola semua ini ada di AI agent yang deploy website otomatis.

FAQ

Apakah Cloudflare Tunnel bisa dipakai untuk domain selain .dev?

Ya. Cloudflare Tunnel bekerja dengan domain apapun yang DNS-nya dikelola Cloudflare. Yang dibutuhkan: domain terdaftar, nameserver mengarah ke Cloudflare, dan tunnel terkonfigurasi.

Berapa banyak situs yang bisa di-host di satu server?

Dibatasi RAM, bukan software. nginx bisa menangani ratusan server block tanpa masalah. Container dan MariaDB adalah yang membatasi. Dengan 16GB RAM, 2-3 situs WordPress aktif, atau 5-8 situs Next.js static.

Apakah traffic ke situs bisa diinspeksi oleh Cloudflare?

Ya, Cloudflare melihat traffic yang melalui edge mereka (karena TLS terminated di edge). Ini implikasi privacy yang harus dipahami. Untuk situs dengan data sensitif, pertimbangkan mode "Full (Strict)" dengan cert origin sendiri, atau tunnel terenkripsi end-to-end.

Bagaimana kalau Cloudflare down?

Semua situs tidak accessible dari publik. Container tetap jalan di local network dan bisa diakses via LAN. Ini risiko ketergantungan pihak ketiga yang harus diterima, atau dimitigasi dengan jalur alternatif (VPS backup, Tailscale).

Kesimpulan

Self-hosting puluhan situs di satu server dengan Cloudflare Tunnel menyelesaikan tiga masalah sekaligus: tidak ekspos IP rumah, TLS otomatis, dan menambah situs baru hampir nol biaya. Trade-off utama: ketergantungan pada Cloudflare dan batas resource host. Untuk homelab atau klien kecil, trade-off ini sepadan.

Langkah berikutnya yang logical: pastikan data aman dengan backup database otomatis.

More Articles