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
- Nginx installed
- Root or sudo access
- DNS records for each domain pointing to the server’s IP (or, for local testing, entries in your local
/etc/hostsfile)
Directory Structure
On Debian/Ubuntu, Nginx conventionally uses two directories:
/etc/nginx/sites-available/— where I create the configuration file for every site, whether active or not/etc/nginx/sites-enabled/— contains symlinks to the files insites-availablethat are actually active; Nginx only loads configs from here (referenced via anincludeline innginx.conf)
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:
- Exact match on
server_name(e.g.,example.commatches exactly) - Wildcard match starting with
*(e.g.,*.example.com) - Wildcard match ending with
*(e.g.,www.*) - Regular expression match (using
~prefix) - If nothing matches, Nginx falls back to the first server block defined for that
listenport, or one explicitly markeddefault_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
- Always define an explicit
default_serverthat rejects or ignores unmatched requests, rather than letting whichever site happens to load first silently catch stray traffic. - Keep each site’s
access_loganderror_logseparate, as shown above — this makes it much easier to audit traffic and diagnose issues per site instead of digging through one combined log. - Set appropriate file permissions on each site’s web root so that one compromised site’s PHP/application code can’t read or write another site’s files if they’re both running under the same user.
Performance Tips
- Server blocks themselves add negligible overhead — Nginx’s
server_namematching is fast even with dozens of virtual hosts defined. - For servers hosting many sites, keep each site’s configuration in its own file (as shown) rather than one giant
nginx.conf— this doesn’t affect runtime performance, but it dramatically improves maintainability and reduces the chance of a typo breaking every site at once. - Use
includestatements to share common configuration (like security headers or gzip settings) across all server blocks without duplicating them in every file:
# /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
- Hosting a company’s main website and blog on separate subdomains from a single VPS.
- Running multiple independent client sites on one shared server for cost efficiency.
- Separating a staging environment (
staging.example.com) from production (example.com) on the same infrastructure. - Multi-tenant SaaS applications where each customer gets a dynamically routed subdomain.
Best Practices
- Keep one file per site, named after the domain, for clarity and easy management.
- Always symlink from
sites-availabletosites-enabledrather than editing files directly insidesites-enabled— this preserves a clean record of every site the server is capable of hosting, even temporarily disabled ones. - Explicitly define a
default_serverto control fallback behavior instead of leaving it to chance. - Separate logs per site for easier auditing and debugging.
- Test configuration syntax with
nginx -tbefore every reload, without exception.
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.