Multi-Tenant Local Dev
Your app may need to allow users/teams/tenants/whatever the ability to have their own subdomain. If your domain is myapp.com
, this means letting tenants use foo.myapp.com
to access their account.
This affects your code base quite a but, but what I want to show you is a server setup for this. I'll be doing videos on this for development and production, so keep an eye on the Youtube channel for more.
In this article (and video!) we're concentrating on local development. We'll setup dnsmasq and Nginx so any subdomain used for a local test domain (e.g. myapp.test
) will point to your one codebase.
Here's a quick run down of what's covered in the video - there's 3 steps to this:
- Installing and configuring
dnsmasq
- Making your computer use
dnsmasq
for DNS resolution - Intalling and configuring
nginx
dnsmasq
The tl;dr on installing and configuring dnsmasq
with homebrew is this:
brew install dnsmasq
echo "address=/test/127.0.0.1" \
| sudo tee /opt/homebrew/etc/dnsmasq.d/test.conf
# use sudo here
sudo brew services start dnsmasq
We install dnsmasq
, and then configure it so that domains ending in .test
resolve to 127.0.0.1
. Then we use brew services
to start dnsmasq
. If you already have dnsmasq, you want to run restart
instead of just start
.
Make sure dnsmasq
is started with sudo
, as it needs elevated permissions to do what it's doing.
To test this, you can run:
dig foo.test @127.0.0.1
The ANSWER
section should tell you that foo.test
resolves to 127.0.0.1
. Using @127.0.0.1
tells the dig
command to use the domain name server (dnsmasq
!) running at 127.0.0.1
.
Using dnsmasq
We need to make our computer (MacOS in this case) use dnsmasq for its DN server (in addition to the regular DN servers it uses). To do that, we'll create a file /etc/resolver/test
.
Warning: This change may not work instantly. You may need to wait a bit or even restart your computer.
Here are the commands to run:
sudo mkdir -p /etc/resolver
echo "nameserver 127.0.0.1" \
| sudo tee /etc/resolver/test
That file just contains nameserver 127.0.0.1
, which tells the operating system to find a nameserver running locally at 127.0.0.1
.
To test this, run the same dig
command as above, but without the @127.0.0.1
part. The dig
command shouldn't need that anymore, since the OS now knows to check localhost for a domain name server first.
dig foo.test
Nginx
Lastly, we can install and configure Nginx
.
brew install nginx
# no sudo here
brew services start nginx
# <Add configuration here, see below>
nginx -t
nginx -s reload
In this case, we install nginx
and then start it without sudo. This make it run as our current user, allowing Nginx to read (and write to, if needed) files owned by our current user. This is good as our code bases are likely owned by our current user.
The video explains these in depth. For now, I'll just write 2 Nginx config files for you to use. One lets you use *.myapp.test
for your myapp
code base. The second configuration sets up a generic thing. Any *.test
domain you use will map to a directory on your filesystem, allowing you to use any domain you want, knowing it will reach an app that is in a folder of the same name.
The multi-tenant setup in file /opt/homebrew/etc/nginx/servers/a-srv.conf
:
server {
location 80;
server_name ~^(?<tenant>.+)\.myapp\.test$;
root /Users/<you>/srv/myapp/public;
index index.html index.htm index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param TENANT $tenant;
include fastcgi_params;
}
}
The multi-tenant setup in file /opt/homebrew/etc/nginx/servers/x-srv.conf
:
server {
location 80;
server_name ~^(?<app>.+)\.test$;
root /Users/<you>/srv/$app/public;
index index.html index.htm index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Notes:
- The filenames are alphabetically ordered so the multi-tenant setup is loaded first
- The directory mapping of
domain -> code base directory
for*.test
domains assumes a document root ofpublic
within the code base, which may be Laravel specific
I'm not a Laravel dev
Here are Nginx configurations to use if you're not a Laravel developer, and your app listens for HTTP requests (e.g. Node, Go).
# /opt/homebrew/etc/nginx/servers/a-srv.conf
server {
location 80;
server_name ~^(?<tenant>.+)\.myapp\.test$;
root /Users/<you>/srv/myapp/public;
index index.html index.htm;
location / {
try_files $uri $uri/ @app;
}
location @app {
proxy_set_header Host $http_host;
proxy_set_header X-Tenant $tenant;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000;
}
}
# /opt/homebrew/etc/nginx/servers/x-srv.conf
server {
location 80;
server_name ~^ (?<app>.+)\.test$;
root /Users/<you>/srv/$app/public;
index index.html index.htm;
location / {
try_files $uri $uri/ @app;
}
location @app {
proxy_set_header Host $http_host;
proxy_set_header X-App $app;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000;
}
}
These proxy over HTTP instead of FastCGI. They use a "named" location block @app
but the principles are otherwise the same.
More Details
There's more details and description in the Youtube video, definitely check it out!
Use Laravel Herd!?
You get this setup (but even better!) using Laravel Herd. If you're a Laravel developer, considering just using that.