| |

LFCE 47 ๐Ÿง Nginx โ€” Virtual Hosts

โฑ๏ธ Estimated read: 13 min | |

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

DirectivePurposeExample
listenAddress and portlisten 80;
listen ... default_serverFallback for unmatched nameslisten 80 default_server;
server_nameHostnames matched against Hostserver_name example.com;
server_name *.example.orgWildcard subdomainsMatches www.example.org
server_name .example.orgApex plus wildcardMatches example.org and *.example.org
server_name ~regexRegular expression~^(?<tenant>.+)\.example\.net$

Name Matching Order

PriorityTypeExample
1Exact nameexample.com
2Longest wildcard starting with **.example.org
3Longest wildcard ending with *mail.*
4First matching regex~^www\d+\.example\.net$
5Default serverdefault_server on listen

Directory Conventions

DistributionDirectoryEnablement
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

PitfallWhy It HappensFix
Wrong site servedserver_name mismatch or default server selectionCheck Host header and default_server
Subdomain not matchedWildcard only matches one levelUse regex or .example.org
Apex not matched by wildcard*.example.org excludes example.orgUse .example.org
Duplicate default_serverTwo blocks marked default on same portRemove one
Configuration not loadedSite file not in sites-enabled or conf.dCreate symlink or move file
nginx -t passes but site failsroot path wrong or permissionsVerify 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

ItemValue
Virtual host containerserver block
Selection fieldHost header matched against server_name
Matching orderExact, * prefix, * suffix, regex
Wildcard boundaryOnly at start or end, on a dot
Apex plus subdomains.example.org
Default serverlisten ... default_server;
Default server name_ (placeholder)
Debian directorysites-available/ โ†’ sites-enabled/
RHEL directoryconf.d/
Validationnginx -t
Testcurl -H "Host: ..."

Key takeaways:

  • Nginx selects the virtual host by the Host header. The server_name directive 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 listen port. 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.org is valid and matches subdomains. www.*.example.org is invalid. The special form .example.org matches both the apex and the subdomains .
  • The directory convention differs by distribution. Debian uses sites-available and sites-enabled with symlinks. RHEL uses conf.d with automatic inclusion. The configuration content is the same; the location and enablement differ .
  • nginx -t validates the entire configuration. It catches syntax errors and duplicate default_server declarations before the reload. The nginx -T command dumps the full configuration for inspection.
  • Testing requires the Host header. curl -H "Host: example.com" http://localhost simulates 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.


Was this helpful?


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!