Back to tutorials
Tutorial

Nginx Server Blocks Tutorial (2026): Host Multiple Domains on One VPS with SSL, PHP-FPM, and Safe Reloads

Nginx server blocks tutorial for 2026: host multiple domains on one VPS with SSL, PHP-FPM, redirects, and safe reloads.

By Anurag Singh
Updated on Sep 12, 2026
Category: Tutorial
Share article
Nginx Server Blocks Tutorial (2026): Host Multiple Domains on One VPS with SSL, PHP-FPM, and Safe Reloads

Hosting several sites on one VPS is routine in 2026. The headaches usually come from the virtual hosting glue. That includes getting server_name right, keeping roots and logs consistent, and rolling out TLS without taking every site down.

This nginx server blocks tutorial shows a clean, repeatable way to host multiple domains on one VPS. You’ll use Let’s Encrypt, PHP-FPM, and a few guardrails. The goal is to avoid the classic “the default site answered my domain” mistake.

If you’re starting from a fresh VPS, HostMyCode VPS plans work well for multi-site Nginx. You can scale CPU/RAM as you add domains without rethinking the layout.

What you’ll build (and what you’ll avoid)

  • Multiple domains served from one Nginx instance using separate server blocks.
  • Per-site document roots with consistent permissions and log locations.
  • HTTP → HTTPS redirect and working Let’s Encrypt certificates.
  • PHP-FPM handling for PHP sites (WordPress, Laravel, plain PHP).
  • Safe config testing so you don’t drop all sites with one syntax error.

You’ll also avoid:

  • Serving the wrong site because a default server block matches first.
  • Breaking ACME challenges during certificate renewal.
  • Reloading Nginx with an invalid config and creating downtime.

Prerequisites and baseline assumptions

This tutorial assumes:

  • Ubuntu 24.04/26.04 LTS or Debian 12/13 (commands are identical for most steps).
  • Nginx installed from distro packages.
  • You can SSH as a sudo user.
  • Domains already registered and you can edit DNS (A/AAAA records).

If your DNS cutover is still in progress, keep this handy: DNS cutover checklist tutorial. It helps you separate real misconfigurations from caching and propagation delays.

Step 1: Install Nginx, Certbot, and PHP-FPM

On Ubuntu/Debian:

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
sudo apt install -y php8.3-fpm

Verify Nginx is running:

systemctl status nginx --no-pager

Open firewall ports if you use UFW:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status

Tip: if you’re on a cPanel VPS, you typically won’t manage Nginx server blocks directly. For Nginx + cPanel performance tuning, use this guide instead: PHP-FPM setup guide on a cPanel VPS.

Step 2: Create a consistent directory layout per domain

Pick a convention and stick to it. Make it obvious at a glance, even six months from now.

  • Web root: /var/www/example.com/public
  • Writable app data (if needed): /var/www/example.com/storage
  • Nginx logs: /var/log/nginx/example.com.access.log and ...error.log

Create folders for two sample domains:

sudo mkdir -p /var/www/example.com/public
sudo mkdir -p /var/www/example.net/public

Set ownership. A common baseline is www-data for ownership and reads.

It’s not the only model, but it’s predictable. It also works well for a basic setup:

sudo chown -R www-data:www-data /var/www/example.com /var/www/example.net
sudo find /var/www -type d -exec chmod 755 {} \;
sudo find /var/www -type f -exec chmod 644 {} \;

Create a quick test page so you can confirm routing immediately:

echo "<h1>example.com on Nginx</h1>" | sudo tee /var/www/example.com/public/index.html
echo "<h1>example.net on Nginx</h1>" | sudo tee /var/www/example.net/public/index.html

Step 3: Understand Nginx server block priority (so the wrong site doesn’t win)

Nginx chooses a server block using listen and server_name matching rules. Most “wrong site” incidents start with an enabled default site. That default block catches requests you did not mean to catch.

See what’s enabled:

ls -la /etc/nginx/sites-enabled/

On many installs you’ll see default. If you don’t want the distro placeholder site responding, disable it:

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

Don’t reload yet. Build your real server blocks first, then validate the full config.

Step 4: Create your first server block (HTTP only)

Create /etc/nginx/sites-available/example.com:

sudo nano /etc/nginx/sites-available/example.com

Paste:

server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

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

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

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

    # PHP (optional for now)
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~ /\.ht {
        deny all;
    }
}

Enable it:

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

Repeat for example.net. Update server_name, root, and the log file names:

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

Test config syntax before touching a running process:

sudo nginx -t

If you see syntax is ok and test is successful, reload:

sudo systemctl reload nginx

Step 5: Point DNS to your VPS and verify the right site answers

Create A records for both domains:

  • example.com → your VPS IPv4
  • www.example.com → same IP (A record) or CNAME to apex
  • Repeat for example.net

From your workstation, confirm the records resolve:

dig +short example.com A
dig +short www.example.com A

Then verify Nginx selects the correct server block. Force the Host header:

curl -I http://YOUR_SERVER_IP -H 'Host: example.com'
curl -I http://YOUR_SERVER_IP -H 'Host: example.net'

If both return the same content, you’re usually looking at one of these:

  • A default server block is still enabled.
  • Both configs share the same server_name, or one uses a wildcard that matches everything.
  • You reloaded Nginx without enabling the new sites.

If DNS still acts weird after a move, this guide is focused and practical: DNS propagation troubleshooting tutorial.

Step 6: Add HTTPS with Certbot (and keep renewals working)

Wait until both domains resolve to the VPS and HTTP loads correctly. Then request certificates.

For example.com:

sudo certbot --nginx -d example.com -d www.example.com

For example.net:

sudo certbot --nginx -d example.net -d www.example.net

Certbot will usually:

  • Add a listen 443 ssl; server block
  • Install certificate paths
  • Add an HTTP → HTTPS redirect

Test renewal right away. If challenge routing is wrong, you’ll see it here:

sudo certbot renew --dry-run

If you want a predictable redirect instead of relying on Certbot’s edits, enforce it yourself. Keep port 80 for ACME, and redirect everything else:

server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com/public;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Need a deeper pass on HTTPS policies, ciphers, and HSTS? Use this: TLS hardening tutorial.

Step 7: Wire PHP-FPM correctly (and avoid the “downloaded PHP file” mistake)

If PHP files download instead of executing, Nginx is not handing the request to PHP-FPM. Start by checking the service:

systemctl status php8.3-fpm --no-pager

Then confirm the socket exists:

ls -la /run/php/php8.3-fpm.sock

A solid, general-purpose PHP section looks like this:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

location ~* \.(?:css|js|jpg|jpeg|gif|png|webp|avif|ico|svg|woff2?)$ {
    expires 7d;
    add_header Cache-Control "public";
    try_files $uri =404;
}

For WordPress, you’ll usually want pretty permalinks. You also want a front-controller fallback:

location / {
    try_files $uri $uri/ /index.php?$args;
}

Create a PHP info file for a quick sanity check. Remove it after testing:

echo "<?php phpinfo();" | sudo tee /var/www/example.com/public/info.php

Visit https://example.com/info.php. Confirm PHP 8.3 loads, then delete the file:

sudo rm -f /var/www/example.com/public/info.php

Step 8: Add a “catch-all” default that fails closed

Once you host multiple domains, random traffic will hit your IP. Don’t let it fall through to a real site.

A catch-all default that returns 444 (Nginx “no response”) helps. It keeps accidental domains and noisy scans away from your apps.

Create /etc/nginx/sites-available/00-catchall:

sudo nano /etc/nginx/sites-available/00-catchall

Use this:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

Enable and reload safely:

sudo ln -s /etc/nginx/sites-available/00-catchall /etc/nginx/sites-enabled/00-catchall
sudo nginx -t
sudo systemctl reload nginx

Why it matters: if someone points a domain at your IP (by mistake or on purpose), they shouldn’t inherit your “default” content. They also shouldn’t be able to trigger application code.

Step 9: Make logs manageable (per-site) and prevent disk surprises

Per-site logs speed up debugging. They also increase log volume. This adds up fast on a VPS with many small sites.

Make sure log rotation is enabled. Then confirm it is doing what you think it is doing.

Check logrotate status and config:

ls -la /etc/logrotate.d/
sudo logrotate -d /etc/logrotate.conf | head

If you want a hosting-friendly rotation setup (including how to avoid “disk spikes” during rotation), use this guide: logrotate tutorial for hosting VPS.

Step 10: Quick troubleshooting checklist (the stuff you’ll actually hit)

  • 502 Bad Gateway: PHP-FPM stopped, wrong fastcgi_pass socket, or permission issues on the socket.
  • 403 Forbidden: bad directory permissions, wrong root, or missing index file.
  • Wrong site loads: default server still active, wildcard server_name, or DNS still pointing elsewhere.
  • Certbot fails validation: domain doesn’t resolve to this VPS, port 80 blocked, or ACME path not reachable.

Useful commands:

# See all included config files
sudo nginx -T | less

# Tail per-site logs
sudo tail -f /var/log/nginx/example.com.error.log

# Verify which server block handled a request (add to log_format if needed)
sudo tail -n 50 /var/log/nginx/example.com.access.log

Step 11: A clean “template” you can copy for every new domain

Below is a compact pattern that fits many PHP sites, including WordPress. Treat it as your house template.

Copy it, then update the domain, root, and log names.

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com/public;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

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

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

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

    # WordPress-friendly routing
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }

    location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|avif|ico|svg|woff2?)$ {
        expires 7d;
        add_header Cache-Control "public";
        try_files $uri =404;
    }

    location ~ /\. {
        deny all;
    }
}

After adding a new domain config:

sudo ln -s /etc/nginx/sites-available/newdomain.tld /etc/nginx/sites-enabled/newdomain.tld
sudo nginx -t
sudo systemctl reload nginx

Step 12: Production habits that keep multi-site VPS hosting stable

  • Test before reload: treat nginx -t as required, not optional.
  • One change per deploy: edit one site, validate, reload, then move to the next.
  • Separate logs: per-site logs make incidents faster to triage.
  • Backup site configs: include /etc/nginx/ and Let’s Encrypt folders in your backups.
  • Document DNS + SSL: track A/AAAA, CNAMEs, and certificate names per domain.

If you also run mail from the same VPS, plan DNS and IP reputation early (SPF/DKIM/DMARC/rDNS). This guide walks you through a clean setup: VPS email setup tutorial.

Summary: multi-domain Nginx hosting that stays predictable

You now have separate server blocks, per-domain roots and logs, working TLS, and a safe reload routine. That’s the day-to-day foundation for multi-site hosting on one VPS.

From here, focus on app-specific caching headers, tighter TLS policies, and backups that include configs and certificates.

If you want this to stay consistent under load, start with a properly sized VPS. Scale as you add sites.

HostMyCode VPS and managed VPS hosting both fit multi-domain Nginx deployments; the difference is how much you want to administer yourself.

Multi-domain hosting gets simpler when the VPS stays stable and you can get help when you need it. Pick a HostMyCode VPS if you want full control, or go with managed VPS hosting if you’d rather hand off routine maintenance and keep your time on the sites.

FAQ

Do I need one Nginx server block per domain?

Yes. Use one server block per domain (or per site) so each site has its own root, logs, and TLS cert paths. That separation helps prevent cross-site mistakes.

Why does the wrong website show up on a new domain?

Most of the time, DNS still points to the old host. Or Nginx is falling back to a default server block. Remove the distro default and add a catch-all that returns 444.

Can multiple domains share one Let’s Encrypt certificate?

They can, but it’s usually cleaner to issue one certificate per site (including the www alias). It keeps troubleshooting straightforward and renewals independent.

Should I use HTTP/2 and HTTP/3 for these sites?

Enable HTTP/2 on TLS listeners by default. Only add HTTP/3 if you can test end-to-end. For QUIC on Nginx, follow a version-appropriate guide and validate firewall/UDP behavior.

What’s the safest way to apply config changes without downtime?

Edit one file, run nginx -t, then systemctl reload nginx. Reload is graceful. Restart is not, and it will drop connections.