DEV Community

Dinh Doan Van Bien
Dinh Doan Van Bien

Posted on

Partie 6 — Deux instances

Partie 6 sur 7 — Supabase en auto-hébergement : retour d'expérience

Version française de Part 6 — Two instances.

L'offre gratuite de Supabase inclut deux projets actifs. Je les utilisais déjà tous les deux. Ajouter une deuxième instance au cluster auto-hébergé n'était pas une question de capacité : c'était une question d'isolation. Quand Supabase fait tourner deux projets sur la même infrastructure, comment les maintient-il séparés ? Ce billet répond à cette question en le faisant concrètement.


Ce que signifie l'isolation ici

Quand je dis que les deux projets sont isolés, voici ce que cela recouvre.

Isolation réseau. Chaque projet possède son propre réseau overlay Docker. Dans le fichier compose, vous écrivez internal: comme nom de réseau, mais quand vous déployez avec docker stack deploy ... project1, Swarm préfixe automatiquement ce nom avec le nom du stack. Le réseau devient project1_internal à l'exécution. Les services de project1 ne peuvent pas atteindre les services de project2 au niveau réseau, même si les deux fichiers compose définissent un réseau appelé internal.

Isolation des données. Chaque projet dispose de son propre conteneur Postgres avec son propre volume. Les bases de données ne partagent ni stockage, ni connexion, ni aucun moyen de se joindre.

Isolation de l'authentification. Les secrets JWT sont différents. Les tables d'utilisateurs sont différentes. Un jeton émis par project1 n'est pas valide sur project2, et vice versa. Les clés API (clé anon, clé service role) sont différentes.

Isolation du routage. Des sous-domaines différents, des certificats différents.

Les seules ressources partagées sont le proxy inverse Traefik (qui se trouve en dehors des deux stacks) et le CPU et la RAM du serveur physique.


Le fichier compose est presque identique

Le fichier compose de project2 est une copie de celui de project1, avec ces modifications :

  1. Les valeurs proviennent d'un chemin Vault distinct (secret/project2)
  2. Des noms de routeurs Traefik différents (c'est le point critique)
  3. Des règles de sous-domaine différentes dans les labels Traefik
  4. Un FLY_ALLOC_ID différent pour Realtime (project2-realtime)

Les noms de réseau ne se modifient pas manuellement : Swarm ajoute automatiquement le nom du stack comme préfixe.


Les noms de routeurs doivent être uniques

C'est le seul détail de configuration qui cassera votre deuxième instance si vous le manquez.

Traefik identifie les routes par leur nom de routeur. Si deux services enregistrent un routeur avec le même nom, Traefik en choisit un et ignore l'autre. Sans erreur, sans message de log indiquant le conflit.

Dans project1, nous avons nommé nos routeurs p1-kong et p1-studio :

traefik.http.routers.p1-kong.rule: Host(`kong.project1.yourdomain.com`)
traefik.http.routers.p1-studio.rule: Host(`studio.project1.yourdomain.com`)
Enter fullscreen mode Exit fullscreen mode

Dans project2, ils doivent être différents :

traefik.http.routers.p2-kong.rule: Host(`kong.project2.yourdomain.com`)
traefik.http.routers.p2-studio.rule: Host(`studio.project2.yourdomain.com`)
Enter fullscreen mode Exit fullscreen mode

Il en va de même pour les noms de services et les noms de middlewares :

traefik.http.services.p2-kong.loadbalancer.server.port: '8000'
traefik.http.middlewares.p2-studio-auth.basicauth.users: ...
traefik.http.routers.p2-studio.middlewares: security-headers@swarm,p2-studio-auth@swarm
Enter fullscreen mode Exit fullscreen mode

Préfixez tout avec l'identifiant du projet. Cela prend trente secondes à faire correctement.


Secrets Vault séparés

Stockez les secrets de project2 sous un chemin distinct :

vault kv put secret/project2 \
  POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
  JWT_SECRET="$(openssl rand -hex 32)" \
  SUPABASE_ANON_KEY="<different anon jwt>" \
  SUPABASE_SERVICE_ROLE_KEY="<different service_role jwt>" \
  API_EXTERNAL_URL="https://kong.project2.yourdomain.com" \
  GOTRUE_EXTERNAL_URL="https://kong.project2.yourdomain.com" \
  SITE_URL="https://kong.project2.yourdomain.com" \
  DB_ENC_KEY="supabaserealtime" \
  GOTRUE_MAILER_AUTOCONFIRM="false" \
  SECRET_KEY_BASE="$(openssl rand -hex 64)" \
  PG_META_CRYPTO_KEY="$(openssl rand -hex 16)"
Enter fullscreen mode Exit fullscreen mode

Créez un jeton en lecture seule séparé pour project2 :

vault policy write project2-readonly vault-policy-project2.hcl
vault token create -policy=project2-readonly -ttl=8760h -format=json \
  | jq -r '.auth.client_token' > /root/project2-token.txt
Enter fullscreen mode Exit fullscreen mode

Stockez le jeton dans /root/project2-token.txt et ajoutez-le comme secret GitHub Actions séparé si vous utilisez des déploiements automatisés.


Mémoire avec deux instances

En faisant tourner deux stacks complets, voici approximativement comment les 4 Go sont utilisés :

Service Projet 1 Projet 2 Total
PostgreSQL ~77 Mo ~77 Mo ~154 Mo
Kong ~229 Mo ~185 Mo ~414 Mo
GoTrue ~12 Mo ~12 Mo ~24 Mo
PostgREST ~17 Mo ~17 Mo ~34 Mo
Realtime ~168 Mo ~168 Mo ~336 Mo
Storage ~18 Mo ~18 Mo ~36 Mo
postgres-meta ~68 Mo ~68 Mo ~136 Mo
Studio ~170 Mo ~170 Mo ~340 Mo

Sous-total pour les deux projets : environ 1,5 Go. En ajoutant Traefik (30 Mo), Vault (140 Mo) et le système d'exploitation (environ 300 Mo), on arrive à approximativement 2,0 à 2,5 Go sur les 4 Go disponibles.

Une troisième instance rentrerait probablement. Je ne l'ai pas encore essayé.


Déployer project2

bash scripts/fetch-env-from-vault.sh project2
set -a && source instances/project2/.env && set +a
docker stack deploy -c instances/project2/docker-compose.yml project2
bash scripts/init-realtime.sh project2
Enter fullscreen mode Exit fullscreen mode

Vérifiez les deux stacks :

docker service ls
Enter fullscreen mode Exit fullscreen mode

Vous devriez voir 17 services ou plus, avec tous les réplicas à 1/1.


Vérifier l'isolation

Créez un utilisateur sur project1 et vérifiez qu'il n'existe pas sur project2 :

curl -X POST https://kong.project1.yourdomain.com/auth/v1/signup \
  -H "apikey: PROJECT1_ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"TestPass123!"}'

# Essayez les mêmes identifiants sur project2
curl -X POST https://kong.project2.yourdomain.com/auth/v1/token?grant_type=password \
  -H "apikey: PROJECT2_ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"TestPass123!"}'
# {"error":"invalid_grant","error_description":"Invalid login credentials"}
Enter fullscreen mode Exit fullscreen mode

Les systèmes d'authentification sont complètement séparés. C'est aussi de cette façon que Supabase maintient les données de ses différents clients isolées sur son infrastructure partagée. L'approche est plus simple que je ne le pensais.

Partie 7 — Sécurité et test de charge →


La série complète

  1. Pourquoi auto-héberger
  2. Le serveur
  3. Traefik et SSL
  4. La première instance Supabase
  5. Vault
  6. Deux instances, vous êtes ici
  7. Sécurité et test de charge

Top comments (0)