Nginx Server Blocks: Host Multiple Domains on One VPS

9 min read·Matthieu|

Configure Nginx server blocks to serve multiple websites from a single VPS. Two full domains, secure defaults, per-site logging, and verification at every step.

A single VPS can serve dozens of websites. Nginx uses server blocks to decide which site to serve based on the domain name in the request. This guide walks through the full setup: two domains configured from scratch, secure defaults, per-site logging, and verification at every step.

This tutorial targets Debian 12 and Ubuntu 24.04. Both use the same sites-available/sites-enabled pattern. Commands work identically on either OS.

What is an Nginx server block?

A server block is a configuration block inside Nginx that defines how requests for a specific domain are handled. It is the Nginx equivalent of Apache's virtual host. Each server block uses the listen directive to bind to a port and the server_name directive to match the domain name from the request's Host header. You can have as many server blocks as you need, all sharing port 80 or 443.

Prerequisites

Before starting, you need:

  • Nginx installed and running ([-> install-nginx-debian-ubuntu])
  • Two domain names with DNS A records pointing to your server's IP
  • A firewall allowing ports 80 and 443 ([-> nginx-administration-vps])
  • SSH access as a non-root user with sudo

Verify Nginx is running:

sudo systemctl status nginx

You should see active (running) in the output. If not, start and enable it:

sudo systemctl enable --now nginx

The enable flag makes Nginx start automatically after reboots. The --now flag starts it immediately.

Verify your DNS records resolve to your server. Replace the domains with your own throughout this guide:

dig +short site-one.com
dig +short site-two.com

Both should return your server's public IP address.

How do you create a server block for a new domain?

The process has three parts: create the document root directory, set permissions, and write the server block config file. We will set up two domains: site-one.com and site-two.com.

What directory structure should you use?

Create a separate document root for each site under /var/www/:

sudo mkdir -p /var/www/site-one.com/html
sudo mkdir -p /var/www/site-two.com/html

Create a test page for each site so you can verify which domain serves which content:

echo '<h1>Site One</h1>' | sudo tee /var/www/site-one.com/html/index.html
echo '<h1>Site Two</h1>' | sudo tee /var/www/site-two.com/html/index.html

What permissions does the web root need?

Nginx runs its worker processes as the www-data user. The web root directories need to be readable by this user.

sudo chown -R www-data:www-data /var/www/site-one.com
sudo chown -R www-data:www-data /var/www/site-two.com

Set permissions to 755 (owner can read/write/execute, group and others can read and traverse):

sudo chmod -R 755 /var/www/site-one.com
sudo chmod -R 755 /var/www/site-two.com

Verify the ownership and permissions:

ls -la /var/www/

Expected output:

drwxr-xr-x  2 www-data www-data 4096 Mar 19 10:00 site-one.com
drwxr-xr-x  2 www-data www-data 4096 Mar 19 10:00 site-two.com

Why www-data and not your user? In production, the web server user should own static files. If your application writes files (uploads, caches), the web server user needs write access. Using your personal user opens a path from a web vulnerability to your SSH account.

Create the server block config files

Nginx on Debian and Ubuntu uses a two-directory pattern: config files live in /etc/nginx/sites-available/ and are activated by symlinking them into /etc/nginx/sites-enabled/. This lets you disable a site without deleting its config. [-> nginx-config-file-structure]

Create the server block for the first domain:

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

    server_name site-one.com www.site-one.com;

    root /var/www/site-one.com/html;
    index index.html;

    access_log /var/log/nginx/site-one.com.access.log;
    error_log /var/log/nginx/site-one.com.error.log;

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

What each directive does:

  • listen 80 and listen [::]:80 bind to port 80 on IPv4 and IPv6.
  • server_name lists the domain names this block handles. Include both bare and www variants.
  • root points to the document root for this site.
  • access_log and error_log write per-site log files. Without these, all sites share /var/log/nginx/access.log, making debugging painful.
  • try_files serves the requested file, then tries it as a directory, then returns 404.

Create the second server block:

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

    server_name site-two.com www.site-two.com;

    root /var/www/site-two.com/html;
    index index.html;

    access_log /var/log/nginx/site-two.com.access.log;
    error_log /var/log/nginx/site-two.com.error.log;

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

How do you enable and disable server blocks?

Server blocks in sites-available/ are inactive until symlinked into sites-enabled/.

Enable a site

Create symlinks for both domains:

sudo ln -s /etc/nginx/sites-available/site-one.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/site-two.com /etc/nginx/sites-enabled/

Remove the default server block that ships with Nginx. It conflicts with your new blocks and serves the "Welcome to Nginx" page:

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

This only removes the symlink. The original file stays in sites-available/ in case you need it later.

Test the config before applying

Always test Nginx config before reloading. A syntax error in one file takes down all sites:

sudo nginx -t

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If you need to see the full merged config (all includes resolved into one output), use:

sudo nginx -T

This is the best tool for debugging server block issues. It shows exactly what Nginx will load, including the order of server blocks.

Reload, not restart

Apply the new config:

sudo systemctl reload nginx

Why reload instead of restart? A reload sends a signal to Nginx to re-read its config files. Existing connections finish processing normally. A restart kills the process and starts a new one, dropping all active connections. In production, always reload.

Verify the reload worked:

sudo systemctl status nginx

Sharp eyes: look for active (running) and check the timestamp on the "Main PID" line. It should not have changed (confirming a reload, not a restart).

Disable a site

To take a site offline without deleting its config:

sudo rm /etc/nginx/sites-enabled/site-two.com
sudo nginx -t && sudo systemctl reload nginx

The config file remains in sites-available/. Re-enable it anytime by creating the symlink again.

How do you verify your server blocks work?

Do not open a browser. On a headless VPS, use curl with the Host header to simulate requests from each domain:

curl -H "Host: site-one.com" http://localhost

Expected output:

<h1>Site One</h1>
curl -H "Host: site-two.com" http://localhost

Expected output:

<h1>Site Two</h1>

This works even before DNS propagates, because you are telling Nginx which domain you want directly via the Host header.

Once DNS has propagated, test from your local machine (not the server):

curl http://site-one.com
curl http://site-two.com

Each should return the correct test page.

How does Nginx decide which server block handles a request?

Nginx matches incoming requests to server blocks in a specific order. When a request arrives, Nginx first filters server blocks by the listen directive (IP and port), then matches the Host header against server_name values.

What is the server_name matching order?

The matching order is fixed and cannot be changed by config file order:

Priority Pattern Type Example When Used
1 (highest) Exact name site-one.com Always try first. Fastest (hash lookup).
2 Longest wildcard prefix *.site-one.com Matches www.site-one.com, api.site-one.com
3 Longest wildcard suffix mail.* Matches mail.site-one.com, mail.site-two.com
4 First matching regex ~^www\d+\.example\.com$ Evaluated in config file order. First match wins.
5 (lowest) default_server N/A Fallback when nothing else matches.

Key points:

  • Exact names are always checked first, regardless of where the server block appears in the config.
  • Wildcard names can only have the * at the start or end, on a dot boundary. w*.example.com is invalid.
  • Regex patterns start with ~ and are tested in the order they appear in config files. First match wins. Use sparingly because they are the slowest match type.
  • If nothing matches and no default_server is set, Nginx uses the first server block in config file order as the default. This is a common source of confusion.

What does the default_server directive do?

The default_server parameter on the listen directive tells Nginx which server block handles requests when no server_name matches. Without it, Nginx silently picks the first server block loaded for that port. This means a misconfigured domain or a bot scanning your IP address gets served one of your real sites.

Create a catch-all server block that drops unmatched requests:

sudo nano /etc/nginx/sites-available/00-catch-all
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name _;

    return 444;
}

What this does:

  • default_server marks this block as the fallback for port 80.
  • server_name _ is a convention for "no valid name." The underscore is not special to Nginx; it just never matches a real hostname.
  • return 444 is a non-standard Nginx code that closes the connection without sending any response. Bots scanning your IP get nothing. No headers, no body, no information about what software you run.

Enable and apply:

sudo ln -s /etc/nginx/sites-available/00-catch-all /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

The filename starts with 00- so it sorts first in directory listings. This has no effect on Nginx behavior (the default_server directive controls selection, not file order), but it makes the config easier to scan for humans.

Test the catch-all by requesting a domain that does not match any server block:

curl -v -H "Host: not-my-domain.com" http://localhost

You should see Empty reply from server because Nginx closed the connection with no response.

How do you set up per-site logging?

Each server block in this guide already includes per-site log directives. Per-site logs let you debug one site without sifting through traffic from all your other domains. The log paths are set in each server block:

access_log /var/log/nginx/site-one.com.access.log;
error_log /var/log/nginx/site-one.com.error.log;

Tail the logs in real time:

sudo tail -f /var/log/nginx/site-one.com.access.log

Or use journalctl for Nginx's main process logs (startup errors, reload failures):

journalctl -u nginx -f

Nginx handles log rotation automatically through /etc/logrotate.d/nginx. The default policy rotates logs daily and keeps 14 days. No extra setup needed.

What are common server block problems and fixes?

Symptom Cause Fix
Default "Welcome to Nginx" page shows for all domains default symlink still in sites-enabled/ sudo rm /etc/nginx/sites-enabled/default && sudo systemctl reload nginx
Wrong site served for a domain Duplicate server_name across blocks, or no exact match found Run `sudo nginx -T
could not build server_names_hash error Domain names too long for the default hash bucket Add server_names_hash_bucket_size 128; inside the http {} block in /etc/nginx/nginx.conf
Changes not taking effect after editing config Forgot to reload sudo nginx -t && sudo systemctl reload nginx
bind() to 0.0.0.0:80 failed Another process (Apache, old Nginx) is using port 80 `sudo ss -tlnp
Symlink created but site still not loading Symlink points to wrong path (relative instead of absolute) Delete and recreate: sudo ln -sf /etc/nginx/sites-available/site-one.com /etc/nginx/sites-enabled/

Debugging with nginx -T

When a site is not behaving as expected, dump the full merged config:

sudo nginx -T 2>&1 | grep -A 5 "server_name"

This shows every server block with its server_name value, letting you see exactly what Nginx loaded and in what order. It resolves all include directives, so you see the real config, not just the files on disk.

Next steps

Your server blocks now serve multiple domains over HTTP. The next step is to add TLS certificates so these sites are served over HTTPS.

For a broader view of managing Nginx on a VPS, see the parent guide. [-> nginx-administration-vps]


Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.

Ready to try it yourself?

Deploy your own server in seconds. Linux, Windows, or FreeBSD.

See VPS Plans