How to Configure Nginx Server Blocks (Virtual Hosts)

How to Configure Nginx Server Blocks (Virtual Hosts)

How to Configure Nginx Server Blocks (Virtual Hosts)

One of the first things I set up on any fresh Nginx installation, before touching SSL or reverse proxying, is proper server block structure. Server blocks are Nginx’s equivalent of Apache’s virtual hosts — they let a single server, with a single IP address, host multiple independent websites, each with its own domain, document root, and configuration. Once I have a clean, consistent structure for this, adding a new site later is a five-minute job instead of a source of config sprawl.

In this guide, I’ll walk through how server blocks work, how Nginx decides which one handles an incoming request, the directory structure I use to keep things organized, and how to add, test, and troubleshoot multiple sites on the same server.

What a Server Block Actually Is

A server block in Nginx configuration defines how to handle requests for a particular domain, port, or IP combination. When a request arrives, Nginx examines the Host header and matches it against the server_name values defined across all loaded server blocks to decide which one should handle the request.

This is what lets a single server running one Nginx process serve example.com, blog.example.com, and anotherdomain.com simultaneously, each with completely separate document roots, logging, and behavior.

Requirements

Directory Structure

On Debian/Ubuntu, Nginx conventionally uses two directories:

On RHEL/CentOS/Fedora, there’s typically no sites-available/sites-enabled split by default — instead, everything goes directly into /etc/nginx/conf.d/, with each file ending in .conf and automatically included. I’ll cover both.

Step 1: Create the Web Root Directory

For each new site, I create a dedicated directory to hold its files:

sudo mkdir -p /var/www/example.com/html
sudo mkdir -p /var/www/blog.example.com/html

I set ownership so my deployment user (not necessarily root) can manage the files:

sudo chown -R $USER:$USER /var/www/example.com/html
sudo chmod -R 755 /var/www/example.com

I drop in a placeholder page to confirm things are working before deploying real content:

echo "<h1>example.com is working</h1>" | sudo tee /var/www/example.com/html/index.html

Step 2: Create the Server Block (Debian/Ubuntu)

sudo nano /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;
    root /var/www/example.com/html;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    access_log /var/log/nginx/example.com.access.log;
    error_log  /var/log/nginx/example.com.error.log;
}

Step 3: Enable the Server Block (Debian/Ubuntu)

I create a symlink from sites-available into sites-enabled:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

Most default Nginx installs on Debian/Ubuntu ship with a default server block already enabled. I usually disable it once I have real sites configured, to avoid ambiguity about which site handles unmatched requests:

sudo rm /etc/nginx/sites-enabled/default

Step 3 (Alternative): RHEL/CentOS/Fedora Setup

On RHEL-based systems, I skip the symlink step entirely and just place the config directly:

sudo nano /etc/nginx/conf.d/example.com.conf

The content of the file is identical to the Debian example above — RHEL-based Nginx automatically includes every .conf file in /etc/nginx/conf.d/ via the main nginx.conf.

Step 4: Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Step 5: Adding a Second Site

The whole point of server blocks is handling multiple sites cleanly. Adding blog.example.com follows the exact same pattern:

sudo nano /etc/nginx/sites-available/blog.example.com
server {
    listen 80;
    listen [::]:80;

    server_name blog.example.com;
    root /var/www/blog.example.com/html;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ =404;
    }

    access_log /var/log/nginx/blog.example.com.access.log;
    error_log  /var/log/nginx/blog.example.com.error.log;
}
sudo ln -s /etc/nginx/sites-available/blog.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Now the same Nginx instance, on the same IP, correctly serves two entirely independent sites based on the Host header of each incoming request.

How Nginx Matches Server Blocks

This is the part that confuses people the most, so I want to be precise about it. When a request comes in, Nginx picks a server block using this order of precedence:

  1. Exact match on server_name (e.g., example.com matches exactly)
  2. Wildcard match starting with * (e.g., *.example.com)
  3. Wildcard match ending with * (e.g., www.*)
  4. Regular expression match (using ~ prefix)
  5. If nothing matches, Nginx falls back to the first server block defined for that listen port, or one explicitly marked default_server

Because of that last rule, I always explicitly mark one server block as the default for a given IP/port combination, to make behavior predictable rather than accidental:

server {
    listen 80 default_server;
    server_name _;
    return 444;  # close connection without a response for unmatched requests
}

Using server_name _; combined with return 444; is a pattern I use specifically to silently drop requests that don’t match any real hostname — useful for blocking scanners and bots that connect via raw IP address instead of a proper domain name, since it denies them a response entirely rather than serving default content that leaks information about the server.

Handling Wildcard Subdomains

If I want a single server block to catch all subdomains rather than creating one per subdomain:

server {
    listen 80;
    server_name *.example.com;
    root /var/www/wildcard/html;
    index index.html;
}

This is useful for multi-tenant applications where subdomains are generated dynamically (like tenant1.example.com, tenant2.example.com) and I don’t want to create a server block manually for each one.

Complete Example: Multiple Sites with SSL

Here’s a fuller, realistic setup combining two independent sites, each with SSL, on the same server:

# example.com
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

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;

    root /var/www/example.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

# blog.example.com
server {
    listen 80;
    server_name blog.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;

    root /var/www/blog.example.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Each domain needs its own certificate (or a single multi-domain/wildcard certificate covering both), obtained separately via Certbot.

Testing Multiple Server Blocks Locally

Before DNS is even set up, I test server block routing locally by editing /etc/hosts on my own machine:

127.0.0.1   example.com
127.0.0.1   blog.example.com

Then, on the server itself:

curl -H "Host: example.com" http://localhost/
curl -H "Host: blog.example.com" http://localhost/

This confirms Nginx is routing based on the Host header correctly, independent of actual DNS resolution — extremely useful for testing before a domain has propagated.

Troubleshooting Common Issues

Wrong site loads for a given domain. Almost always a server_name matching issue — check for typos, or another server block unintentionally catching the request first due to default_server ordering.

nginx: [warn] conflicting server name "example.com" on 0.0.0.0:80, ignored. This means two different server blocks define the same server_name on the same port. Nginx just uses the first one and warns about the rest — I search for duplicate definitions, often left behind from a leftover default config.

New site returns the default Nginx welcome page instead of my content. This usually means my new server block wasn’t actually enabled (missing symlink in sites-enabled, or the config file doesn’t end in .conf on RHEL-based systems), or the default server block is still catching requests because mine isn’t marked correctly and comes after it in load order.

Changes to a server block aren’t taking effect. Confirm I edited the file in sites-available (which is the one that’s actually referenced), not a stray duplicate, and that I reloaded Nginx after saving:

sudo nginx -t && sudo systemctl reload nginx

Security Considerations

Performance Tips

# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
server {
    include snippets/security-headers.conf;
    ...
}

Real-World Use Cases

Best Practices

Wrapping Up

Server blocks are the foundation everything else in Nginx builds on top of — SSL, reverse proxying, rate limiting, all of it gets configured within this same per-site structure. Once I have a clean sites-available/sites-enabled pattern (or the equivalent conf.d structure on RHEL-based systems) with one file per domain, scaling from one site to a dozen on the same server becomes a matter of copying a template file and changing a few lines, not fighting with a single sprawling configuration.

Exit mobile version