Virtual Host #
Virtual hosting adalah kemampuan menjalankan beberapa website berbeda dari satu server fisik. Ini bukan fitur mewah — ini adalah praktik standar. Satu server $20/bulan di VPS bisa mengelola puluhan website sekaligus dengan performa yang sangat baik, asalkan konfigurasinya benar.
Nginx menangani virtual hosting dengan sangat efisien. Setiap “virtual host” di Nginx diwakili oleh sebuah server block, dan Nginx memutuskan server block mana yang merespons setiap request berdasarkan kombinasi IP address, port, dan header Host.
Bagaimana Nginx Memilih Server Block #
Pemahaman tentang algoritma pemilihan server block sangat kritis untuk menghindari bug yang sulit di-debug. Nginx mengevaluasi request masuk dengan urutan:
flowchart TD
A["Request masuk\nGET / HTTP/1.1\nHost: example.com"] --> B{"Cocokkan IP:port\ndengan directive listen"}
B -- "Tidak cocok" --> C["Drop connection /\ncari port lain"]
B -- "Cocok" --> D{"Dari semua server block\nyang cocok IP:port,\ncocokkan server_name\ndengan Host header"}
D -- "Exact match\ncontoh.com" --> E["Gunakan server block ini"]
D -- "Wildcard prefix\n*.contoh.com" --> F["Gunakan server block ini"]
D -- "Wildcard suffix\ncontoh.*" --> G["Gunakan server block ini"]
D -- "Regex match\n~^www\\.contoh\\.com$" --> H["Gunakan server block ini"]
D -- "Tidak ada yang cocok" --> I["Gunakan server block\ndengan default_server\natau server block pertama"]Prioritas pemilihan server_name (dari yang tertinggi ke terendah):
| Prioritas | Jenis | Contoh |
|---|---|---|
| 1 | Exact match | server_name example.com; |
| 2 | Wildcard prefix (terpanjang) | server_name *.example.com; |
| 3 | Wildcard suffix (terpanjang) | server_name example.*; |
| 4 | Regex (urutan kemunculan) | server_name ~^www\.example\.com$; |
| 5 | Default server | listen 80 default_server; |
Name-Based Virtual Hosting #
Name-based virtual hosting adalah standar industri saat ini. Satu IP address, banyak domain — Nginx membedakan berdasarkan header Host dari HTTP request.
# /etc/nginx/conf.d/example.com.conf
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html;
access_log /var/log/nginx/example.com-access.log;
error_log /var/log/nginx/example.com-error.log warn;
location / {
try_files $uri $uri/ =404;
}
}
# /etc/nginx/conf.d/another.com.conf
server {
listen 80;
server_name another.com www.another.com;
root /var/www/another.com/html;
index index.html;
access_log /var/log/nginx/another.com-access.log;
error_log /var/log/nginx/another.com-error.log warn;
location / {
try_files $uri $uri/ =404;
}
}
Keduanya berjalan di port 80 yang sama. Ketika request datang dengan header Host: example.com, Nginx merespons dari server block pertama. Ketika Host: another.com, dari server block kedua.
Server_name dengan Wildcard #
# Mencocokkan semua subdomain dari example.com
server {
listen 80;
server_name example.com *.example.com;
# www.example.com, blog.example.com, api.example.com — semua cocok
root /var/www/example.com/html;
}
# Lebih spesifik: bisa pisahkan subdomain tertentu
server {
listen 80;
server_name api.example.com;
# api.example.com akan cocok di sini dulu (exact match lebih tinggi prioritasnya)
location / {
proxy_pass http://localhost:3000;
}
}
Struktur Direktori untuk Multi-Site #
Untuk server yang mengelola banyak site, konsistensi struktur direktori sangat membantu saat debugging dan manajemen jangka panjang. Ini adalah struktur yang direkomendasikan:
/var/www/
├── example.com/
│ ├── html/ ← document root (hanya file public di sini)
│ ├── logs/ ← log khusus site ini (opsional)
│ └── cache/ ← proxy/fastcgi cache (jika ada)
│
├── blog.example.com/
│ ├── html/
│ └── logs/
│
└── api.example.com/
└── html/ ← untuk API biasanya tidak ada static file
/etc/nginx/
├── nginx.conf
├── conf.d/
│ ├── example.com.conf
│ ├── blog.example.com.conf
│ └── api.example.com.conf
└── snippets/ ← konfigurasi reusable (SSL params, security headers)
├── ssl-params.conf
└── security-headers.conf
Mengapa html/ sebagai subdirektori dari domain directory (bukan langsung /var/www/domain.com)? Ini memungkinkan kita menyimpan file lain (log, cache, private config) di luar document root tanpa risiko terekspos ke publik.
Virtual Host dengan HTTPS dan SNI #
HTTPS dengan multi-site dimungkinkan oleh SNI (Server Name Indication) — ekstensi TLS yang memungkinkan client mengirimkan nama domain yang dituju sebelum TLS handshake selesai. Ini yang memungkinkan Nginx melayani sertifikat SSL yang berbeda untuk domain yang berbeda di satu IP address.
sequenceDiagram
participant C as Client (Browser)
participant N as Nginx
C->>N: TCP Connect ke 443
C->>N: TLS ClientHello\n(SNI: example.com)
Note over N: Pilih sertifikat\nuntuk example.com
N->>C: TLS ServerHello\n(sertifikat example.com)
C->>N: Selesaikan TLS Handshake
C->>N: HTTP GET / Host: example.com
N->>C: HTTP 200 ResponseKonfigurasi HTTPS Multi-Site #
# ─── HTTP → HTTPS redirect (bisa satu block untuk semua domain) ─────────────
server {
listen 80;
server_name example.com www.example.com blog.example.com api.example.com;
return 301 https://$host$request_uri;
}
# ─── example.com ─────────────────────────────────────────────────────────────
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Include parameter SSL yang reusable (lihat di bawah)
include /etc/nginx/snippets/ssl-params.conf;
root /var/www/example.com/html;
index index.html;
access_log /var/log/nginx/example.com-access.log;
error_log /var/log/nginx/example.com-error.log warn;
location / {
try_files $uri $uri/ =404;
}
}
# ─── blog.example.com ────────────────────────────────────────────────────────
server {
listen 443 ssl;
http2 on;
server_name blog.example.com;
# Bisa pakai sertifikat terpisah
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
# Atau pakai wildcard *.example.com yang mencakup semua subdomain
# ssl_certificate /etc/letsencrypt/live/example.com-wildcard/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/example.com-wildcard/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;
root /var/www/blog.example.com/html;
location / {
try_files $uri $uri/ =404;
}
}
# ─── api.example.com (reverse proxy ke backend) ──────────────────────────────
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
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;
}
}
SSL Parameters Snippet (Reusable) #
Membuat snippet parameter SSL yang dipakai bersama oleh semua virtual host menghindari duplikasi dan memudahkan update parameter SSL di satu tempat:
# /etc/nginx/snippets/ssl-params.conf
# Protocol — hanya TLSv1.2 dan TLSv1.3 (TLSv1.0/1.1 sudah deprecated)
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites yang aman (Mozilla Modern kompatibel)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Session cache — kurangi overhead TLS handshake untuk koneksi berulang
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling — certificate validation tanpa round trip ke CA
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS — beritahu browser untuk selalu gunakan HTTPS (minimal 6 bulan)
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always;
Wildcard Certificate: Satu Sertifikat untuk Semua Subdomain #
Wildcard certificate (*.example.com) meng-cover semua subdomain satu level di bawah domain utama. Ini sangat berguna jika kita punya banyak subdomain atau subdomain yang dinamis:
*.example.com meng-cover:
✓ www.example.com
✓ blog.example.com
✓ api.example.com
✓ staging.example.com
✗ www.sub.example.com (dua level di bawah, tidak di-cover)
✗ example.com (domain utama, tidak di-cover — perlu dua SAN terpisah)
Wildcard certificate dari Let’s Encrypt membutuhkan DNS challenge, bukan HTTP challenge. Ini bisa dilakukan dengan Certbot:
# Request wildcard certificate
# Membutuhkan DNS plugin yang sesuai dengan provider DNS kita
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d example.com \
-d "*.example.com"
# Gunakan satu sertifikat wildcard untuk semua subdomain
server {
listen 443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Sertifikat ini berlaku untuk *.example.com DAN example.com (jika diminta bersamaan)
}
server {
listen 443 ssl;
server_name blog.example.com;
# Sertifikat yang sama!
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}
IP-Based Virtual Hosting #
Jika server memiliki beberapa IP address, kita bisa memisahkan site berdasarkan IP — tanpa melihat header Host sama sekali:
# Site di IP pertama
server {
listen 192.168.1.10:80;
root /var/www/site-a;
# ...
}
# Site di IP kedua
server {
listen 192.168.1.11:80;
root /var/www/site-b;
# ...
}
# Bisa mix dengan name-based di IP yang sama
server {
listen 192.168.1.10:80;
server_name special.example.com;
root /var/www/special;
}
IP-based virtual hosting jarang digunakan hari ini karena:
- IPv4 terbatas dan mahal — tidak ekonomis mengalokasikan satu IP per site
- Wildcard dan SAN certificate sudah mencakup hampir semua kebutuhan multi-domain
- SNI memungkinkan HTTPS multi-site di satu IP
Kasus penggunaan yang masih relevan: klien yang memerlukan dedicated IP untuk compliance tertentu, atau sistem legacy yang tidak mendukung SNI.
Default Server: Menangani Request yang Tidak Cocok #
default_server adalah server block yang dipilih ketika tidak ada server_name yang cocok dengan header Host dari request:
# Tangkap semua request yang tidak cocok dengan virtual host manapun
server {
listen 80 default_server;
server_name _; # _ adalah nama yang tidak valid — tidak akan pernah cocok dengan real host
# Pilihan 1: Tolak dengan segera
return 444; # 444 = close connection tanpa mengirim respons (Nginx extension)
# Pilihan 2: Redirect ke site utama
# return 301 https://example.com;
# Pilihan 3: Halaman info
# root /var/www/default;
}
Selalu definisikandefault_server. Tanpanya, request denganHostyang tidak dikenali akan direspons oleh server block pertama dalam urutan file config (berdasarkan alphabetical order nama file). Ini bisa mengekspos konten yang tidak diinginkan ke request yang tidak valid.
Log per Virtual Host #
Memisahkan log per virtual host adalah praktik yang sangat direkomendasikan. Bayangkan debugging masalah di example.com ketika semua log dari 20 site tercampur dalam satu file.
http {
# ─── Format log kustom (lebih informatif dari combined default) ─────────
log_format main_extended
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time ' # waktu response
'$host'; # tambah domain
server {
server_name example.com;
# Log per-site dengan format yang lebih lengkap
access_log /var/log/nginx/example.com-access.log main_extended;
error_log /var/log/nginx/example.com-error.log warn;
# ...
}
}
Rotasi Log #
Untuk log yang bisa tumbuh besar, pastikan logrotate dikonfigurasi:
# /etc/logrotate.d/nginx-sites
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
postrotate
nginx -s reopen
endscript
}
Workflow Manajemen Multi-Site #
Ketika jumlah site bertambah, workflow yang terstruktur membantu menjaga sanity. Berikut prosedur yang kita rekomendasikan:
Menambah Site Baru #
# 1. Buat struktur direktori
sudo mkdir -p /var/www/newsite.com/{html,logs}
sudo chown -R www-data:www-data /var/www/newsite.com
# 2. Buat halaman placeholder
echo "<!DOCTYPE html><html><body><h1>Coming Soon</h1></body></html>" \
| sudo tee /var/www/newsite.com/html/index.html
# 3. Buat konfigurasi Nginx
sudo nano /etc/nginx/conf.d/newsite.com.conf
# 4. Validasi syntax
sudo nginx -t
# 5. Dapatkan sertifikat SSL
sudo certbot --nginx -d newsite.com -d www.newsite.com
# 6. Reload Nginx (tanpa downtime)
sudo nginx -s reload
# 7. Verifikasi
curl -I https://newsite.com/
Menonaktifkan Site Sementara #
# Ganti ekstensi — file tidak di-include lagi saat Nginx reload
sudo mv /etc/nginx/conf.d/newsite.com.conf \
/etc/nginx/conf.d/newsite.com.conf.disabled
sudo nginx -t && sudo nginx -s reload
# Untuk mengaktifkan kembali:
sudo mv /etc/nginx/conf.d/newsite.com.conf.disabled \
/etc/nginx/conf.d/newsite.com.conf
sudo nginx -t && sudo nginx -s reload
Menghapus Site #
# 1. Hapus konfigurasi
sudo rm /etc/nginx/conf.d/newsite.com.conf
# 2. Revoke dan hapus sertifikat (opsional)
sudo certbot delete --cert-name newsite.com
# 3. Reload
sudo nginx -t && sudo nginx -s reload
# 4. Hapus direktori (setelah yakin tidak diperlukan)
sudo rm -rf /var/www/newsite.com
Konfigurasi Lengkap: Template Multi-Site Production #
Berikut template yang bisa kita gunakan sebagai titik awal untuk setiap site baru. Simpan sebagai /etc/nginx/conf.d/template.conf.example:
# Template Virtual Host — copy dan sesuaikan
# cp /etc/nginx/conf.d/template.conf.example /etc/nginx/conf.d/DOMAIN.conf
# ─── HTTP → HTTPS Redirect ───────────────────────────────────────────────────
server {
listen 80;
server_name DOMAIN.com www.DOMAIN.com;
return 301 https://$host$request_uri;
}
# ─── HTTPS ───────────────────────────────────────────────────────────────────
server {
listen 443 ssl;
http2 on;
server_name DOMAIN.com www.DOMAIN.com;
# SSL
ssl_certificate /etc/letsencrypt/live/DOMAIN.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/DOMAIN.com/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;
# Directories
root /var/www/DOMAIN.com/html;
index index.html;
# Logging (per-site)
access_log /var/log/nginx/DOMAIN.com-access.log;
error_log /var/log/nginx/DOMAIN.com-error.log warn;
# Keamanan dasar
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Routing
location / {
try_files $uri $uri/ =404;
# Untuk SPA: try_files $uri $uri/ /index.html;
}
# Cache asset
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|webp|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Blokir file sensitif
location ~ /\. {
deny all;
log_not_found off;
}
}
Ringkasan #
- Name-based virtual hosting adalah standar — satu IP banyak domain, Nginx bedakan lewat header
Host.- Algoritma pemilihan server block: exact match → wildcard prefix → wildcard suffix → regex → default_server.
- Selalu definisikan
default_serveryang menolak request (return 444) untuk mencegah server block pertama dijadikan fallback tanpa disengaja.- SNI memungkinkan HTTPS multi-site di satu IP — setiap domain bisa punya sertifikat SSL sendiri.
- Wildcard certificate (
*.example.com) ideal untuk banyak subdomain — hemat request ke CA dan memudahkan manajemen.- Log terpisah per-site (
access_log+error_logper server block) adalah praktik wajib untuk debugging yang efisien.- Snippet SSL (
include snippets/ssl-params.conf) untuk reuse parameter TLS tanpa duplikasi kode.
← Sebelumnya: Serving Static Files Berikutnya: Root & Alias →