Virtual Host

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):

PrioritasJenisContoh
1Exact matchserver_name example.com;
2Wildcard prefix (terpanjang)server_name *.example.com;
3Wildcard suffix (terpanjang)server_name example.*;
4Regex (urutan kemunculan)server_name ~^www\.example\.com$;
5Default serverlisten 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 Response

Konfigurasi 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 definisikan default_server. Tanpanya, request dengan Host yang 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_server yang 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_log per 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 →

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