LFCE 47 ๐ง Nginx โ Virtual Hosts
Nginx virtual hosts are the mechanism that allows a single server to serve multiple domains. The previous chapter covered the server block as the container for one site. A virtual host is a server block that is distinguished from the others by its server_name directive. When a request arrives, nginx reads the Host header and compares it to the server_name values in every block that listens on the matching address and port. The block whose name matches handles the request. If no name matches, the request goes to the default server for that port .
This chapter covers the virtual host configuration: the directory conventions that differ between Debian and RHEL, the server_name matching rules that determine which block is selected, the wildcard and regular-expression forms, and the catch-all block that handles requests for unknown hostnames. It builds on the basic configuration from the previous chapter; the listen, root, and location directives are already established, and this chapter adds the multi-site dimension.
Key point: Nginx selects the virtual host by matching the request’s Host header against the server_name directives. The matching order is: exact name, longest wildcard starting with *, longest wildcard ending with *, then the first matching regular expression . If no name matches, the request goes to the default_server for that port. The default_server parameter is a property of the listen directive, not of the server_name, and only one block per address-port pair can have it .
Why virtual hosts require understanding name matching
The name-precedence problem.
A configuration can contain multiple server blocks that could match a request. One block has server_name example.com;, another has server_name *.example.com;, and a third has a regular expression that also matches. Nginx does not evaluate them in the order they appear in the file. It evaluates exact names first, then wildcards starting with an asterisk, then wildcards ending with an asterisk, then regular expressions in the order they appear . A request for example.com matches the exact name and never reaches the wildcard or the regex, regardless of where those blocks are placed in the file.
The default-server problem.
The default_server parameter designates the block that handles requests that match no other name. It is set on the listen directive, not on server_name. listen 80 default_server; makes that block the fallback for port 80. If no block has the parameter, the first block that listens on that port becomes the default . This is why the order of blocks matters when default_server is not explicitly set: the first block in the configuration is the one that catches unmatched requests, and an administrator who adds a new site at the top of the file inadvertently changes which site handles unknown hostnames.
The wildcard-boundary problem.
A wildcard name can contain an asterisk only at the start or the end, and only on a dot boundary. *.example.org is valid, and it matches www.example.org and www.sub.example.org. www.*.example.org is invalid because the asterisk is not at a boundary. The special form .example.org matches both the exact name example.org and the wildcard *.example.org . A wildcard that is placed incorrectly produces a configuration that nginx refuses to load, or a pattern that does not match the intended hosts.
a. The directory conventions for virtual hosts
The location of the virtual host configuration depends on the distribution. Debian and Ubuntu use sites-available and sites-enabled; RHEL and its derivatives use conf.d.
# Debian / Ubuntu: create the file in sites-available
sudo tee /etc/nginx/sites-available/example.com.conf > /dev/null <<'EOF'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
EOF
# Enable the site by creating a symlink in sites-enabled
sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/
# RHEL / Rocky / Fedora: create the file in conf.d
sudo tee /etc/nginx/conf.d/example.com.conf > /dev/null <<'EOF'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
EOF
The Debian convention separates the available configurations from the enabled ones. A configuration file in sites-available is inert until a symlink in sites-enabled points to it. This allows a site to be disabled by removing the symlink without deleting the configuration. The RHEL convention is simpler: every .conf file in conf.d is included by the main nginx.conf, and there is no enable/disable step .
b. Server name matching and the default server
The server_name directive accepts exact names, wildcard names, and regular expressions. The matching order determines which block handles a request when multiple blocks could match.
# Exact name: matches only example.com and www.example.com
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example;
}
# Wildcard: matches any subdomain of example.org
server {
listen 80;
server_name *.example.org;
root /var/www/example-org;
}
# Regular expression: matches tenant subdomains
server {
listen 80;
server_name ~^(?<tenant>[a-z0-9]+)\.example\.net$;
root /var/www/tenants/$tenant;
}
# Default server: catches requests that match no other name
server {
listen 80 default_server;
server_name _;
return 444;
}
The exact name has the highest precedence. A request for example.com matches the first block, even if the wildcard block appears earlier in the file. The wildcard *.example.org matches www.example.org and api.example.org, but not example.org itself; the special form .example.org is required to match both the apex and the subdomains . The regular expression captures the tenant name and uses it in the root path, which allows a single block to serve multiple tenants with distinct content directories.
The default server block uses server_name _;, which is a conventional placeholder that does not match any real hostname. The return 444; directive closes the connection without sending a response, which is the standard way to reject requests for unknown hostnames . An alternative is to return a 404 or redirect to a known domain, depending on the desired behavior.
c. Validating and testing virtual hosts
The nginx -t command validates the entire configuration, including all virtual host files. It catches syntax errors and duplicate default_server declarations before the reload.
# Validate the configuration
sudo nginx -t
# Output: nginx: configuration file /etc/nginx/nginx.conf test is successful
# Reload to apply the new virtual host
sudo systemctl reload nginx
# Test with the Host header of the virtual host
curl -H "Host: example.com" http://localhost
# Output: the content served by the example.com block
# Test a subdomain
curl -H "Host: www.example.com" http://localhost
# Test an unknown hostname; it goes to the default server
curl -I -H "Host: unknown.com" http://localhost
# Output depends on the default server configuration
The Host header in the curl command simulates a request for the named virtual host. Without it, curl sends the IP address or localhost as the host, which may match a different block. The nginx -T command dumps the entire configuration, including all included files, which is useful for confirming that the virtual host file is actually being read .
Complete Example Session
# ============================================
# PART 1: CREATE THE SITE DIRECTORIES
# ============================================
sudo mkdir -p /var/www/example.com /var/www/api.example.com /var/www/default
echo "<h1>Main Site</h1>" | sudo tee /var/www/example.com/index.html
echo "<h1>API</h1>" | sudo tee /var/www/api.example.com/index.html
echo "<h1>Default</h1>" | sudo tee /var/www/default/index.html
# ============================================
# PART 2: CREATE THE MAIN VIRTUAL HOST
# ============================================
sudo tee /etc/nginx/sites-available/example.com.conf > /dev/null <<'EOF'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
EOF
# ============================================
# PART 3: CREATE THE API VIRTUAL HOST
# ============================================
sudo tee /etc/nginx/sites-available/api.example.com.conf > /dev/null <<'EOF'
server {
listen 80;
server_name api.example.com;
root /var/www/api.example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
EOF
# ============================================
# PART 4: CREATE THE DEFAULT SERVER
# ============================================
sudo tee /etc/nginx/sites-available/default.conf > /dev/null <<'EOF'
server {
listen 80 default_server;
server_name _;
root /var/www/default;
index index.html;
}
EOF
# ============================================
# PART 5: ENABLE THE SITES
# ============================================
sudo ln -sf /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/api.example.com.conf /etc/nginx/sites-enabled/
sudo ln -sf /etc/nginx/sites-available/default.conf /etc/nginx/sites-enabled/
# Remove the distribution's default site if it conflicts
sudo rm -f /etc/nginx/sites-enabled/default
# ============================================
# PART 6: VALIDATE THE CONFIGURATION
# ============================================
sudo nginx -t
# Output: syntax is ok, test is successful
# ============================================
# PART 7: RELOAD NGINX
# ============================================
sudo systemctl reload nginx
# ============================================
# PART 8: TEST THE MAIN SITE
# ============================================
curl -H "Host: example.com" http://localhost
# Output: <h1>Main Site</h1>
# ============================================
# PART 9: TEST THE API SITE
# ============================================
curl -H "Host: api.example.com" http://localhost
# Output: <h1>API</h1>
# ============================================
# PART 10: TEST THE DEFAULT SERVER
# ============================================
curl -H "Host: unknown.com" http://localhost
# Output: <h1>Default</h1>
The ten parts demonstrate the complete virtual host configuration: content directories, the main site block, the API block, the default server, the enablement step, validation, reload, and the three test cases. The critical verification is Part 10: a request for an unknown hostname reaches the default server, which confirms that the default_server parameter is working.
Quick Reference
Virtual Host Directives
| Directive | Purpose | Example |
|---|---|---|
listen | Address and port | listen 80; |
listen ... default_server | Fallback for unmatched names | listen 80 default_server; |
server_name | Hostnames matched against Host | server_name example.com; |
server_name *.example.org | Wildcard subdomains | Matches www.example.org |
server_name .example.org | Apex plus wildcard | Matches example.org and *.example.org |
server_name ~regex | Regular expression | ~^(?<tenant>.+)\.example\.net$ |
Name Matching Order
| Priority | Type | Example |
|---|---|---|
| 1 | Exact name | example.com |
| 2 | Longest wildcard starting with * | *.example.org |
| 3 | Longest wildcard ending with * | mail.* |
| 4 | First matching regex | ~^www\d+\.example\.net$ |
| 5 | Default server | default_server on listen |
Directory Conventions
| Distribution | Directory | Enablement |
|---|---|---|
| Debian / Ubuntu | /etc/nginx/sites-available/ | Symlink to sites-enabled/ |
| RHEL / Rocky | /etc/nginx/conf.d/ | Automatic include |
Best Practices
โ Do This:
# Use exact names for the primary domains
server_name example.com www.example.com; # โ
# Use .example.org to match apex and subdomains
server_name .example.org; # โ
# Define an explicit default_server
listen 80 default_server; # โ
# Use a placeholder name for the default server
server_name _; # โ
# Validate before reloading
nginx -t # โ
# Test with the Host header
curl -H "Host: example.com" http://localhost # โ
โ Don’t Do This:
# Use an invalid wildcard in the middle
server_name www.*.example.org; # Invalid; use regex # โ
# Rely on block order for the default server
# The first block becomes the default if none is marked # โ
# Duplicate default_server on the same port
listen 80 default_server; # Twice โ nginx -t fails # โ
# Forget to test with the Host header
curl http://localhost # May hit the wrong block # โ
# Omit the trailing semicolon
server_name example.com # Syntax error # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Wrong site served | server_name mismatch or default server selection | Check Host header and default_server |
| Subdomain not matched | Wildcard only matches one level | Use regex or .example.org |
| Apex not matched by wildcard | *.example.org excludes example.org | Use .example.org |
Duplicate default_server | Two blocks marked default on same port | Remove one |
| Configuration not loaded | Site file not in sites-enabled or conf.d | Create symlink or move file |
nginx -t passes but site fails | root path wrong or permissions | Verify path and permissions |
Real-World Examples
1. Exact name virtual host
server { listen 80; server_name example.com; root /var/www/example; }
2. Wildcard subdomains
server { listen 80; server_name *.example.org; root /var/www/org; }
3. Apex plus wildcard
server { listen 80; server_name .example.org; root /var/www/org; }
4. Regex tenant capture
server { listen 80; server_name ~^(?<tenant>[a-z0-9]+)\.example\.net$; root /var/www/$tenant; }
5. Default server
server { listen 80 default_server; server_name _; return 444; }
6. Debian enablement
ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/
7. RHEL placement
cp example.com.conf /etc/nginx/conf.d/
8. Validate configuration
nginx -t
9. Test with Host header
curl -H "Host: example.com" http://localhost
10. Dump full configuration
nginx -T | less
Visual
Server Name Matching Order
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NGINX TESTS NAMES IN THIS ORDER โ
โ โ
โ 1. Exact name โ
โ server_name example.com; โ
โ โ Matches only "example.com" โ
โ โ
โ 2. Longest wildcard starting with * โ
โ server_name *.example.org; โ
โ โ Matches www.example.org, api.example.org โ
โ โ
โ 3. Longest wildcard ending with * โ
โ server_name mail.*; โ
โ โ Matches mail.example.com โ
โ โ
โ 4. First matching regex (in file order) โ
โ server_name ~^www\d+\.example\.net$; โ
โ โ
โ 5. Default server โ
โ listen 80 default_server; โ
โ โ Handles anything that matched nothing above โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Directory Layout Comparison
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DEBIAN vs RHEL CONVENTIONS โ
โ โ
โ Debian / Ubuntu: โ
โ /etc/nginx/ โ
โ โโโ sites-available/ โ All site configs โ
โ โ โโโ example.com.conf โ
โ โ โโโ api.example.com.conf โ
โ โโโ sites-enabled/ โ Symlinks to active sites โ
โ โโโ example.com.conf โ ../sites-available/... โ
โ โโโ api.example.com.conf โ ../sites-available/... โ
โ โ
โ RHEL / Rocky: โ
โ /etc/nginx/ โ
โ โโโ conf.d/ โ All .conf files auto-included โ
โ โโโ example.com.conf โ
โ โโโ api.example.com.conf โ
โ โ
โ Enable/disable: Debian uses symlinks. RHEL moves files. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Default Server Selection
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE DEFAULT SERVER HANDLES UNMATCHED Host HEADERS โ
โ โ
โ Request: Host: unknown.com โ
โ โ โ
โ โผ โ
โ Check exact names โ no match โ
โ Check wildcards โ no match โ
โ Check regexes โ no match โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ server { โ โ
โ โ listen 80 default_server; โ โ
โ โ server_name _; โ โ
โ โ return 444; โ โ
โ โ } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The _ is a placeholder; it has no wildcard power. โ
โ The default_server flag is what makes this the fallback. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Virtual host container | server block |
| Selection field | Host header matched against server_name |
| Matching order | Exact, * prefix, * suffix, regex |
| Wildcard boundary | Only at start or end, on a dot |
| Apex plus subdomains | .example.org |
| Default server | listen ... default_server; |
| Default server name | _ (placeholder) |
| Debian directory | sites-available/ โ sites-enabled/ |
| RHEL directory | conf.d/ |
| Validation | nginx -t |
| Test | curl -H "Host: ..." |
Key takeaways:
- Nginx selects the virtual host by the
Hostheader. Theserver_namedirective defines the hostnames that the block responds to. The matching order is exact, wildcard-prefix, wildcard-suffix, then regex, and the first match wins . - The default server is a property of the
listenport.listen 80 default_server;designates the block that handles requests that match no other name. Without it, the first block for that port is the default . - Wildcards have strict boundaries.
*.example.orgis valid and matches subdomains.www.*.example.orgis invalid. The special form.example.orgmatches both the apex and the subdomains . - The directory convention differs by distribution. Debian uses
sites-availableandsites-enabledwith symlinks. RHEL usesconf.dwith automatic inclusion. The configuration content is the same; the location and enablement differ . nginx -tvalidates the entire configuration. It catches syntax errors and duplicatedefault_serverdeclarations before the reload. Thenginx -Tcommand dumps the full configuration for inspection.- Testing requires the
Hostheader.curl -H "Host: example.com" http://localhostsimulates a request for the named virtual host. Without the header, the request may match a different block or the default server.
Remember: A virtual host is a server block with a server_name that distinguishes it from the others. The Host header selects the block, and the matching order determines which block wins when multiple names could apply. The default server catches the requests that match nothing. The directory convention determines where the file lives and how it is enabled. Validate with nginx -t, reload with systemctl reload nginx, and test with curl and the correct Host header. A configuration that parses is not necessarily a configuration that serves the right site; the test confirms both.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!