Virtual Host #

Virtual hosting is the ability to run several different websites from one physical server. This isn’t a luxury feature — it’s standard practice. A single $20/month VPS can manage dozens of websites at once with excellent performance, as long as the configuration is right.

Nginx handles virtual hosting very efficiently. Each “virtual host” in Nginx is represented by a server block, and Nginx decides which server block responds to each request based on a combination of IP address, port, and the Host header.

How Nginx Selects a Server Block #

Understanding the server block selection algorithm is critical to avoiding bugs that are hard to debug. Nginx evaluates incoming requests in this order:

flowchart TD
    A["Request arrives\nGET / HTTP/1.1\nHost: example.com"] --> B{"Match IP:port\nagainst listen directives"}
    B -- "No match" --> C["Drop connection /\nlook for another port"]
    B -- "Match" --> D{"From all server blocks\nmatching IP:port,\nmatch server_name\nagainst the Host header"}
    D -- "Exact match\nexample.com" --> E["Use this server block"]
    D -- "Wildcard prefix\n*.example.com" --> F["Use this server block"]
    D -- "Wildcard suffix\nexample.*" --> G["Use this server block"]
    D -- "Regex match\n~^www\\.example\\.com$" --> H["Use this server block"]
    D -- "Nothing matches" --> I["Use the server block\nwith default_server\nor the first server block"]

server_name selection priority (highest to lowest):

PriorityTypeExample
1Exact matchserver_name example.com;
2Wildcard prefix (longest)server_name *.example.com;
3Wildcard suffix (longest)server_name example.*;
4Regex (order of appearance)server_name ~^www\\.example\\.com$;
5Default serverlisten 80 default_server;

Name-Based Virtual Hosting #

Name-based virtual hosting is the industry standard today. One IP address, many domains — Nginx distinguishes them by the Host header of the 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;
    }
}

Both run on the same port 80. When a request arrives with the header Host: example.com, Nginx responds from the first server block. When it’s Host: another.com, from the second server block.

server_name with Wildcards #

# Matches all subdomains of example.com
server {
    listen 80;
    server_name example.com *.example.com;

    # www.example.com, blog.example.com, api.example.com — all match
    root /var/www/example.com/html;
}

# More specific: you can separate out a particular subdomain
server {
    listen 80;
    server_name api.example.com;

    # api.example.com will match here first (exact match has higher priority)
    location / {
        proxy_pass http://localhost:3000;
    }
}

Directory Structure for Multi-Site #

For servers managing many sites, consistent directory structure really helps with debugging and long-term management. Here’s the recommended structure:

/var/www/
├── example.com/
│   ├── html/          ← document root (only public files here)
│   ├── logs/          ← site-specific logs (optional)
│   └── cache/         ← proxy/fastcgi cache (if any)
│
├── blog.example.com/
│   ├── html/
│   └── logs/
│
└── api.example.com/
    └── html/          ← APIs usually have no static files

/etc/nginx/
├── nginx.conf
├── conf.d/
│   ├── example.com.conf
│   ├── blog.example.com.conf
│   └── api.example.com.conf
└── snippets/          ← reusable configuration (SSL params, security headers)
    ├── ssl-params.conf
    └── security-headers.conf

Why html/ as a subdirectory of the domain directory (rather than directly /var/www/domain.com)? It lets us store other files (logs, cache, private config) outside the document root without risking public exposure.


Virtual Hosts with HTTPS and SNI #

HTTPS with multiple sites is made possible by SNI (Server Name Indication) — a TLS extension that lets the client send the domain name it’s targeting before the TLS handshake completes. This is what allows Nginx to serve different SSL certificates for different domains on a single IP address.

sequenceDiagram
    participant C as Client (Browser)
    participant N as Nginx

    C->>N: TCP Connect to 443
    C->>N: TLS ClientHello\n(SNI: example.com)
    Note over N: Select the certificate\nfor example.com
    N->>C: TLS ServerHello\n(example.com certificate)
    C->>N: Complete TLS Handshake
    C->>N: HTTP GET / Host: example.com
    N->>C: HTTP 200 Response

Multi-Site HTTPS Configuration #

# ─── HTTP → HTTPS redirect (can be one block for all domains) ─────────────
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 reusable SSL parameters (see below)
    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;

    # Can use a separate certificate
    ssl_certificate     /etc/letsencrypt/live/blog.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;

    # Or use a wildcard *.example.com covering all subdomains
    # 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 to a 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) #

Creating an SSL parameter snippet shared by all virtual hosts avoids duplication and makes updating SSL parameters in one place easy:

# /etc/nginx/snippets/ssl-params.conf

# Protocols — only TLSv1.2 and TLSv1.3 (TLSv1.0/1.1 are deprecated)
ssl_protocols TLSv1.2 TLSv1.3;

# Safe cipher suites (Mozilla Modern compatible)
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 — reduce TLS handshake overhead for repeat connections
ssl_session_cache   shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

# OCSP Stapling — certificate validation without a round trip to the CA
ssl_stapling        on;
ssl_stapling_verify on;
resolver            8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout    5s;

# HSTS — tell browsers to always use HTTPS (minimum 6 months)
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains" always;

Wildcard Certificates: One Certificate for All Subdomains #

A wildcard certificate (*.example.com) covers all subdomains one level below the main domain. This is very useful if you have many subdomains or dynamic subdomains:

*.example.com covers:
  ✓ www.example.com
  ✓ blog.example.com
  ✓ api.example.com
  ✓ staging.example.com
  ✗ www.sub.example.com  (two levels down, not covered)
  ✗ example.com          (the main domain, not covered — needs a separate SAN)

A wildcard certificate from Let’s Encrypt requires a DNS challenge, not an HTTP challenge. This can be done with Certbot:

# Request a wildcard certificate
# Requires a DNS plugin matching our DNS provider
sudo certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
    -d example.com \
    -d "*.example.com"
# Use one wildcard certificate for all subdomains
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;
    # This certificate covers *.example.com AND example.com (if requested together)
}

server {
    listen 443 ssl;
    server_name blog.example.com;

    # The same certificate!
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

IP-Based Virtual Hosting #

If the server has multiple IP addresses, you can separate sites by IP — without looking at the Host header at all:

# Site on the first IP
server {
    listen 192.168.1.10:80;
    root /var/www/site-a;
    # ...
}

# Site on the second IP
server {
    listen 192.168.1.11:80;
    root /var/www/site-b;
    # ...
}

# Can mix with name-based on the same IP
server {
    listen 192.168.1.10:80;
    server_name special.example.com;
    root /var/www/special;
}

IP-based virtual hosting is rarely used today because:

  • IPv4 is limited and expensive — it’s not economical to allocate one IP per site
  • Wildcard and SAN certificates already cover almost all multi-domain needs
  • SNI enables multi-site HTTPS on a single IP

Use cases that are still relevant: clients requiring a dedicated IP for certain compliance needs, or legacy systems that don’t support SNI.


Default Server: Handling Unmatched Requests #

default_server is the server block chosen when no server_name matches the Host header of a request:

# Catch all requests that don't match any virtual host
server {
    listen 80 default_server;
    server_name _;  # _ is an invalid name — never matches a real host

    # Option 1: Reject immediately
    return 444;  # 444 = close the connection without sending a response (Nginx extension)

    # Option 2: Redirect to the main site
    # return 301 https://example.com;

    # Option 3: Info page
    # root /var/www/default;
}
Always define a default_server. Without it, requests with unrecognized Host headers will be answered by the first server block in config file order (based on alphabetical file name order). This can expose unwanted content to invalid requests.

Logs per Virtual Host #

Separating logs per virtual host is a highly recommended practice. Imagine debugging a problem on example.com when all logs from 20 sites are mixed in one file.

http {
    # ─── Custom log format (more informative than the default combined) ─────────
    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 '  # response time
        '$host';                                   # add the domain

    server {
        server_name example.com;

        # Per-site logs with a more complete format
        access_log /var/log/nginx/example.com-access.log main_extended;
        error_log  /var/log/nginx/example.com-error.log warn;

        # ...
    }
}

Log Rotation #

For logs that can grow large, make sure logrotate is configured:

# /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
}

Multi-Site Management Workflow #

As the number of sites grows, a structured workflow helps keep your sanity. Here’s the procedure we recommend:

Adding a New Site #

# 1. Create the directory structure
sudo mkdir -p /var/www/newsite.com/{html,logs}
sudo chown -R www-data:www-data /var/www/newsite.com

# 2. Create a placeholder page
echo "<!DOCTYPE html><html><body><h1>Coming Soon</h1></body></html>" \
    | sudo tee /var/www/newsite.com/html/index.html

# 3. Create the Nginx configuration
sudo nano /etc/nginx/conf.d/newsite.com.conf

# 4. Validate the syntax
sudo nginx -t

# 5. Get the SSL certificate
sudo certbot --nginx -d newsite.com -d www.newsite.com

# 6. Reload Nginx (without downtime)
sudo nginx -s reload

# 7. Verify
curl -I https://newsite.com/

Temporarily Disabling a Site #

# Change the extension — the file is no longer included when Nginx reloads
sudo mv /etc/nginx/conf.d/newsite.com.conf \
        /etc/nginx/conf.d/newsite.com.conf.disabled

sudo nginx -t && sudo nginx -s reload

# To re-enable:
sudo mv /etc/nginx/conf.d/newsite.com.conf.disabled \
        /etc/nginx/conf.d/newsite.com.conf
sudo nginx -t && sudo nginx -s reload

Deleting a Site #

# 1. Remove the configuration
sudo rm /etc/nginx/conf.d/newsite.com.conf

# 2. Revoke and remove the certificate (optional)
sudo certbot delete --cert-name newsite.com

# 3. Reload
sudo nginx -t && sudo nginx -s reload

# 4. Remove the directory (after confirming it's no longer needed)
sudo rm -rf /var/www/newsite.com

Complete Configuration: Production Multi-Site Template #

Here’s a template you can use as a starting point for every new site. Save it as /etc/nginx/conf.d/template.conf.example:

# Virtual Host Template — copy and adapt
# 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;

    # Basic security
    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;
        # For SPAs: try_files $uri $uri/ /index.html;
    }

    # Asset cache
    location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|webp|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Block sensitive files
    location ~ /\. {
        deny all;
        log_not_found off;
    }
}

Summary #

  • Name-based virtual hosting is the standard — one IP, many domains, Nginx distinguishes them via the Host header.
  • Server block selection algorithm: exact match → wildcard prefix → wildcard suffix → regex → default_server.
  • Always define a default_server that rejects requests (return 444) to prevent the first server block from being used as an unintended fallback.
  • SNI enables multi-site HTTPS on one IP — every domain can have its own SSL certificate.
  • Wildcard certificates (*.example.com) are ideal for many subdomains — saves CA requests and simplifies management.
  • Separate per-site logs (access_log + error_log per server block) are a must for efficient debugging.
  • SSL snippets (include snippets/ssl-params.conf) reuse TLS parameters without code duplication.

← Previous: Serving Static Files   Next: Root & Alias →

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