Utiliser Coolify et Nuxt en multi tenant avec des sous-domaines dynamiques
Lorsqu'on construit une application, c'est assez courant de créer des "instances" de votre site par client.
Par exemple, mettons que vous souhaitiez avoir : clientA.monapp.com et clientB.monapp.com
Eh bien je vous propose de voir comment on peut mettre ça en place avec Coolify.
Coolify
Commencons par le commencement. C'est quoi Coolify ?
Coolify c'est un peu votre PAAS personnel. C'est une base de lancement pour déployer vos applications, mais aussi bénéficier de bases de données managées (postgresql, redis, mysql etc..) ou des services managées (listmonk, mautic, un PDS bluesky etc...)
C'est un logiciel opensource qui remplace les PAAS du marché, que ce soit Heroku, Clever Cloud etc...
Bon évidemment, si vous l'hébergez vous-même, ça veut aussi dire que vous l'opérez vous-même, donc il faut le monitorer, le mettre à jour etc...
Cela dit, il existe une version cloud payante mais c'est pas le sujet de ce billet.
Bref, Coolify c'est ce que j'utilise pour hakanai.io et bientôt pour un autre produit sur lequel je travaille et qui devrait sortir fin du mois.
Une application multi tenant ?
Une application multi tenant, c'est globalement une application unique qui permet de servir plusieurs clients.
Par exemple Notion est une application multitenant. Vous avez une seule application qui supporte tous les utilisateurs.
A l'inverse de Jira, qui est une application multi instance, c'est-à-dire que chaque client installe une instance du logiciel.
Les bases de données ne se voient pas.
Donc gardez en tête que, dans mon cas, j'ai une application web, hébergé sur Coolify, qui doit fonctionner pour tout un tas de clients différents.
Et pourtant, je souhaite leur donner l'illusion d'être sur des instances différentes, en attribuant à chacun un sous domaine.
Donc par exemple client A => clientA.monapp.com et client B => clientB.monapp.com
Pourtant dans les faits, chaque client va atterrir sur la même application.
C'est-a-dire que par exemple :
- clientA.monapp.com => 192.168.0.1
- clientB.monapp.com => 192.168.0.1
La configuration Coolify
Pour pouvoir router l'ensemble du trafic correspondant à clientA, clientB, clientC sur tout les sous domaines de monapp.com, on va donc utiliser les wildcard domains
C'est à dire que votre objectif c'est de répondre sur *.monapp.com
On pourrait croire qu'il suffit de rentrer dans "Domains" le wildcard suivant : https://*.monapp.com
Or c'est pas exactement le cas, sinon j'aurais pas fait d'article à ce sujet ^^
La subtilité, c'est qu'il va falloir éditer les Container labels
qui définissent la conf de Traefik.
En principe ça ressemble à quelque chose comme ça :
traefik.enable=true
traefik.http.middlewares.gzip.compress=true
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
traefik.http.routers.http-0-ow8g70707sockck8sgo8kc55g.entryPoints=http
traefik.http.routers.http-0-ow8g70707sockck8sgo8kc55g.middlewares=redirect-to-https
traefik.http.routers.http-0-ow8g70707sockck8sgo8kc55g.rule=Host(`*.monapp.com`) && PathPrefix(`/`)
traefik.http.routers.http-0-ow8g70707sockck8sgo8kc55g.service=http-0-ow8g70707sockck8sgo8kc55g
traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.entryPoints=https
traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.middlewares=gzip
traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.rule=Host(`*.monapp.com`) && PathPrefix(`/`)
traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.service=https-0-ow8g70707sockck8sgo8kc55g
traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.tls.certresolver=letsencrypt
traefik.http.routers.https-0-ow8g70707sockck8sgo8kc55g.tls=true
traefik.http.services.http-0-ow8g70707sockck8sgo8kc55g.loadbalancer.server.port=3000
traefik.http.services.https-0-ow8g70707sockck8sgo8kc55g.loadbalancer.server.port=3000
caddy_0.encode=zstd gzip
caddy_0.handle_path.0_reverse_proxy={{upstreams 3000}}
caddy_0.handle_path=/*
caddy_0.header=-Server
Il y a deux règles qui nous intéressent et qui définissent sur quoi va réagir Traefik
Host('*.monapp.com')
Cette règle va accepter tout le trafic qui arriverait sur *.monapp.com mais pas sur clientA.monapp.com
Pour cela, on va éditer la règle pour utiliser une expression régulière :
HostRegexp(`^.+\.monapp\.com$`)
Il suffit de restart l'application, et désormais elle répond sur tout les sous domaines de monapp.com
La gestion des tenants sur nuxt
À partir de là, vous devez quand même faire la différentiation entre les sous domaines uniquement en vous basant sur le nom du sous domaine.
Pour ça, vous allez lire la request.
Dans une application Nuxt, vous pouvez déporter cette logique dans un composable comme celui-ci :
export const useTenant = () => {
// Server side
if (import.meta.server) {
const headers = useRequestHeaders()
const host = headers.host || ''
const parts = host.split('.')
if (parts.length <= 2) {
return null
}
return parts[0]
}
// Client side
if (import.meta.client) {
const host = window.location.host
const parts = host.split('.')
if (parts.length <= 2) {
return null
}
return parts[0]
}
return null
}
À partir de là, il suffira d'appeler useTenant pour obtenir le nom du sous domaine, qui correspond donc à votre identifiant d'instance.
Et, c'est tout :)
(c'est marrant parce que ca s'explique en moins de 150 lignes, mais j'ai galéré dessus pendant 1 jour parce que je lis mal la doc ^^)