Multi-tenant nginx server

Matching and capturing multiple domains in one immutable nginx config - how to and examples!

Jannes Mingram Jannes Mingram 2 min read

Context

In a multi-teanent scenario it is desireable to have different domains for different customers.

Often, nginx is the entry to your SaaS or PaaS application. In such scneario one nginx server needs to service all customers.

It is hard to create a new config per customer, in particular from an automated fashion; not only is it potentially insecure (an automated api needs to edit nginx config files!), but also not scaleable (the nginx server needs to reload the config after each edit).

Solution

It is therefore much better to have a generic nginx config that resolves for all customers. The magic ingredient is called capture group and hgas the following syntax: (?<variable>.+). Here variable will become a variable available in the current context that holds the contents of the matched regex (regex here being .+).

In the following there are different example configuration that for exactly above scenario.

server {
   server_name  ~^(www\.)?(?<tenant>[a-zA-Z0-9]+)\.mysaas\.com$;
   
   root /var/$tenant/www;
   .....
}
Serving static content per tenant
server {
 server_name  ~^(www\.)?(?<tenant>[a-zA-Z0-9]+)\.mysaas\.com$;
 location / {
   proxy_pass      http://127.0.0.1:8080/$tenant;
   ....
 }
 ....
}
serving dynamic content with teanent as path-param
server {
 server_name  ~^(www\.)?(?<tenant>[a-zA-Z0-9]+)\.mysaas\.com$;
 location / {
   proxy_set_header mysaas-tenant $tenant;
   proxy_pass      http://127.0.0.1:8080/;
   ....
 }
 ....
}
serving dynamic content with teanent as request-header
server {
 server_name  ~^(www\.)?(?<tenant>[a-zA-Z0-9]+)\.mysaas\.com$;
 location / {
   fastcgi_pass unix:/var/run/php-fpm-www.sock;
   fastcgi_param SCRIPT_FILENAME $document_root/$tenant/mysaas.php;
   .....
 }
 ....
}
serving dynamic content with teanent as path to php file

All above scenarios match www.<anything>.mysaas.com (www is optional) where <anything> matches any number of alphanumeric characters. (If you wanted it to match any character, you could use  (?.+) but be warned about path traversal attacks, dns wildcards not match subdomains and similar!)

Notes

If you are worried about the regex capturing too much, e.g. because you only want to match domains you have not explicit specified, don't worry. In such "catch-all" scenario, nginx will smartly choose "the right one". In more formal terms, if more than one server-name matches, the following rules apply in precedence:

  1. exact name
  2. wildcard match starting with star (longest match wins)
  3. wildcard match ending with star (longest match wins)
  4. regular expression match (in order of appearance in a config)

This means, it is perfectly fine to have a config as below.

server {
   server_name  ~^(www\.)?admin\.mysaas\.com$;
   .... serve the admin app ....
}
server {
   server_name  ~^(www\.)?(?<tenant>[a-zA-Z0-9]+)\.mysaas\.com$;
   root /var/$tenant/www;
   ..... catch all .... everything that is not admin ....
}
Exact name taking precedence over a regex

Sources and more information