How to make a multi tenant website
Tags: webdev, server
Published: 2021-04-07 14:54:27

Today I'll tell how do I make wildcard subdomains and how do I handle the data between multiple user account. I thought it was hard, but turns out it is not (or maybe it is easy because I doing it wrong? I don't know).

Every time I search about how to make a multi tenant website, there are 0 articles on how to build them. I always wonder how do they do their architecture etc.

The key to this type of website architecture are just wildcard subdomain and reading the host on your code. As simple as that.

The website requirement are :

1. It's blog platform
2. Every user will have their own subdomain (zeke.bloggy.net, eren.bloggy.net, etc)
3. Every subdomain had their own data

So there are some steps.

> There are some tips later on using it with cloud

## Buy a Domain and VPS

First you must own a domain name and VPS (Virtual Private Server). I do think that this could be done in with serverless too, I just didn't tried that.

## Point your domain to the VPS

On your domain registrar DNS Management There are 4 record that you should add. You also need your VPS IP address to point to.

```txt
| domain     | record type | value                              | host                               | ttl   |
|------------|-------------|------------------------------------|------------------------------------|-------|
| bloggy.com | A Record    | 192.0.0.1                          | bloggy.com                         | 14400 |
| bloggy.com | CNAME       | bloggy.com                         | www                                | 14400 |
| bloggy.com | A Record    | 192.0.0.1                          | *                                  | 14400 |
| bloggy.com | TXT Record  | radom-value-provided-by-lestecrypt | radom-value-provided-by-lestecrypt | 14400 |
```

The IP address there are your VPS IP address the domain are your domain (till the end of this article we'll use bloggy.com)

The **TXT Record** will be given by LetsEncrypt when we need *https*, I'll on the next step.

## Setup nginx to point to your domain

Install Nginx.

Apache, Nginx, or other web server thingy have their own syntax of configuration I used Nginx here. Use your own preferences

The configuration I use for `bloggy.com` are the same as article [here](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04) try to follow that article guide (I want to keep this posts as short as possible)

The difference is when I want to register  the wildcard subdomain. The certbot command I used are different.

It is not :

```
sudo certbot --nginx -d bloggy.com -d www.bloggy.com
```

but this :

```
sudo certbot --server https://acme-v02.api.letsencrypt.org/directory -d *.example.com --manual --preferred-challenges dns-01 certonly
```

On this part you'll need to make the **TXT Record**. 

I don't know why the `-d *.example.com` won't work, seems like it is a domain registrar problem. I obtain this trick from [this article](https://medium.com/@utkarsh_verma/how-to-obtain-a-wildcard-ssl-certificate-from-lets-encrypt-and-setup-nginx-to-use-wildcard-cfb050c8b33f)

For now, you will have a working wildcard subdomain and domain. Whether you visit `bloggy.com`, `zeke.bloggy.com`, `eren.bloggy.com` you will see Nginx welcome page (If you use nginx)

## Handle the subdomain on your code

I use node.js (Express.js) for this, different app may have different syntax. But the rule are simple, read the hostname to get the account name and fetch the data;

The app had 2 routes `home` and `blog` Here the simplest code possible

```js
const express = require('express')
const app = express()
const port = 3000

const dataBlog = [
  { account: 'zeke', blog: ['so zeke thing']  },
  { account: 'eren', blog: ['so eren thing 2']  }
]

// Home route list all blog and account
app.get('/', (req, res) => {
  res.send(dataBlog)
})

// Blog route only list the blog based on host account name
// You will see that we dont use params here, we use hostname
app.get('/blog', (req, res) => {
  const host = req.headers.host // or 'x-forwarded-host'
  const accountName = host.toString().split('.')[0] // get the account name on domain.
  if (accountName) {
    const accountData = dataBlog.filter(acc => acc.account === accountName)
    res.send(accountData)
  } else {
    res.code(404).send('Not Found')
  }  
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})
```

## Proxy your web app with nginx

Node.js aren't like PHP it has its own web servers, but to contact with real world it needs proxy. So we just proxy it to our nginx.

On `bloggy.com` nginx configuration

```bash
# so the home route will be on bloggy.com
proxy_pass http://localhost:3000;
```

On the wildcard `*.bloggy.com` nginx configuration 


```bash
# so the blog route will point to *.bloggy.com
proxy_pass http://localhost:3000/blog;
```

That's it! Every time you visit `bloggy.com/blog` it will respond 'not found' but if you visit `zeke.bloggy.com` it will fetch the data for 'zeke'.

## On Cloud

So later example are for VPS, however if you want to load balance your apps with VPS it going to be little hard (in my experience).

Better use cloud service like GKE, DO, or AWS. With K8S (Kubernetess). It is more easy. You can achieve it using [ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/).

The step by step to take that are on [here](https://www.digitalocean.com/community/tutorials/how-to-set-up-an-nginx-ingress-on-digitalocean-kubernetes-using-helm) It is the same.

And you can just point your domain name to the ingress IP.

Here are some config

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: $BUDDY_PROJECT_NAME
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: 'letsencrypt-clusterissuer'
spec:
  tls:
  - hosts:
    - "bloggy.com"
    secretName: domain-$BUDDY_PROJECT_NAME
  rules:
  - host: 'bloggy.com'
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: $BUDDY_PROJECT_NAME
            port:
              number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rewrite-$BUDDY_PROJECT_NAME
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: 'letsencrypt-clusterissuer'
    nginx.ingress.kubernetes.io/rewrite-target: /blog/$1
spec:
  tls:
  - hosts:
    - "*.bloggy.com"
    secretName: wildcard-certificate-tls
  rules:
  - host: '*.bloggy.com'
    http:
      paths:
      - backend:
          service:
            name: $BUDDY_PROJECT_NAME
            port:
              number: 80
        path: "/(.*)"
        pathType: Prefix
---
```

## Closing

I hope this will help you to make your multi tenant website. Have a great day. I welcome every feedback you had! :D