Create a Multi-Tenant Laravel App With Docker
Time to read: 10 minutes
Create a Multi-Tenant Laravel App With Docker
When it comes to modern software architecture, multi-tenancy is a significant consideration and is essential for software developers to grasp. But why? Let’s first explain what multi-tenancy is.
Multi-tenancy denotes an architecture where a single instance of an application serves multiple users, or "tenants". In a multi-tenant application, one codebase along with resources is dynamically shared and allocated among all the tenants. Even though the codebase is shared, the isolation of each tenant’s data is paramount. Systems are built in such a way that each tenant’s data and identity are protected.
Multi-tenancy comes with many advantages, including scalability, as all tenants are being served from the same infrastructure, which shares and uses resources in a cost-effective manner. Additionally, a multi-tenant infrastructure allows for better configurability options, as well as reducing the burden for system administrators to maintain the system for upgrades, patches, and bug fixes.
All these advantages combine to provide system administrators and users with a great experience at a lower cost, and many of these systems charge based on subscription-based pricing models.
In this article, you’ll be building a multi-tenant system with Laravel and Docker Compose. Why? Well, Docker Compose provides an easy way to set up all the right tools and infrastructure for the app. It also provides the ability to easily move the app between systems, such as from your local development environment to a production environment. We’ll discuss all the tools you’ll need, along with tips on how to set up your database.
Prerequisites
Working knowledge of PHP and Laravel (we'll be working with version 10)
Composer globally installed
Docker Desktop installed. For more information on Docker and how useful it is, refer to this article
Set up the application
Let’s call this app WebApartment, where users will be able to make their own spaces on the internet. Navigate to the folder where you store your projects and run the following scripts in your terminal; it will generate a new Laravel project and change into the new project directory:
You will now install Laravel Sail, which is a CLI for interacting with your local Docker environment, with the command below. Upon installation, it will prompt you to choose the services you want to install. For the time being, select only pgsql. You will manually construct your docker-compose.yml file, which is utilized to build the Docker instance for this project, shortly.
Next, install Laravel Breeze, a tool that facilitates the straightforward setup of basic authentication features for Laravel. These features encompass registration, login, password reset, and more. Additionally, for this project, you'll be employing a React frontend, and use Breeze to simplify its setup. Execute the following command:
Finally, install the Tenancy For Laravel package. This package will assist in implementing our multi-tenancy strategy for the project, and will save time by automating the process of having each tenant create its own database. Install the package by executing the following command:
Update the Docker Compose configuration
docker-compose.yml provides the Docker Compose configuration, outlining the containers that compose our project. Open the file, which you'll find in the root directory of your project, in your editor or IDE of choice.
Currently, the only services you should find in the file are laravel.test and pgsql (note that these names might differ for you). Beneath pgsql, locate the volumes property. Update it to match the following code.
The changes inform Docker that you will relocate the SQL initialization script from the Laravel Sail package folder to the ./database folder. This allows you to make adjustments for a specific reason, which will be explained shortly.
For the first value, note that the mount point has been modified to /var/lib/postgresql/tenants/data instead of /var/lib/postgresql/data. This adjustment allows the separation of tenant-specific data within the PostgreSQL (pgsql
) container, and ensures the segregation of tenant information from the 'landlord' information.
The term 'landlord' refers to the main PostgreSQL database that will store all application information as well as details on how to connect to each tenant.
Certain environment variables will need to be changed to successfully run our Docker configuration. One such variable is APP_PORT
, which should be added to the .env file. Set it equal to an available port on your machine if 80 is already in use. In my case, I set it to 8000.
If you have PostgreSQL on your local machine or another program running on port 5432, include FORWARD_DB_PORT
in the .env file. I have it at 5430, but you can choose any open port. Lastly, modify the value of DB_DATABASE
to webapartment
.
Here's what it should look like:
Remember earlier when you edited the entry point for the database volume in the Docker config file? In this step, you’ll create that file. Sail, currently, only has permission to perform CRUD operations with the central database.
To ensure that Sail has permission to read and write to your tenant databases, create a folder in the database folder called entry-point. Then, in that folder, create a file called create-testing-database.sql. With that done, copy and paste the below code into the new file:
Due to all the changes you’ve made to your config file, you’ll need to rebuild your container images. Afterward, you’ll be able to spin up your new Docker Compose instance. Below are the two commands that will help you do this.
The Tenancy package
To publish the resources related to the Laravel Tenancy package, including the tenancy config file, run the following command:
For this project, you’ll to make a couple of changes:
The central application (landlord) to redirect requests to the correct tenant, accessible by their subdomain.
For the application to create and recognize tenant databases, you’ll need to create two new models that extend the ones provided by the package: Domain and Tenant.
Then, in the tenancy.php config file, extend the two models with the custom ones you’ve made
In the new Domain model code below, note that the booted()
method is invoked, and there’s a creating()
function inside there. This ensures that the domain
attribute is modified before the model is created. The Tenancy package has its own way of naming domains and storing that information to the database. If allowed to do this, it will interfere with your goal of standardizing the naming and identifying tenants through the correct subdomains.
The only real change you’ll make to the Tenant model is implementing TenantWithDatabase
, which, as the name suggests, is the interface that will enable you to create and identify separate databases for each tenant.
Create the models by executing the following commands:
Once that is done, go to app/Models/Domain.php and copy and paste the following code to it:
Afterward, go to app/Models/Tenant.php and copy and paste the following code to it:
Finally, go to config/tenancy.php and replace the two default models with the ones you created, like this:
In the same file, scroll down to the central_domains
array and replace what is there with just localhost
. This array will come in handy when we create the admin area in the next tutorial.
To ensure the right connection is used when a new tenant database is created, scroll down to the database
array where you’ll see template_tenant_connection
. Change the value for this key from null
to pgsql
, like so:
You have one more optional change that you can make, and that is to have the PostgreSQL manager create separate tenant schemas rather than separate databases, which is the default option. You may want to use this option for the following advantages:
It is more resource-efficient as these schemas are within a single database, which is perfect for small and medium-sized applications.
It’s easier to find and maintain tenant schemas, especially when you’ll need to do arduous database tasks such as backups, migrations, and checking data integrity.
However, as you can imagine, this option is not so great for larger applications, especially when you need to scale your database horizontally across different servers. It’s not impossible, but it’s a lot more difficult. Also, security is limited because schemas are not isolated like separate databases. This could run you afoul of compliance for many critical applications in healthcare and government.
Two other files were made along with the tenancy config file: routes/tenant.php and app/Providers/TenancyServiceProvider.php. You’ll be working on the ServiceProvider file in the next section, but for this article, the tenant.php route file will not be touched.
Alter the Tenant Creation event
For this app, only the "landlord" will be allowed to create new tenants. Hence, you’ll need to make some changes so that a new user can be created during the tenant creation process. The TenancyServiceProvider.php file will be instrumental during this process.
The TenancyServiceProvider.php file is probably one of the most pivotal files you’ll work with, as it acts as a central hub, orchestrating and responding to all events related to tenancy. It handles the registration of events crucial to the tenancy lifecycle.
If you look at how these events are grouped, you’ll realize that various stages are covered such as tenancy creation, domain creation, database creation, and deletions, etc. Additionally, you will find event listeners under certain events that trigger specific actions or behaviors tied to these events.
Look at the TenantCreated
event, for instance. When this event occurs, you should see a job pipeline that includes the creation of the database and the migration of that database. You’ll be creating a new job that will create a new user in this event.
First, you’ll need to step out of this file for a second and go to the database/migrations folder. There, if the folder hasn’t already been created, create a new folder called tenant. Copy all the files in the migrations folder, except the *create_tenants_table.php and the *create_domains_table.php migration files, and paste them into the tenant folder.
Next, go to your terminal and run the following command to create the job that will be responsible for creating the main user for each tenant:
Once you’ve done that, you will be adding code to app/Jobs/CreateTenantMainUser.php that will create the new tenant main user. In the code sample below, you’ll see that the tenant’s context is passed into the class through the __construct()
method. This context is then used in the handle()
method, where the callable within the $this->tenant->run()
function is responsible for first encrypting the password and creating the user. If you don’t encrypt the password before the user is created, the password will be stored in plain text.
Overwrite the contents of the CreateTenantMainUser.php file with the code below:
Once that’s done, go back to app/Providers/TenancyServiceProvider.php and add your newly created job to the job pipeline under the TenantCreated
event, like so:
Finally, go to config/app.php and scroll down to the providers
array and add TenancyServiceProvider
like so:
Complete the application setup
There are a few more things that you will need to do to finish the setup of your multi-tenant application. First, you’ll need the resources from Laravel Breeze to be published and installed. You can do this via a regular terminal window by running the following commands:
Finally, you’ll need to run the migrations. But first, add the following line to the Schema::create()
functions in the up()
method in both the *create_domains_table
and *create_tenant_table
migration files (best to add it after the $table->timestamps()
line):
Now, run your migrations by doing the following in a new terminal tab or window:
You also have the option of running a terminal window from the Docker environment. The easiest way to do this is by opening Docker Desktop, clicking Containers in the left-hand side menu, navigating to the webapartment instance, expanding it if necessary, and clicking on the Action button (the three vertical dots) beside the laravel.test container. A menu will appear and you will then click on Open in Terminal. A full-window terminal should appear and you’ll be able to run commands as normal without appending sail
to the front.
Create new tenants
To test that our implementation works, we’ll be using Laravel Tinker, which should already be installed. Tinker is an invaluable tool that will help you interact with your Laravel application at the command line. It’s an interactive shell that will allow you to dynamically experiment, test, and debug code snippets.
In this way, you can quickly get insights into your application's data and behavior. For this project, you will use Tinker to interact with Eloquent ORM and the Tenancy package to ensure that it is working properly in the creation of new tenants.
First, open Tinker by running the following command in your terminal:
Then, you’ll create your first tenants along with their default users with their corresponding databases by running the following commands, after replacing the placeholders with your name and email, respectively:
Your terminal should look something like this for the first tenant:
You might have realized that under the Domain
model, the value for domain
is foo.localhost
. This wasn’t magic; it was actually due to the function that you added to the booted()
method in the Domain
model. You’ll also see that the password was hashed due to the Job that you added to the Job pipeline in the TenancyServiceProvider.php file.
But why does tenancy_db_name
have a value of tenantfoo
and not just foo
? Go back to config/tenancy.php, scroll down until you get to the database
array. Scroll down some more and you’ll see the prefix
and suffix
keys, with tenant
as the value for prefix
. It either prefixes and/or suffixes the tenant ID, which in this case, is foo
. You can change the prefix or the suffix to anything you want.
Now, look at the second tenant below. In this instance you did not add an ID, yet one was made automatically. If you look at the tenancy config file again and scroll to the top, you’ll see the `id_generator` key with Stancl\Tenancy\UUIDGenerator::class
as a value. This is the tool that is responsible for generating an ID for the Tenant
model when none is inputted. Also, as seen in the example above, the ID was used to create a name for the new tenant database.
So, great! You can see the new tenants being created in our central database, but you may be wondering how you’ll know that the tenant databases were actually created. Go back to Docker Desktop and open a new terminal for the pgsql container.
Next, once you run the command below, you should see several databases, with one being the main central database (webapartment) and two being the new tenant databases that you created above.
You can also get the ID of the pgsql container and run this command in a new terminal window:
That's how to set up a Laravel multi-tenant application with Docker Compose
The Tenancy For Laravel package offers a powerful method to swiftly set up a multi-tenant application utilizing multiple databases. As outlined earlier, this architectural approach proves highly beneficial for scaling extensive applications, while ensuring the secure isolation of each tenant's data.
The importance of multi-tenancy in contemporary web applications cannot be overstated. As businesses progress, efficiently managing and scaling applications for numerous tenants becomes crucial.
The subsequent article will delve further into enhancing this application by implementing CRUD operations with the assistance of Filament. You will gain insights into the fundamentals of this robust package for effective tenant management.
Lloyd Miller is an experienced web developer specializing in Laravel and React. He enjoys creating consumer-facing products, especially in the e-commerce space. You can learn more about him on LinkedIn.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.