Multi-Tenant Laravel on Ubuntu 18.04 with NGINX, MariaDB, and PHP 7.3

Configuring Laravel as a multi-tenant application

In today’s world, it is very common for online businesses to present themselves as software as a service, or SaaS. SaaS provides the opportunity to use a single codebase for multiple companies, or tenants. This type of architecture provides multiple benefits for developers. Having a single codebase means that code isn’t replicated, documentation can have a single source of truth, and, ideally, adding a new tenant will require little to no work.

As a result of this guide, you will have a Laravel instance with multi-tenancy, one system database (read: “master”, for anything that is for the parent site, or not tenant-specific), one database per tenant, and any number of domains per tenant.

What is multi-tenancy?

For a moment, consider a standard Laravel instance as a single-family home. When building this home, construction workers will put their efforts into one frame, however small or large it may be. When the next home needs built, the construction workers will start over and build a new one from the ground up. Contrasting to this, a multi-tenant Laravel instance can be visualized as an apartment building. An apartment building may have much greater capacity than a single-family home, but it ideally only needs to be built once. Once the structure is built, you can populate its many empty suites with new families. Also, it is possible that each family will want to make changes to their suites. Generally, this is allowed in small amounts. Too much customization leads to much more difficult maintenance of the apartment buildings, as essentially one suite may become wholly different from the next.

These points hold true with single-tenant vs multi-tenancy in Laravel. If the application you are building does not call for the need of hosting multiple tenants, then a standard Laravel instance will provide you with everything you need. If your requirements dictate that you should be able to support the data of multiple companies with a single codebase, though, a multi-tenant instance makes sense. There are additional architectural considerations to be made for multi-tenancy. At this point, those considerations are left to the reader.

Before we get started

Pre-requisites

We are starting from a Known Good Configuration: Ubuntu 18.04 up-to-date with NGINX, MariaDB, and PHP 7.3 (with PHP 7.3 FPM) installed. Additionally, Composer should already be installed. Keep in mind that any customizations to the configurations of this required software prior to this guide will result in this guide not leading us to a predictable result.

Ensure that your system packages are up-to-date:

sudo apt update && sudo apt upgrade

Other knowledge

Through this process, you might see permissions issues with editing files. Make your changes, then run the following commands to fix permissions:

sudo chown -R www-data:www-data /var/www
sudo find /var/www -type d -exec chmod 2750 {} \+
sudo find /var/www -type f -exec chmod 640 {} \+

The most popular package for multi-tenancy support for Laravel is available at https://github.com/tenancy/multi-tenant (formerly hyn/multi-tenant). This package provides a lot of great things out of the box: per-tenant configs, views, code and routes; multiple database separation methods; and event-driven architecture. What this package seems to lack, though, is a guide covering implementation from start to finish. Through my attempts at configuring multi-tenancy with Laravel, I had to patch the writings of multiple “be all and end all” guides to come to a fully working installation.

Getting Started

Creating an NGINX site config

The default NGINX site config won’t work for us, so let’s create a new none!

Run nano /etc/nginx/sites-available/laravel to create our new configuration. We should populate this file with what I have listed below. Be sure to replace xyz.com with your domain!

server {
listen 80 default_server;
server_name xyz.com, *.xyz.com;
  root /var/www/html/app/public;
index index.php index.html index.htm index.nginx-debian.html;
  location / {
try_files $uri $uri/ /index.php?$query_string;
}
  location ~* \.php$ {
fastcgi_pass unix:/run/php/php7.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}

After saving this file, create a symlink from sites-available to sites-enabled:

sudo ln -s /etc/nginx/sites-available/laravel /etc/nginx/sites-enabled/

Run NGINX’s configuration test to confirm that we didn’t break anything, and then restart NGINX:

sudo nginx -t
sudo service nginx restart

A couple of important things to call out:

  • Ensure that you replaced xyz.com with your domain!
  • Check out the following line:
  • server_name xyz.com, *.xyz.com;
  • This line is what will eventually allow us to catch traffic to xyz.com, as well as any subdomain of xyz.com like tenant1.xyz.com, apples.xyz.com, and so forth.

Creating our system database and user

As I mentioned in the opening of this guide, the system database is the master database. If you have experience creating Laravel websites already, think of this as the default database that you create. Anything that isn’t tenant-specific will go here. The only difference here is that we are calling it “system”!

During the installation of MariaDB, you were asked to set the root user’s password (note: this is the root MariaDB password, not the system’s root user’s /etc/passwd password). You will need that, so have it handy!

mysql -u root -p

The shell will ask for the root MariaDB password we mentioned, so enter it now.

CREATE DATABASE laravel;
CREATE USER laravel IDENTIFIED BY 'PasswordHere';
GRANT ALL PRIVILEGES ON *.* TO laravel@localhost IDENTIFIED BY 'PasswordHere' WITH GRANT OPTION;

At this point, we now have NGINX configured and our MariaDB system database and user created. Take note of the username and password that you used for the database user! Anywhere you see PasswordHere in this guide, replace it with the password you specified.

Add our user to the www-data group to allow for easier modification of files

By default, your user account will not have permission to modify files in the web root. This is the most frustrating part of this whole process! I like to add my user to the www-data group to make it a bit easier:

sudo chown -R www-data:www-data /var/www
sudo find /var/www -type d -exec chmod 2750 {} \+
sudo find /var/www -type f -exec chmod 640 {} \+

Creating our Laravel application

Traverse to the grandparent directory of the server root we specified in our laravel NGINX configuration file and create a new Laravel project:

cd /var/www/html/
composer create-project --prefer-dist laravel/laravel app

Great! Now we have a (sort of?) functional Laravel installation! If you navigate to your domain, you should see the Laravel welcome page.

Configuring Laravel

Now that we have a working Laravel instance, navigate to the root directory of the app and edit the file named .env:

cd /var/www/html/app/
nano .env

We will need to change a few values here. Listed below are changes:

...
APP_URL=http://xyz.com
...
DB_CONNECTION=system
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=PasswordHere
...

Below are additions:

LIMIT_UUID_LENGTH_32=true
TENANT_URL_BASE=xyz.com
TENANCY_DATABASE_AUTO_DELETE=true
TENANCY_DATABASE_AUTO_DELETE_USER=true

Installing the multi-tenant package

After saving the .env file, run the following composer command to install the multi-tenant package:

composer require "hyn/multi-tenant" 5.4.*

That last line, composer require "hyn/multi-tenant" 5.4.*, tells composer that we need the multi-tenant package, but we only want versions greater than or equal to 5.4, and versions less than 5.5. Generally, when the developers of packages only change that last number in a release, nothing breaks, so we want to restrict to that range for safety.

Run the following commands to setup Laravel’s built-in auth routes / templates / migrations, as well as to publish the multi-tenancy configuration:

php artisan make:auth
php artisan vendor:publish --tag=tenancy

Modify the database configuration file at config/database.php by changing the key of the 'mysql' connection to 'system' (on around line 46, inside the array keyed 'connections').

Setting Up Multi-Tenancy

Configuration changes

Modify the tenancy configuration file at config/tenancy.php by setting 'update-app-url' to true.

EnforceTenancy Middleware

Run the following command to create a new middleware called EnforceTenancy:

php artisan make:middleware EnforceTenancy

This will create a new file in app/Http/Middleware called EnforceTenancy.php. Modify it, adding the following to a new line right before return $next($request); in the handle function:

Config::set('database.default', 'tenant');

Additionally, add the following on a new line directly below use Closure;:

use Illuminate\Support\Facades\Config;

HTTP Kernel Modifications

Add the following line in app/Http/Kernel.php to the $routeMiddleware array:

'tenancy.enforce' => \App\Http\Middleware\EnforceTenancy::class,

Tenant Migrations

Create a new directory in the database/migrations folder called tenant and move all migrations that are tenant-specific to this folder. For our purposes, this includes all files not starting with tenancy_ (so the two auth migrations):

mkdir /var/www/html/app/database/migrations/tenant
mv /var/www/html/app/database/migrations/2014_10_12* /var/www/html/app/database/migrations/tenant/

You can now run the system migrations:

php artisan migrate

UsesTenantConnection Trait

For any model that should uses the tenant-specific database, ensure that the model uses the UsesTenantConnection trait.

Modify the User model at app/User.php by adding the following line directly below use Illuminate\Foundation\Auth\User as Authenticatable;:

use Hyn\Tenancy\Traits\UsesTenantConnection;

and replace the line use Notifiable; with the following:

use Notifiable, UsesTenantConnection;

UsesSystemConnection Trait

Any model that should instead use the system database (not tenant-specific), ensure that the model uses the UsesSystemConnection trait.

Making Changes

Creating a Tenant helper class

Create a new file at app/Tenant.php. The contents should be as follows:

<?php
namespace App;
use Illuminate\Support\Facades\Artisan;
use Hyn\Tenancy\Environment;
use Hyn\Tenancy\Models\Hostname;
use Hyn\Tenancy\Models\Website;
use Hyn\Tenancy\Contracts\Repositories\HostnameRepository;
use Hyn\Tenancy\Contracts\Repositories\WebsiteRepository;
use Illuminate\Support\Facades\Log;
/**
* @property Website website
* @property Hostname hostname
*/
class Tenant
{
public function __construct(Website $website = null, Hostname $hostname = null)
{
        $this->website = $website ?? $sub->website;
$this->hostname = $hostname ?? $sub->websites->hostnames->first();
}
    public function delete()
{
app(HostnameRepository::class)->delete($this->hostname, true);
app(WebsiteRepository::class)->delete($this->website, true);
}
    public static function create($fqdn): Tenant
{
if (static::tenantExists($fqdn)) {
$hostname = app(HostnameRepository::class)->findByHostname($fqdn);
$website = $hostname->website;
Log::info('Was asked to create tenant for FQDN ' . $fqdn . ' but a tenant with this FQDN already exists; returned existing tenant');
return new Tenant($website, $hostname);
}
// Create New Website
$website = new Website;
app(WebsiteRepository::class)->create($website);
        // associate the website with a hostname
$hostname = new Hostname;
$hostname->fqdn = $fqdn;
// $hostname->force_https = true;
app(HostnameRepository::class)->attach($hostname, $website);
        // make hostname current
app(Environment::class)->tenant($website);
        return new Tenant($website, $hostname);
}
    public static function tenantExists($name)
{
return Hostname::where('fqdn', $name)->exists();
}
}

This will allow for an easy interface to create new tenants.

Creating a MakeTenant command

Most guides for multi-tenancy that I have read integrate the creation of tenants into the registration of specific users. Thus, the creation of any one user would result in a new tenant being created. It seems more intuitive to first create a tenant, and then allow users to register for that specific tenant by using a registration form at said tenant’s URL.

To enable this, let’s create an artisan command that will allow us to make tenants!

php artisan make:command CreateTenant

Running this command will create a file at app/Console/Commands/CreateTenant.php. Let's modify that file.

Add the following line right below use Illuminate\Console\Command;:

use App\Tenant;

Change the signature of the command to:

protected $signature = 'tenancy:make-tenant {fqdn}';

and the description to:

protected $description = 'Create a new tenant';

Add the following to the handle() method:

$fqdn = $this->argument('fqdn') . '.' . env('TENANT_URL_BASE');
Tenant::create($fqdn);

This will allow us to create tenants using artisan like so:

php artisan tenancy:make-tenant test

…which would create the tenant test.xyz.com. Pretty cool, eh?

You can validate this by checking the hostnames table in your system database to see if test.xyz.com was created!

Changing routes

Now that we have all of the required tenancy changes in place, we need to alter our routes to support these changes. In our setup, an administrative user is required to run the make-tenant command before a user can navigate to a given tenant site.

Because of this, it should not be possible for a user to log in or register from the main site (the site that doesn’t have a tenant associated with it). To make this happen, we need to set up our routes file and modify some templates!

Change your routes/web.php file to look like this:

<?php
Route::domain('xyz.com')->group(function () {
Route::get('/', function () {
return view('welcome');
});
Route::any('{any}', function () {
abort(404);
})->where('any', '.*');
});
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
Route::get('/', 'HomeController@index');

Fixing registration

Registration is broken! By default, the registration controller checks to ensure that the registering user’s email address is unique in the users table. However, that validation doesn't pare down to the tenant database. We need to change that!

Modify app/Http/Controllers/Auth/RegisterController.php and change the email address validation from:

'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],

to:

'email' => ['required', 'string', 'email', 'max:255', 'unique:tenant.users'],

Changing the layout file

On the main site’s page, we see the Login and Register link, which is not desired. Modify the file resources/views/welcome.blade.php and remove the lines starting with @if (Route::has('login')) and ending at @endif at the same indentation level. We don't want users to try to login or register from this page.

Wrapping Up

Congratulations! At this point, you have a fully functioning, multi-tenant supporting, multi-database managed Laravel app. You can create new tenants by running php artisan tenancy:make-tenant hello, for instance, to create a new tenant at hello.xyz.com. To register a user, navigate to hello.xyz.com (of course, substituting xyz.com with your real domain) and click Register.

What you can do from here is unlimited, and I have to stop somewhere. Go make something great :-)

Full code: https://github.com/alexleeelkins/Laravel-Multi-Tenant-Example

Sources of inspiration:

https://medium.com/faun/laravel-multi-tenant-app-setup-part-0-ee4c730f4c2a