Skip to content
DevOps devops webservers 5 min read

Nginx Server Blocks (Virtual Hosts)

A single Ubuntu server can host many websites at the same time, each with its own domain name and its own files. In Nginx, the configuration that defines one of those sites is called a server block (often called a virtual host, the same idea Apache uses under a different name). A server block tells Nginx: “when a request comes in for this domain, serve files from this folder.” This page walks you through building a server block from scratch and hosting example.com on a fresh Ubuntu 22.04/24.04 LTS server.

What a server block actually does

When a browser asks for http://example.com, it opens a connection to your server’s IP address and sends along the host name it wants (example.com). Nginx looks at every server block you have defined and picks the one whose server_name matches. That matching block decides which directory the files come from, which port to listen on, and a hundred other things.

This matters because it lets one cheap server run example.com, blog.example.com, and shop.example.com all at once, each pointing at different files — no extra machines needed.

When to use server blocks: any time you host more than one site or domain on a server, or even when you host just one site but want clean, per-site config files. When NOT to bother: for a quick throwaway test on localhost, the default config is fine — you only need named server blocks once real domains are involved.

The sites-available and sites-enabled pattern

On Ubuntu, Nginx config is split into two directories on purpose:

DirectoryWhat it holdsWhen Nginx reads it
/etc/nginx/sites-availableEvery site config you have written, enabled or notNever directly
/etc/nginx/sites-enabledSymbolic links to the configs that are liveOn every reload

The main config file /etc/nginx/nginx.conf contains a line include /etc/nginx/sites-enabled/*;, so only files (links) inside sites-enabled are actually loaded. A symbolic link (or symlink) is just a pointer to another file. This split lets you keep a site’s config on disk but turn it off by removing only the link — you never lose the real file.

Step 1 — create the web root and a test page

The web root is the folder whose files get served to visitors. Create one for the site and drop in a simple HTML file:

sudo mkdir -p /var/www/example.com/html
sudo chown -R $USER:$USER /var/www/example.com/html
echo '<h1>Hello from example.com</h1>' | sudo tee /var/www/example.com/html/index.html

The mkdir -p makes the full path in one go, chown gives your user ownership so you can edit files without sudo, and tee writes the test page.

Step 2 — write the server block

Create a new config file in sites-available, named after the domain so it is easy to find later:

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

Paste this in:

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

    server_name example.com www.example.com;

    root /var/www/example.com/html;
    index index.html index.htm;

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

Here is what each directive means:

DirectiveWhat it does
listen 80;Accept plain HTTP on port 80 (IPv4). The [::]:80 line does the same for IPv6.
server_nameThe domain(s) this block answers to. List both the bare and www versions.
rootThe folder on disk to serve files from.
indexWhich file to serve when someone requests a directory (here, index.html).
location /Rules for matching request paths; try_files looks for the file, then the directory, then returns a 404 if neither exists.

Link the file from sites-available into sites-enabled. The -s flag makes it a symbolic link rather than a full copy:

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

Gotcha: never copy the file into sites-enabled instead of linking it. If you copy it, you end up with two separate files, and editing one won’t update the other — a classic source of “but I changed the config!” confusion. Always use ln -s.

Step 4 — test the config and reload

Always check your config for typos before reloading. The nginx -t command parses everything and reports problems without touching the running server:

sudo nginx -t

Output:

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

If the test passes, reload Nginx so it picks up the new block. A reload applies new config without dropping existing connections, unlike a full restart:

sudo systemctl reload nginx

Step 5 — open the firewall and verify

If you use ufw (Uncomplicated Firewall, Ubuntu’s simple firewall front-end), make sure HTTP traffic is allowed:

sudo ufw allow 'Nginx HTTP'
sudo ufw status

Output:

Status: active

To                         Action      From
--                         ------      ----
Nginx HTTP                 ALLOW       Anywhere
Nginx HTTP (v6)            ALLOW       Anywhere (v6)

Now request the site. If your DNS already points example.com at the server, visit it in a browser. To test before DNS is set up, force the host header locally with curl:

curl -H "Host: example.com" http://localhost

Output:

<h1>Hello from example.com</h1>

That output confirms the right server block answered for example.com.

Disabling a site

To take a site offline without deleting its config, remove only the symlink and reload:

sudo rm /etc/nginx/sites-enabled/example.com
sudo systemctl reload nginx

The real file in sites-available stays put, so you can re-enable it later by linking it again.

Best Practices

  • Name each config file after its primary domain (example.com) so the purpose is obvious at a glance.
  • Keep web roots organized under /var/www/<domain>/html — one predictable folder per site.
  • Always run sudo nginx -t before every reload; never reload a config you haven’t tested.
  • Enable sites with symlinks (ln -s), and disable them by removing the link, not by editing the real file.
  • List both example.com and www.example.com in server_name so visitors reach you either way.
  • Once a site works on HTTP, add HTTPS with a free Let’s Encrypt certificate (via certbot) before going to production.
  • Remove the bundled default symlink from sites-enabled once your own block is the intended catch-all, to avoid confusing fall-through behavior.
Last updated June 15, 2026
Was this helpful?