Name-based virtual hosting with Nginx

posted on 2013-09-14

It's sometimes called mass virtual hosting or simply name-based virtual hosting: using the requested server domain name to pick the right site to host. The mass part is where you do not simply configure each name but use a dynamic virtual host form the request. Each request is then mapped to a root containing the right content.

Having used this functionality in Apache I decided to give it a try in Nginx.

In Nginx you solve this with a regular expression (and then you have two problems ;-)). Using named match groups to generate the parts of the path on disk. The regular expression I use is:

~^((?<subdomain>.*)\.)?(?<domain>[^.]+)\.(?<tld>[^.]+)$

which works s follows: match the complete string only, where the subdomain is possibly there (bneijt.nl) but if it's there, it's everything but the last dot (((?<subdomain>.*)\.)?). Both the domain and the top level domain are required and they need to have a . in between them. I found debuggex.com a handy tool in constructing this regular expression.

When a request comes in, we simply map this to a root based on the variables created by the regular expression:

server_name ~^((?<subdomain>.*)\.)?(?<domain>[^.]+)\.(?<tld>[^.]+)$;
location / {
    root /srv/http/vhost/${domain}.${tld}/${subdomain};
}

One problem we are left with is what will happen when the subdomain is empty. The root would become /src/http/vhost/example.com/ which would host the subdomain www in a directory below the domain instead. Not what we want. To solve this, we add an if statement to the configuration. This results in the final configuration reading:

server {
    server_name ~^((?<subdomain>.*)\.)?(?<domain>[^.]+)\.(?<tld>[^.]+)$;
    if ($subdomain = "") {
        set $subdomain "_";
    }
    location / {
        index index.html;
        root /srv/http/vhost/${domain}.${tld}/${subdomain};
    }
}

Testing the configuration

During development, I had error_log /tmp/error.log; to debug the configuration and regex in Nginx. You will see error records when no file is found and you can just place the error_log statement inside the server context.

To test various subdomains, add a random domain to your /etc/hosts file, for example:

149.210.136.127 how.www.hello.com www.hello.com hello.com

After adding this rule, you can open a browser and just visit hello.com, www.hello.com and how.www.hello.com. Of course you need the IP address to point to your own server (not mine). If everything works well

  • hello.com should serve /srv/http/vhost/hello.com/_/index.html
  • www.hello.com should serve '/srv/http/vhost/hello.com/www/index.html`
  • how.www.hello.com should serve /srv/http/vhost/hello.com/how.www/index.html

Loose ends

One of the thing I have not figured out is how to get rid of the if statement. For something as common as a default value, I would expect there to be a better solution. The Nginx if documentation also states that in most cases if is evil.

Another thing is you probably want a redirect to, or from, the www subdomain to the plain domain. To solve this you will need per site configuration in the _ or www folder (in Apache I used an .htaccess file). If you can determine one rule for all the domains you vhost, then there should be no problem. But if you can't, I don't see how you can easily add the same per-domain control yet. I guess the best solution would be to add a server section per specific domain with a rewrite rule.

If you have a solution to any of the above problems, please consider adding a comment.