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:

  1. Installing and configuring dnsmasq
  2. Making your computer use dnsmasq for DNS resolution
  3. 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:

  1. The filenames are alphabetically ordered so the multi-tenant setup is loaded first
  2. The directory mapping of domain -> code base directory for *.test domains assumes a document root of public 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.