DEV Community

Cover image for Building a Pseudo Multi-Tenant App in Strapi: Step-by-Step
Strapi for Strapi

Posted on • Edited on • Originally published at strapi.io

Building a Pseudo Multi-Tenant App in Strapi: Step-by-Step

In Strapi, a multitenant approach refers to the ability of the system to serve multiple tenants or customers, each with its own isolated and independent set of resources such as content, users, and permissions. It allows you to create multiple application instances using a single codebase. In this way, all clients have a separate database, theming, and separate domain for each Strapi instance. The advantage of running multitenant is you can roll out the new feature easily to all clients easily and it involves low maintenance with a scalable solution.

This tutorial will show how to run multiple Strapi applications sharing the same code. This is not the complete approach for multitenancy. But it solves some problems, like rolling the same features to multiple websites simultaneously. I have been running around ten applications, such as driving school, jewelry shop, personal portfolio, and educational consultancy.

The approach is simple. I have a separate database for each instance or website, but they all run from a single code base. So it somehow acts as a multi-tenant. I have been able to manage different themes for each client but can roll out the same features for all instances at once.

Prerequisites

Create a Strapi Application

Create a Strapi project running the following command.

     npx create-strapi-app@latest strapi-multitenancy
Enter fullscreen mode Exit fullscreen mode

Terminal

Once all the dependencies are installed, it should be up and running at http://localhost:1337/
By default, Strapi runs on the SQLite database. Stop the app and let’s change the database to MySQL.

Add MySQL

Navigate to the directory where you created strapi-multitenancy (or your custom project name). Then run the following command to add the MYSQL package.

     npm install mysql --save
Enter fullscreen mode Exit fullscreen mode

Changing Strapi Config

Initially, the project structure and config folder should look like this:

Project Structure

Suppose we want to run three website development, site1, and site2. Then the new config structure should look like the one below:

Config Structure

By default, all the files inside the directory are the same as the files being created.

Creating MySQL Database

I assume that you have a MySQL server running. Let's create three databases:

  • development
  • site1
  • site2

Keep the database username and password safe for later use.

Update Database Configuration

After creating the database and config directory, let's modify the database configuration.
The default database.js file is as below.

    const path = require('path');

    module.exports = ({ env }) => ({
      connection: {
        client: 'sqlite',
        connection: {
          filename: path.join(__dirname, '..', env('DATABASE_FILENAME', '.tmp/data.db')),
        },
        useNullAsDefault: true,
      },
    });
Enter fullscreen mode Exit fullscreen mode

We want to change this to MySQL and each website specific. In my environment, the database username is root and the database password is root.

Now you will update site1 and site2 as below.

The database config for site1:

    module.exports = ({ env }) => ({
      connection: {
        client: 'mysql',
        connection: {
          host: env('DATABASE_HOST', 'localhost'),
          port: env.int('DATABASE_PORT', 3306),
          database: env('DATABASE_NAME', 'site1'),
          user: env('DATABASE_USERNAME', 'root'),
          password: env('DATABASE_PASSWORD', 'root'),
          ssl: env.bool('DATABASE_SSL', false),
        },
      },
    });
Enter fullscreen mode Exit fullscreen mode

The database config for site2:

    module.exports = ({ env }) => ({
      connection: {
        client: 'mysql',
        connection: {
          host: env('DATABASE_HOST', 'localhost'),
          port: env.int('DATABASE_PORT', 3306),
          database: env('DATABASE_NAME', 'site2'),
          user: env('DATABASE_USERNAME', 'root'),
          password: env('DATABASE_PASSWORD', 'root'),
          ssl: env.bool('DATABASE_SSL', false),
        },
      },
    });
Enter fullscreen mode Exit fullscreen mode

Update Host Configuration

Now we need to specify different host ports for running three applications. The default server.js file is as below, which we need to update.

    module.exports = ({ env }) => ({
      host: env('HOST', '0.0.0.0'),
      port: env.int('PORT', 1337),
      app: {
        keys: env.array('APP_KEYS'),
      },
    });
Enter fullscreen mode Exit fullscreen mode

Let’s modify for app host. This is a configuration for app site1:


    module.exports = ({ env }) => ({
      host: env('HOST', '0.0.0.0'),
      port: env.int('HOST_PORT_SITE1', 4338),
      app: {
        keys: env.array('APP_KEYS'),
      },
    });

Enter fullscreen mode Exit fullscreen mode

This is a configuration for app site2:

    module.exports = ({ env }) => ({
      host: env('HOST', '0.0.0.0'),
      port: env.int('HOST_PORT_SITE2', 4339),
      app: {
        keys: env.array('APP_KEYS'),
      },
    });

Enter fullscreen mode Exit fullscreen mode

Install PM2 Runtime

We will be using PM2 for running our Strapi application.

Install PM2 by running:

    npm install pm2@latest -g
Enter fullscreen mode Exit fullscreen mode

The pm2 configuration file must be located at the root level and the filename as ecosystem.config.js.

The pm2 configuration is as below.

    module.exports = {
      apps: [
        {
          name: 'site1',
          cwd: '/Users/bikramkawan/Bikram/strapi-multitenancy-v4',
          script: 'npm',
          args: 'start',
          env: {
            NODE_ENV: 'site1',
            HOST_PORT_SITE1: 4338,
            DOMAIN_URL: 'site1.example.com'
          }
        },
        {
          name: 'site2',
          cwd: '/Users/bikramkawan/Bikram/strapi-multitenancy-v4',
          script: 'npm',
          args: 'start',
          env: {
            NODE_ENV: 'site2',
            HOST_PORT_SITE2: 4339,
            DOMAIN_URL: 'site2.example.com'
          }
        }
      ]
    };

Enter fullscreen mode Exit fullscreen mode

From the above configuration, you need to change the correct path where your application is:

  • cwd -> Change to the directory where you create the Strapi application
  • Database credentials
  • Host ports
  • DOMAIN_URL is for a fully qualified domain like strapimultitest1.example.com

Testing on Local Environment (localhost)

At this point, the app should be running on your local environment you are doing on your local machine. It should accessible via localhost or either public IP.

Run the app running this command on the root:

    pm2 start ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode

Ideally, you should be able to access http://localhost:4338/ and http://localhost:4339/.

For more documentation on pm2, you can read from here

Configuring nginx for Live (Production)

Based on your distro, you need to install nginx correctly. I am running ubuntu 20.04. Please feel free to follow any documentation for installing nginx on your distro. I followed the DigitalOcean article for installing nginx.

It is important to make sure that port 80 and port 443 (for SSL) as open. After successfully installing, we will configure the correct file.

Go to cd /etc/nginx/sites-available, then create the file for site1.example.com using vi strapimultitest1.example.com. Simply vi /etc/nginx/sites-available/site1.example.com and paste the following. Change the corresponding port depending on your application server 127.0.0.1:4338;

    upstream site1.example.com {
      server 127.0.0.1:4338;
      keepalive 64;
    }

    server {
      server_name site1.example.com;
      access_log /var/log/nginx/site1.example.com-access.log;
      error_log /var/log/nginx/site1.example.com-error.log;
      location / {
        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_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://site1.example.com;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_cache_bypass $http_upgrade;
      }
    }

    server {
      listen 80;
      server_name site1.example.com;
    }


    server {
      server_name site1.example.com;
        listen 80;
        return 404; 
    }
Enter fullscreen mode Exit fullscreen mode

We need to create another file for site2 as:

    upstream site2.example.com {
      server 127.0.0.1:4339;
      keepalive 64;
    }

    server {
      server_name site2.example.com;
      access_log /var/log/nginx/site2.example.com-access.log;
      error_log /var/log/nginx/site2.example.com-error.log;
      location / {
        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_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://site2.example.com;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_cache_bypass $http_upgrade;
      }
    }

    server {

      listen 80;
      server_name site2.example.com;

    }


    server {
      server_name site2.example.com;
        listen 80;
        return 404; 

    }

Enter fullscreen mode Exit fullscreen mode

After creating nginx config file then, run:

    sudo ln -s /etc/nginx/sites-available/site1.example.com /etc/nginx/sites-enabled/site1.example.com

    sudo ln -s /etc/nginx/sites-available/site2.example.com /etc/nginx/sites-enabled/site2.example.com
Enter fullscreen mode Exit fullscreen mode

Check if nginx config is ok by running:

    sudo nginx -t
Enter fullscreen mode Exit fullscreen mode

Restart nginx:

    sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

If port 80 is open, both applications site1.example.com and site2.example.com should be accessible.

Install SSL Certificate

Let's secure our application and install SSL certificate. Check the Strapi documentation to install SSL certificate.

Run the following command:

    sudo apt install certbot python3-certbot-nginx -y
Enter fullscreen mode Exit fullscreen mode

Now we need to add our domains:

    certbot --nginx -d site1.example.com -d site2.example.com
Enter fullscreen mode Exit fullscreen mode

It should now be running SSL on both of the domains.

Conclusion

In this tutorial, we learn how to create a multitenant on Strapi. This means you can run multiple Strapi instances with separate domains and separate databases. But with the same code base. From this approach, we will save time on maintaining applications and easy to roll out new features easily.

Here is the GitHub repo I have created for the approach you can test on your own at localhost or production.

You can join the Discord community to connect with more people using Strapi. You can ask for help or get answers to your question from the community.

Let me know if you run into an issue in the comments. Thank you!

Top comments (1)

Collapse
 
hrtsislian profile image
Hristos Tsislianis • Edited

Thanks this is a great idea - exactly the logic i need for my use case - but the caveat is you can't really have separate dev environments for each site can you? I tried to implement it, it works as such - and i also configured ecosystem.config.js to optionally start in development environments - but as it seems there is only one overarching development environment - no matter if i start site1 or site2 or the development website in development mode. That means i cannot make specific changes only to site1 or site2 - every change "bleeds-over" to the other app too. Is this the intended use of this solution or am i missing something?? Sheding some light on this would be highly appreciated - been trying to figure this out for weeks now!! Thanks in advance.