DB-유

nginx Server Blocks (Virtual Hosts) on Ubuntu

· linux, nginx, ubuntu

Apache people call them virtual hosts; nginx calls them server blocks. Either way the idea is the same: one nginx instance on one IP address serves several sites, and picks the right document root based on the Host header the browser sends. On Ubuntu 16.04 the package layout makes this tidy, with one file per site under /etc/nginx/sites-available/ and a symlink into sites-enabled/ to turn it on.

I use this constantly for internal tools: phpMyAdmin on one hostname, a status page on another, all on the same small VM. Below is two sites plus a default catch-all, so random hostnames don't land on the wrong site.

You need nginx installed (sudo apt install nginx, which is 1.10.x on xenial), DNS A records or hosts-file entries for your names, and port 80 open in ufw (sudo ufw allow 'Nginx HTTP').

Document roots

I keep sites under /var/www/<hostname>/html so the path tells me what it is:

sudo mkdir -p /var/www/example.com/html
sudo mkdir -p /var/www/tools.example.com/html
sudo chown -R $USER:$USER /var/www/example.com/html /var/www/tools.example.com/html
sudo chmod -R 755 /var/www

Drop a trivial page in each so you can tell them apart later:

echo '<h1>example.com</h1>' > /var/www/example.com/html/index.html
echo '<h1>tools.example.com</h1>' > /var/www/tools.example.com/html/index.html

The first server block

I don't edit the shipped default file for real sites. Copy it and strip it down:

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

Replace the contents with:

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

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

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

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

The key line is server_name. nginx compares it to the Host header, exact names first, then wildcard names like *.example.com, then regular expressions. Per-site log files are optional but make debugging much easier than grepping one shared log.

Second site

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

    server_name tools.example.com;
    root /var/www/tools.example.com/html;
    index index.html;

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

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

The default catch-all

If a request arrives with a Host that matches no block, nginx serves the first block it finds for that port, which is whatever sorts first alphabetically in sites-enabled/. I would rather it return nothing. Edit /etc/nginx/sites-available/default to be a bare block with default_server and the special return 444, which closes the connection without a response:

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

Only one block per listen address/port can carry default_server, otherwise nginx refuses to start.

Enable and reload

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/tools.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t must print syntax is ok and test is successful before you reload. A reload with a broken config leaves the old worker processes running, but it is still better to catch it first.

Testing it

From your workstation, force the Host header with curl so DNS does not have to be in place yet:

curl -H 'Host: example.com' http://203.0.113.10/
curl -H 'Host: tools.example.com' http://203.0.113.10/
curl -H 'Host: nothing.example.net' http://203.0.113.10/

The first two should return the matching <h1>; the third should fail with curl: (52) Empty reply from server, which is the 444 doing its job. On the server, tail -f /var/log/nginx/example.com.access.log while you make requests confirms the right log is being written.

When it goes wrong

  • nginx: [emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 32: a long hostname exceeded the default bucket. Add server_names_hash_bucket_size 64; inside the http {} block of /etc/nginx/nginx.conf.
  • nginx: [emerg] a duplicate default server for 0.0.0.0:80: two blocks claim default_server. Remove it from one of them.
  • The wrong site loads for a hostname: usually a typo in server_name, or the symlink points at an old file. nginx -T dumps the whole effective configuration so you can search it.
  • 403 Forbidden with directory index of "/var/www/..." is forbidden in the error log: there is no index.html and autoindex is off, or the www-data user cannot traverse the directory. Check ls -ld on every path component.

Adding a site later is one file, one symlink, one reload. Good enough for me.