Your First Virtual Host

Introduction

A virtual host is one server { } block: a hostname, a document root, and rules for URL paths. This chapter walks through creating a site from scratch—HTML on disk, config in sites-available, symlink in sites-enabled, and verification with curl. By the end, you will have a repeatable pattern for every new project.

Prerequisites

What You Will Build

ItemValue
Hostname (local test)demo.local
Web root/var/www/demo
Config file/etc/nginx/sites-available/demo
ContentSingle index.html page

Step 1: Create the Web Root and Page

bash
sudo mkdir -p /var/www/demo

Create /var/www/demo/index.html:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo Virtual Host</title>
  </head>
  <body>
    <h1>Hello from demo.local</h1>
    <p>This page is served by your first Nginx virtual host.</p>
  </body>
</html>

Write with a text editor or:

bash
sudo tee /var/www/demo/index.html > /dev/null <<'EOF'
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Demo Virtual Host</title>
  </head>
  <body>
    <h1>Hello from demo.local</h1>
  </body>
</html>
EOF

Set permissions so Nginx can read files:

bash
sudo chown -R www-data:www-data /var/www/demo
sudo find /var/www/demo -type d -exec chmod 755 {} \;
sudo find /var/www/demo -type f -exec chmod 644 {} \;

Code explanation:

  • Nginx workers run as www-data on Debian/Ubuntu
  • Directories need execute bit (755) for traversal; files need read (644)

Step 2: Write the server Block

Create /etc/nginx/sites-available/demo:

Code explanation:

  • server_name demo.local — matches when the Host header is demo.local
  • try_files — serve file, then directory index, else 404
  • Separate logs make debugging this site easier on a shared server

Save with sudo nano /etc/nginx/sites-available/demo or your preferred editor.

Step 3: Enable the Site

bash
sudo ln -sf /etc/nginx/sites-available/demo /etc/nginx/sites-enabled/demo
sudo nginx -t
sudo systemctl reload nginx

Code explanation:

  • Symlink activates the config via include /etc/nginx/sites-enabled/*
  • Never skip nginx -t before reload

Tip

Disable the Default Site Optional

Ubuntu’s default site may still answer for localhost. For a clean test of demo.local, disable it:

bash
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Step 4: Point demo.local to Your Machine

On Linux / WSL

bash
grep demo.local /etc/hosts || echo '127.0.0.1 demo.local' | sudo tee -a /etc/hosts

On macOS

Edit /etc/hosts as root and add:

text
127.0.0.1 demo.local

Code explanation:

  • /etc/hosts maps a name to an IP before public DNS
  • Production uses real DNS A/AAAA records instead

Step 5: Verify

bash
curl -I -H "Host: demo.local" http://127.0.0.1/
curl http://demo.local/

Expected:

text
HTTP/1.1 200 OK
...

Browser: open http://demo.local/ and see your heading.

Check logs if something fails:

bash
sudo tail -n 20 /var/log/nginx/demo.error.log
sudo tail -n 20 /var/log/nginx/demo.access.log

Alternative: conf.d Style

Same server block in /etc/nginx/conf.d/demo.conf works on many distros—no symlink step:

bash
sudo cp /etc/nginx/sites-available/demo /etc/nginx/conf.d/demo.conf
sudo nginx -t && sudo systemctl reload nginx

Pick one layout (sites-available or conf.d) per server to avoid loading the same server twice.

Troubleshooting

SymptomCheck
Default Nginx page instead of yoursWrong server_name; request Host header; disable default site
403 ForbiddenPermissions on /var/www/demo; SELinux on RHEL
404 Not Foundroot path wrong; index.html missing
nginx -t failsTypo in config; duplicate listen conflicts
Browser cannot resolve demo.local/etc/hosts entry missing

Test which server block Nginx selects:

bash
curl -v -H "Host: demo.local" http://127.0.0.1/ 2>&1 | grep -E '< HTTP|< Server'

Project Layout Reference

text
/var/www/demo/
└── index.html
 
/etc/nginx/sites-available/demo
/etc/nginx/sites-enabled/demo  →  symlink
 
/var/log/nginx/demo.access.log
/var/log/nginx/demo.error.log

What Comes Next in This Track

Later chapters add static SPA routing, gzip, reverse proxy to Java/Node, and HTTPS—each extends this same server block pattern.

FAQ

Can I use an IP address in server_name?

You can use the IP as server_name, but hostname-based virtual hosts are standard. For IP-only access, default_server on listen is common.

How many sites on one server?

Many—each gets its own server { } with distinct server_name (or listen options).

Do I delete sites-available when disabling?

Remove the sites-enabled symlink only; keep the file in sites-available for easy re-enable.

Why try_files instead of only index?

try_files also serves other static files (about.html, assets) and returns 404 when missing.

Can I test without editing /etc/hosts?

Yes: curl -H "Host: demo.local" http://127.0.0.1/ sends the correct Host header.

Same config for production domain?

Replace demo.local with example.com, point DNS to your VPS IP, and add HTTPS in a later chapter.