<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dinh Doan Van Bien</title>
    <description>The latest articles on DEV Community by Dinh Doan Van Bien (@voieducode).</description>
    <link>https://dev.to/voieducode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F616832%2Fa77162f5-4cab-494e-9396-4ada0d9d8204.png</url>
      <title>DEV Community: Dinh Doan Van Bien</title>
      <link>https://dev.to/voieducode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voieducode"/>
    <language>en</language>
    <item>
      <title>What Is a Productype?</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 13 Mar 2026 17:44:05 +0000</pubDate>
      <link>https://dev.to/voieducode/what-is-a-productype-41f9</link>
      <guid>https://dev.to/voieducode/what-is-a-productype-41f9</guid>
      <description>&lt;p&gt;Here's the situation. You prompt an AI coding tool, it generates an app, you ship it, and people actually use it. It's not a prototype: you didn't throw it away. It's not a product: nobody's paying for it. So what is it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The definition
&lt;/h2&gt;

&lt;p&gt;A productype is software you maintain for a small audience that could become a product but hasn't yet. Picture a spectrum:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prototype&lt;/strong&gt; → built to learn, then discarded.&lt;br&gt;
&lt;strong&gt;Productype&lt;/strong&gt; → maintained, useful, serving real users, uncommitted to commercialization.&lt;br&gt;
&lt;strong&gt;Product&lt;/strong&gt; → monetized, supported, marketed.&lt;/p&gt;

&lt;p&gt;The key distinction: it works well enough that people depend on it, you keep maintaining it, but you haven't committed to turning it into a business. The quality is there. The decision isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two examples
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JiiaCat&lt;/strong&gt;: home inventory app, built with Lovable. 675 commits over ten months. Started as a photo grid for tracking stuff around the house. Now it has a map view, a container hierarchy (the toolbox is in the closet, the closet is in the bedroom), and passwordless sign-in. Solves a universal problem. No landing page, no pricing, no marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nokosha&lt;/strong&gt;: subscription tracker, also Lovable. 292 commits over seven months. Tracks payments across multiple currencies with exchange rate conversions, stores payment history, encrypts financial data. Built for exactly one person's financial situation. Useful to its maintainer, awkward for anyone else.&lt;/p&gt;

&lt;p&gt;Both maintained. Both working. Neither a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;AI coding tools changed the economics. When maintenance is cheap enough, personal software doesn't have to die after the weekend or pivot into a business. A productype can just stay a productype, for as long as it's useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full story
&lt;/h2&gt;

&lt;p&gt;I wrote a &lt;a href="https://vibing.voieduco.de/en/" rel="noopener noreferrer"&gt;five-part series&lt;/a&gt; on how these productypes came out of building with Replit, Bolt, and Lovable. What the tools actually produce, what breaks, what it takes to keep the results alive.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>indiehackers</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Partie 7 — Sécurité et test de charge</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:19:39 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-7-securite-et-test-de-charge-3ejk</link>
      <guid>https://dev.to/voieducode/partie-7-securite-et-test-de-charge-3ejk</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 7 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-7-security-and-the-load-test-1c0l"&gt;Part 7 — Security and the load test&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nous avons un cluster à deux projets qui fonctionne. Deux questions se posent maintenant : est-il réellement sécurisé, et qu'est-ce qu'il faut pour le faire craquer ?&lt;/p&gt;




&lt;h2&gt;
  
  
  Les couches de sécurité
&lt;/h2&gt;

&lt;p&gt;La sécurité ici repose sur la défense en profondeur. Plusieurs couches, chacune compliquant la tâche d'un attaquant. Aucune n'est suffisante à elle seule.&lt;/p&gt;

&lt;h3&gt;
  
  
  ufw et fail2ban
&lt;/h3&gt;

&lt;p&gt;La couche externe. Seuls les ports 22, 80 et 443 sont ouverts. fail2ban bannit les adresses IP après cinq tentatives SSH échouées. Cela arrête les scanners automatisés et les attaques par force brute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kong : authentification par clé et limitation de débit
&lt;/h3&gt;

&lt;p&gt;Chaque requête à l'API doit inclure un en-tête &lt;code&gt;apikey&lt;/code&gt; valide. Sans lui, Kong renvoie un 401 avant que la requête n'atteigne aucun service backend. GoTrue, PostgREST, Realtime, Storage : aucun d'eux ne voit le trafic non authentifié.&lt;/p&gt;

&lt;p&gt;Limitation de débit : 30 requêtes par minute par adresse IP par défaut (configuré avec &lt;code&gt;limit_by: ip&lt;/code&gt; dans le plugin rate-limiting de Kong). Cela limite les dégâts du credential stuffing et empêche d'utiliser GoTrue comme plateforme d'inscription en masse.&lt;/p&gt;

&lt;p&gt;Kong est configuré via un fichier YAML (&lt;code&gt;kong.yml&lt;/code&gt;) qui réside sur le serveur et n'est jamais versionné dans git. Les sections importantes :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;key_names&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apikey"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate-limiting&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
      &lt;span class="na"&gt;limit_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ip&lt;/span&gt;
      &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Studio protégé par une authentification basique
&lt;/h3&gt;

&lt;p&gt;Le tableau de bord Studio donne un accès administrateur complet à votre base de données. Il ne doit pas être accessible publiquement. Nous l'avons mis en place dans la partie 4 : le middleware d'authentification basique de Traefik protège la route Studio, et le hash du mot de passe est intégré dans les labels du service. Consultez la partie 4 pour la commande &lt;code&gt;htpasswd -nB&lt;/code&gt; et la règle d'échappement &lt;code&gt;$$&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Falco : détection d'intrusion à l'exécution
&lt;/h3&gt;

&lt;p&gt;ufw et Kong traitent les menaces externes. Falco surveille ce qui se passe à l'intérieur des conteneurs en cours d'exécution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://falco.org/docs/" rel="noopener noreferrer"&gt;Falco&lt;/a&gt; est un outil de sécurité qui s'accroche aux appels système Linux pour surveiller l'activité des conteneurs : accès aux fichiers, exécution de processus, connexions réseau, changements de privilèges. Nous utilisons &lt;code&gt;falco-modern-bpf&lt;/code&gt;, qui utilise eBPF comme mécanisme de capture. Il tourne sur l'hôte, hors du contrôle des conteneurs. Les événements qui correspondent à des règles déclenchent des alertes.&lt;/p&gt;

&lt;p&gt;Installez-le :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://falco.org/repo/falcosecurity-packages.asc &lt;span class="se"&gt;\&lt;/span&gt;
  | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/falco-archive-keyring.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://download.falco.org/packages/deb stable main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/falcosecurity.list

apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install &lt;/span&gt;falco &lt;span class="nt"&gt;-y&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;falco-modern-bpf
systemctl start falco-modern-bpf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les règles personnalisées se placent dans &lt;code&gt;/etc/falco/rules.d/&lt;/code&gt;. Pour notre cluster, on peut par exemple alerter sur les connexions sortantes inattendues depuis les conteneurs ou sur les commandes psql contenant des instructions destructives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Le piège des alertes.&lt;/strong&gt; La directive &lt;code&gt;program_output&lt;/code&gt; redirige les alertes vers un script. Vous pouvez vérifier qu'elle fonctionne en regardant le journal Falco pendant que vous déclenchez un événement : &lt;code&gt;journalctl -u falco-modern-bpf -f&lt;/code&gt;. Si vous voyez des événements là mais que votre script de traitement n'est jamais appelé, &lt;code&gt;program_output&lt;/code&gt; est cassé dans votre version. Le contournement fiable : un service systemd séparé qui suit le journal directement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/falco-alerter.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Falco alert handler&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;falco-modern-bpf.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c 'journalctl -f -u falco-modern-bpf -o cat | /root/supabase-vps-cluster/scripts/falco-alert.sh'&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le gestionnaire d'alertes enregistre les événements dans &lt;code&gt;/var/log/falco-alerts.log&lt;/code&gt; avec une temporisation de cinq minutes par règle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bruit attendu.&lt;/strong&gt; Les vérifications d'état nginx de Kong s'exécutent toutes les dix secondes. Chacune lance un sous-processus shell et lit &lt;code&gt;/etc/passwd&lt;/code&gt;. Falco le signale comme « Shell spawned in container » et « Sensitive file read in container ». C'est un comportement normal, pas une attaque. La temporisation maintient le journal lisible. Après vingt-quatre heures d'observation, vous pouvez affiner les règles pour exclure le processus Kong de la surveillance.&lt;/p&gt;




&lt;h2&gt;
  
  
  L'architecture de sécurité
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
    |
  ufw (ports 22/80/443 only)
    |
  Traefik (TLS, security headers)
    |
    +-- Studio (Traefik basic auth)
    +-- Kong (key-auth + rate limiting)
              |
              +-- internal services (not publicly reachable)

Host layer:
  SSH key-only auth, fail2ban
  Falco eBPF watching all container syscalls
  Vault on localhost only, UI disabled
  No published Postgres port
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Le test de charge
&lt;/h2&gt;

&lt;p&gt;Je voulais une réponse concrète à la question : quelle est la limite réelle de ce serveur ?&lt;/p&gt;

&lt;p&gt;J'ai utilisé Grafana Cloud k6 pour les tests. Avant d'en lancer un, il vous faut :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://grafana.com/docs/k6/latest/" rel="noopener noreferrer"&gt;k6&lt;/a&gt; installé en local (&lt;code&gt;brew install k6&lt;/code&gt; sur macOS, ou téléchargement depuis k6.io pour les autres plateformes)&lt;/li&gt;
&lt;li&gt;Un compte Grafana Cloud (gratuit sur grafana.com/products/cloud). L'offre gratuite permet jusqu'à 50 utilisateurs virtuels simultanés et des tests d'une durée d'environ dix minutes&lt;/li&gt;
&lt;li&gt;La clé anon et la clé service role de votre projet (depuis Vault, ou depuis le fichier &lt;code&gt;.env&lt;/code&gt; si vous n'avez pas encore configuré Vault)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trois tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 1 : ré-authentification à chaque requête
&lt;/h3&gt;

&lt;p&gt;Chaque utilisateur virtuel se connecte à chaque itération. Le test monte progressivement à 50 utilisateurs virtuels simultanés.&lt;/p&gt;

&lt;p&gt;Le serveur a craqué aux alentours de 30 à 50 utilisateurs virtuels.&lt;/p&gt;

&lt;p&gt;À 50 utilisateurs virtuels, le CPU de la base de données a atteint 100 % et y est resté. GoTrue a commencé à renvoyer des timeouts 504. Le problème est bcrypt, l'algorithme de hachage de mot de passe intentionnellement coûteux en CPU. Chaque connexion nécessite une vérification bcrypt via l'extension pgcrypto de PostgreSQL. Avec 50 utilisateurs qui se ré-authentifient à quelques secondes d'intervalle, la base de données était saturée rien que par le travail cryptographique.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Au repos&lt;/th&gt;
&lt;th&gt;Pendant le test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;51 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;100 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cela semble alarmant, mais aucune application réelle ne fonctionne ainsi. Les jetons JWT sont valables une heure. Vous vous authentifiez une fois, utilisez le jeton, le rafraîchissez à expiration. Vous ne vous ré-authentifiez pas à chaque appel API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 2 : JWT en cache, CRUD pur
&lt;/h3&gt;

&lt;p&gt;Chaque utilisateur virtuel se connecte une seule fois pendant la phase de configuration et met le jeton en cache pour toute la durée du test. Ensuite, il exécute en boucle des opérations d'insertion, lecture, suppression sans ré-authentification.&lt;/p&gt;

&lt;p&gt;Aucun point de rupture à 50 utilisateurs virtuels. L'offre gratuite de Grafana Cloud a atteint sa limite de durée de test (environ cinq minutes) avant que le serveur ne montre le moindre signe de stress.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Au repos&lt;/th&gt;
&lt;th&gt;Pendant le test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;0,02 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;9 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;13 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Le CPU de la base de données est passé de 100 % à 9 %. Le seul changement était la mise en cache du JWT. PostgREST est désormais le service le plus gourmand en CPU et finirait par devenir le goulot d'étranglement à plus forte charge, mais nous n'avons pas atteint ce plafond.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 3 : sessions réalistes
&lt;/h3&gt;

&lt;p&gt;Trois profils d'utilisateurs en parallèle, avec des temps de réflexion aléatoires entre les actions :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;70 % d'utilisateurs occasionnels (temps de réflexion de 10 à 30 secondes, principalement des lectures)&lt;/li&gt;
&lt;li&gt;20 % d'utilisateurs actifs (temps de réflexion de 5 à 15 secondes, usage mixte)&lt;/li&gt;
&lt;li&gt;10 % d'utilisateurs intensifs (temps de réflexion de 2 à 8 secondes, davantage d'écritures)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chaque utilisateur se connecte une fois par session. Des périodes d'inactivité aléatoires simulent le changement d'onglet ou une pause.&lt;/p&gt;

&lt;p&gt;Le test s'est déroulé jusqu'à son terme complet : 10 minutes 30 secondes, zéro erreur.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Au repos&lt;/th&gt;
&lt;th&gt;Pic pendant le test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;0,02 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;0,67 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;0 % CPU&lt;/td&gt;
&lt;td&gt;1,19 % CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Le serveur était essentiellement inactif.&lt;/p&gt;

&lt;p&gt;Un seuil a tout de même été franchi : la latence de lecture au 95e percentile a dépassé 300 ms. Ce n'était pas le serveur. Le test s'exécutait depuis la région Ohio de Grafana Cloud vers notre serveur en Allemagne. Le temps d'aller-retour de base est de 100 à 130 ms. Le cluster était en bonne santé tout au long du test : la latence venait du réseau, pas de l'application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ce que ces chiffres signifient
&lt;/h3&gt;

&lt;p&gt;Avec 50 utilisateurs simultanés et des temps de réflexion réalistes, il y a de 3 à 8 requêtes de base de données qui s'exécutent réellement à tout moment. Les utilisateurs ne cliquent pas en continu : ils lisent quelque chose, rédigent une réponse, réfléchissent à ce qu'ils vont faire ensuite. Le temps de réflexion change complètement la donne.&lt;/p&gt;

&lt;p&gt;Le CX22 convient parfaitement à un projet personnel avec de vrais utilisateurs. Le seul scénario qui le sature est un test de ré-authentification en boucle continue, ce qu'aucune application réelle ne fait.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que le service géré apporte
&lt;/h2&gt;

&lt;p&gt;Après avoir traversé tout cela, j'ai une vision bien plus claire de ce que le niveau gratuit de Supabase vous apporte.&lt;/p&gt;

&lt;p&gt;J'ai passé plusieurs soirées sur la configuration, le paramétrage et le débogage. J'ai eu droit à l'incident Vault. J'ai débogué des problèmes de routage Traefik, des crashs de Realtime, des portées de variables d'environnement incorrectes et le comportement des vérifications d'état dans Docker Swarm. J'ai mis en place un système de monitoring pour savoir ce que fait le serveur. Je suis responsable des mises à jour, des sauvegardes et des incidents.&lt;/p&gt;

&lt;p&gt;Supabase fait tout cela pour deux projets gratuits. L'infrastructure qui sous-tend un seul projet gratuit est plus complexe que tout ce qui est présenté dans cette série. L'équipe maintient GoTrue, PostgREST, Realtime et le reste à jour et en fonctionnement, en continu, à grande échelle.&lt;/p&gt;

&lt;p&gt;Le plan Pro — base de données avec point-in-time recovery, sauvegardes automatisées, pooling de connexions via PgBouncer, garanties de disponibilité — est un prix juste pour ce qu'il élimine de votre quotidien. Je le comprends maintenant, parce que j'ai vu concrètement ce qu'il vous épargne.&lt;/p&gt;

&lt;p&gt;Auto-hébergez si vous voulez apprendre. Utilisez le service géré si vous voulez construire.&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pourquoi auto-héberger&lt;/li&gt;
&lt;li&gt;Le serveur&lt;/li&gt;
&lt;li&gt;Traefik et SSL&lt;/li&gt;
&lt;li&gt;La première instance Supabase&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Deux instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sécurité et test de charge&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Partie 6 — Deux instances</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:19:20 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-6-deux-instances-ml4</link>
      <guid>https://dev.to/voieducode/partie-6-deux-instances-ml4</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 6 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-6-two-instances-26ll"&gt;Part 6 — Two instances&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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é ne visait pas la capacité, mais l'isolation. Quand Supabase fait tourner deux projets sur la même infrastructure, comment les maintient-il séparés ? Ce billet y répond par la pratique.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce que signifie l'isolation ici
&lt;/h2&gt;

&lt;p&gt;Quand je dis que les deux projets sont isolés, voici ce que j'entends par là.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation réseau.&lt;/strong&gt; Chaque projet possède son propre réseau overlay Docker. Le fichier compose déclare &lt;code&gt;internal:&lt;/code&gt; comme nom de réseau, mais au déploiement via &lt;code&gt;docker stack deploy ... project1&lt;/code&gt;, Swarm préfixe automatiquement ce nom avec celui du stack. Le réseau devient &lt;code&gt;project1_internal&lt;/code&gt; à l'exécution. Les services de project1 ne peuvent pas atteindre les services de project2 via leurs réseaux internes, même si les deux fichiers compose définissent un réseau appelé &lt;code&gt;internal&lt;/code&gt;. Le seul réseau partagé est &lt;code&gt;traefik_default&lt;/code&gt;, sur lequel les conteneurs Kong et Studio des deux projets coexistent pour le routage. En théorie, les conteneurs sur le même réseau overlay peuvent communiquer entre eux : l'isolation n'est donc pas absolue au niveau réseau. En pratique, chaque service n'écoute que sur son propre port et exige la clé API de son propre projet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation des données.&lt;/strong&gt; 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 communiquer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation de l'authentification.&lt;/strong&gt; Secrets JWT, tables d'utilisateurs, clés API (clé anon, clé service role) — tout est distinct d'un projet à l'autre. Un jeton émis par project1 n'est pas valide sur project2, et inversement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation du routage.&lt;/strong&gt; Des sous-domaines différents, des certificats différents.&lt;/p&gt;

&lt;p&gt;Les seules ressources partagées sont le proxy inverse Traefik (extérieur aux deux stacks) et le CPU et la RAM du serveur physique.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le fichier compose est presque identique
&lt;/h2&gt;

&lt;p&gt;Le fichier compose de project2 est une copie de celui de project1, avec ces modifications :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Les valeurs proviennent d'un chemin Vault distinct (&lt;code&gt;secret/project2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Des noms de routeurs Traefik différents (c'est le point critique)&lt;/li&gt;
&lt;li&gt;Des règles de sous-domaine différentes dans les labels Traefik&lt;/li&gt;
&lt;li&gt;Un &lt;code&gt;FLY_ALLOC_ID&lt;/code&gt; différent pour Realtime (&lt;code&gt;project2-realtime&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inutile de modifier les noms de réseau à la main : Swarm préfixe automatiquement chaque nom avec celui du stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Les noms de routeurs doivent être uniques
&lt;/h2&gt;

&lt;p&gt;C'est le seul détail de configuration qui cassera votre deuxième instance si vous passez à côté.&lt;/p&gt;

&lt;p&gt;Traefik identifie les routes par leur nom de routeur. Si deux services enregistrent un routeur avec le même nom, Traefik journalise une erreur (« Router defined multiple times with different configurations ») et les routes concernées renvoient un 404. Dans un flux de logs chargé, c'est facile à rater.&lt;/p&gt;

&lt;p&gt;Dans project1, nous avons nommé nos routeurs &lt;code&gt;p1-kong&lt;/code&gt; et &lt;code&gt;p1-studio&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.routers.p1-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project1.yourdomain.com`)&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.p1-studio.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`studio.project1.yourdomain.com`)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dans project2, ils doivent être différents :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.routers.p2-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project2.yourdomain.com`)&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.p2-studio.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`studio.project2.yourdomain.com`)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il en va de même pour les noms de services et les noms de middlewares :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.services.p2-kong.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8000'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.p2-studio-auth.basicauth.users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.p2-studio.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm,p2-studio-auth@swarm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Préfixez tout avec l'identifiant du projet. C'est l'affaire de trente secondes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secrets Vault séparés
&lt;/h2&gt;

&lt;p&gt;Stockez les secrets de project2 sous un chemin distinct :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secret/project2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;different anon jwt&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;different service_role jwt&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project2.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project2.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project2.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"supabaserealtime"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Créez un jeton en lecture seule séparé pour project2 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write project2-readonly vault-policy-project2.hcl
vault token create &lt;span class="nt"&gt;-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project2-readonly &lt;span class="nt"&gt;-ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8760h &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auth.client_token'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/project2-token.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  Mémoire avec deux instances
&lt;/h2&gt;

&lt;p&gt;En faisant tourner deux stacks complets, voici approximativement comment les 4 Go sont utilisés :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Projet 1&lt;/th&gt;
&lt;th&gt;Projet 2&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;~77 Mo&lt;/td&gt;
&lt;td&gt;~77 Mo&lt;/td&gt;
&lt;td&gt;~154 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;~229 Mo&lt;/td&gt;
&lt;td&gt;~185 Mo&lt;/td&gt;
&lt;td&gt;~414 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;~12 Mo&lt;/td&gt;
&lt;td&gt;~12 Mo&lt;/td&gt;
&lt;td&gt;~24 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;~17 Mo&lt;/td&gt;
&lt;td&gt;~17 Mo&lt;/td&gt;
&lt;td&gt;~34 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime&lt;/td&gt;
&lt;td&gt;~168 Mo&lt;/td&gt;
&lt;td&gt;~168 Mo&lt;/td&gt;
&lt;td&gt;~336 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;~18 Mo&lt;/td&gt;
&lt;td&gt;~18 Mo&lt;/td&gt;
&lt;td&gt;~36 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;postgres-meta&lt;/td&gt;
&lt;td&gt;~68 Mo&lt;/td&gt;
&lt;td&gt;~68 Mo&lt;/td&gt;
&lt;td&gt;~136 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Studio&lt;/td&gt;
&lt;td&gt;~170 Mo&lt;/td&gt;
&lt;td&gt;~170 Mo&lt;/td&gt;
&lt;td&gt;~340 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sous-total pour les deux projets : environ 1,5 Go. Ajoutez Traefik (30 Mo), Vault (140 Mo) et le système d'exploitation (~300 Mo) et l'on arrive à 2,0–2,5 Go sur les 4 Go disponibles.&lt;/p&gt;

&lt;p&gt;Une troisième instance tiendrait probablement. Je n'ai pas encore essayé.&lt;/p&gt;




&lt;h2&gt;
  
  
  Déployer project2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/fetch-env-from-vault.sh project2
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;instances/project2/.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; instances/project2/docker-compose.yml project2
bash scripts/init-realtime.sh project2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vérifiez les deux stacks :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vous devriez voir 17 services ou plus, avec tous les réplicas à &lt;code&gt;1/1&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vérifier l'isolation
&lt;/h2&gt;

&lt;p&gt;Créez un utilisateur sur project1 et vérifiez qu'il n'existe pas sur project2 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://kong.project1.yourdomain.com/auth/v1/signup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: PROJECT1_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"test@example.com","password":"TestPass123!"}'&lt;/span&gt;

&lt;span class="c"&gt;# Essayez les mêmes identifiants sur project2&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://kong.project2.yourdomain.com/auth/v1/token?grant_type&lt;span class="o"&gt;=&lt;/span&gt;password &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: PROJECT2_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"test@example.com","password":"TestPass123!"}'&lt;/span&gt;
&lt;span class="c"&gt;# {"error":"invalid_grant","error_description":"Invalid login credentials"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les systèmes d'authentification sont complètement séparés. C'est d'ailleurs ainsi que Supabase isole les données de ses clients sur son infrastructure mutualisée. Plus simple que je ne le pensais.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/partie-7-securite-et-test-de-charge-3ejk" class="crayons-btn crayons-btn--primary"&gt;Partie 7 — Sécurité et test de charge →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pourquoi auto-héberger&lt;/li&gt;
&lt;li&gt;Le serveur&lt;/li&gt;
&lt;li&gt;Traefik et SSL&lt;/li&gt;
&lt;li&gt;La première instance Supabase&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deux instances&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;li&gt;Sécurité et test de charge&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Partie 5 — Vault, et l'après-midi où j'ai tout effacé</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:18:37 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-5-vault-et-lapres-midi-ou-jai-tout-efface-1df9</link>
      <guid>https://dev.to/voieducode/partie-5-vault-et-lapres-midi-ou-jai-tout-efface-1df9</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 5 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-5-vault-and-the-afternoon-i-deleted-everything-4kdj"&gt;Part 5 — Vault, and the afternoon I deleted everything&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Je vais vous raconter l'après-midi où j'ai remplacé tous mes secrets Supabase par le mot &lt;code&gt;change_me&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mon mot de passe Postgres : &lt;code&gt;change_me&lt;/code&gt;. Mon secret JWT : &lt;code&gt;change_me&lt;/code&gt;. Ma clé service role : &lt;code&gt;change_me&lt;/code&gt;. Tout.&lt;/p&gt;

&lt;p&gt;Les services ont démarré. Les vérifications d'état ont réussi (Traefik se contente de vérifier les codes de statut HTTP). Puis chaque appel API a commencé à échouer avec des erreurs d'authentification. Je me suis connecté en SSH, j'ai inspecté l'environnement en cours d'exécution, et j'ai vu le problème immédiatement. Une seule commande mal choisie avait remplacé l'intégralité du coffre de secrets par une unique paire clé-valeur.&lt;/p&gt;

&lt;p&gt;Pas un script malveillant. Le modèle Docker Compose de Supabase utilise &lt;code&gt;${POSTGRES_PASSWORD:-change_me}&lt;/code&gt; comme valeur de repli pour chaque variable secrète — un espace réservé censé être remplacé avant le déploiement. Quand mon script de récupération a régénéré le fichier &lt;code&gt;.env&lt;/code&gt; depuis Vault, il n'a trouvé qu'une seule clé. Tout le reste est retombé sur la valeur par défaut du modèle. J'explique précisément comment c'est arrivé dans la section suivante.&lt;/p&gt;

&lt;p&gt;La bonne nouvelle : HashiCorp Vault conserve un historique complet des versions. J'ai tout récupéré depuis la version 7 de mon secret. Mais ça a été vingt minutes de stress, et je vais vous expliquer précisément comment éviter cette erreur.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi utiliser Vault
&lt;/h2&gt;

&lt;p&gt;L'alternative à Vault, c'est stocker les secrets dans des fichiers &lt;code&gt;.env&lt;/code&gt; sur le serveur. Ça fonctionne, mais avec des inconvénients réels.&lt;/p&gt;

&lt;p&gt;Le plus évident : si un fichier &lt;code&gt;.env&lt;/code&gt; se retrouve dans git par erreur, vos identifiants sont dans l'historique de façon permanente. Supprimer le fichier ne suffit pas — l'historique, lui, reste.&lt;/p&gt;

&lt;p&gt;Le moins évident : avec deux projets en cours d'exécution, vous avez deux fichiers &lt;code&gt;.env&lt;/code&gt;. Il vous faut un système pour renouveler les secrets et savoir quelle version de quel secret était déployée à quel moment. Les fichiers plats n'offrent rien de tout cela.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.hashicorp.com/vault/docs" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt; résout tous ces problèmes. Les secrets sont chiffrés au repos, l'accès est contrôlé par des jetons aux permissions spécifiques, et chaque modification est versionnée — ce qui permet de récupérer n'importe quelle version antérieure de n'importe quel secret.&lt;/p&gt;

&lt;p&gt;Nous faisons tourner Vault sur le même serveur que nos stacks Docker, lié uniquement à localhost. Il n'est jamais accessible depuis internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation de Vault
&lt;/h2&gt;

&lt;p&gt;Installez &lt;code&gt;jq&lt;/code&gt; en premier. Le script de récupération l'utilise pour parser la sortie JSON de Vault :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;jq &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-O-&lt;/span&gt; https://apt.releases.hashicorp.com/gpg | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/hashicorp-archive-keyring.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://apt.releases.hashicorp.com &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-cs&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/hashicorp.list

apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install &lt;/span&gt;vault &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Créez la configuration :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/vault
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/vault/config.hcl &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
ui             = false
disable_mlock  = true

storage "file" {
  path = "/opt/vault/data"
}

listener "tcp" {
  address       = "127.0.0.1:8200"
  tls_cert_file = "/etc/vault/tls/vault.crt"
  tls_key_file  = "/etc/vault/tls/vault.key"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Deux éléments méritent une explication.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;disable_mlock = true&lt;/code&gt; : par défaut, Vault verrouille toutes ses pages mémoire pour empêcher les secrets d'être écrits en swap. mlock n'alloue pas de mémoire supplémentaire, mais il épingle toutes les pages résidentes de Vault en RAM pour qu'elles ne puissent pas être échangées sur disque. Sur ce serveur, j'ai observé Vault à environ 376 Mo avec mlock activé contre environ 140 Mo sans, probablement parce que le noyau récupère les pages inutilisées plus agressivement quand elles ne sont pas épinglées. Sur notre serveur de 4 Go avec 17 conteneurs en cours d'exécution, désactiver mlock est un compromis raisonnable. Pour un nœud unique sans module de sécurité matériel, le risque de swap est faible.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;address = "127.0.0.1:8200"&lt;/code&gt; : Vault écoute uniquement sur localhost. Il n'est accessible que depuis le serveur lui-même, pas depuis le réseau.&lt;/p&gt;

&lt;p&gt;Vault exige TLS même pour les connexions localhost. Générez un certificat auto-signé :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/vault/tls
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /etc/vault/tls/vault.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; /etc/vault/tls/vault.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=vault-localhost"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-addext&lt;/span&gt; &lt;span class="s2"&gt;"subjectAltName=IP:127.0.0.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Créez l'unité systemd et démarrez Vault :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/vault.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=HashiCorp Vault
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/vault server -config=/etc/vault/config.hcl
ExecReload=/bin/kill --signal HUP &lt;/span&gt;&lt;span class="nv"&gt;$MAINPID&lt;/span&gt;&lt;span class="sh"&gt;
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
LimitMEMLOCK=infinity

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;vault
systemctl start vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Initialisez Vault :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault operator init &lt;span class="nt"&gt;-key-shares&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-key-threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Cette commande affiche une clé de déverrouillage et un jeton racine. Sauvegardez les deux dans un endroit sûr et hors ligne. La note sécurisée d'un gestionnaire de mots de passe convient parfaitement. Si vous perdez la clé de déverrouillage, vos données chiffrées sont définitivement inaccessibles — aucune procédure de récupération n'existe.&lt;/p&gt;

&lt;p&gt;Déverrouillez Vault :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200 &lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  vault operator unseal YOUR_UNSEAL_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stocker les secrets
&lt;/h2&gt;

&lt;p&gt;Activez le moteur de secrets KV (Key-Value) version 2 :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_ROOT_TOKEN

vault secrets &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret kv-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Stockez les secrets de project1 :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secret/project1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-anon-jwt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-service-role-jwt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project1.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project1.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project1.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"supabaserealtime"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  L'erreur : put versus patch
&lt;/h2&gt;

&lt;p&gt;KV v2 propose deux commandes pour écrire des secrets.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vault kv put&lt;/code&gt; remplace l'intégralité du secret par exactement ce que vous spécifiez. Si vous exécutez &lt;code&gt;vault kv put secret/project1 GOTRUE_MAILER_AUTOCONFIRM=true&lt;/code&gt;, le résultat est un secret contenant exactement une clé : &lt;code&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/code&gt;. Tout le reste disparaît de la version courante.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vault kv patch&lt;/code&gt; fusionne les nouvelles clés dans le secret existant. Exécuter &lt;code&gt;vault kv patch secret/project1 GOTRUE_MAILER_AUTOCONFIRM=true&lt;/code&gt; ajoute la clé en conservant tout le reste intact.&lt;/p&gt;

&lt;p&gt;J'avais besoin d'ajouter &lt;code&gt;GOTRUE_MAILER_AUTOCONFIRM=true&lt;/code&gt; pour activer la confirmation automatique des e-mails dans le cadre d'un test de charge. J'ai utilisé &lt;code&gt;vault kv put&lt;/code&gt;. Toutes les autres clés ont été effacées de la version courante.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
⚠️ &lt;strong&gt;Utilisez &lt;code&gt;vault kv patch&lt;/code&gt; pour ajouter ou mettre à jour des clés individuelles.&lt;/strong&gt; Réservez &lt;code&gt;vault kv put&lt;/code&gt; aux situations où vous souhaitez intentionnellement remplacer l'intégralité du secret.&lt;br&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Récupération depuis l'historique des versions
&lt;/h2&gt;

&lt;p&gt;KV v2 conserve les versions antérieures d'un secret (jusqu'à 10 par défaut, configurable). Listez les versions :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv metadata get secret/project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lisez une version spécifique :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv get &lt;span class="nt"&gt;-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 secret/project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;J'ai retrouvé tous mes secrets d'origine en version 7, puis j'ai utilisé &lt;code&gt;vault kv patch&lt;/code&gt; pour les restaurer dans la version courante. Vingt minutes de stress, mais entièrement récupérables.&lt;/p&gt;

&lt;p&gt;Cet historique de versions n'est pas un simple bonus — c'est la raison d'utiliser KV v2 plutôt que KV v1.&lt;/p&gt;




&lt;h2&gt;
  
  
  Récupérer les secrets pour le déploiement
&lt;/h2&gt;

&lt;p&gt;Nous utilisons un script qui lit depuis Vault et écrit un fichier &lt;code&gt;.env&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200
&lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /root/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-token&lt;/span&gt;.txt&lt;span class="si"&gt;)&lt;/span&gt;

vault kv get &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json secret/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.data | to_entries[] | "\(.key)=\(.value)"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/supabase-vps-cluster/instances/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La séquence de déploiement devient :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/fetch-env-from-vault.sh project1
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;instances/project1/.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; instances/project1/docker-compose.yml project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Jetons par projet
&lt;/h2&gt;

&lt;p&gt;Le jeton racine de Vault donne un accès illimité à tout — hors de question de l'utiliser pour les déploiements. À la place, nous créons une politique d'accès qui accorde un accès en lecture seule aux secrets d'un seul projet :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vault-policy-project1.hcl&lt;/span&gt;
&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"secret/data/project1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"secret/metadata/project1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write project1-readonly vault-policy-project1.hcl
vault token create &lt;span class="nt"&gt;-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project1-readonly &lt;span class="nt"&gt;-ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8760h &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auth.client_token'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/project1-token.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le script de déploiement lit depuis &lt;code&gt;/root/project1-token.txt&lt;/code&gt;. Si vous utilisez GitHub Actions pour automatiser les déploiements, stockez ce jeton comme secret de dépôt (&lt;code&gt;VAULT_TOKEN_PROJECT1&lt;/code&gt;) et transmettez-le au script de récupération. Il peut lire les secrets de project1 et rien d'autre.&lt;/p&gt;




&lt;h2&gt;
  
  
  Une limitation : les redémarrages
&lt;/h2&gt;

&lt;p&gt;Quand le VPS redémarre, Vault se retrouve dans un état scellé. Toutes les requêtes sont refusées jusqu'à ce que vous le déverrouilliez manuellement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_VPS_IP
&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200 &lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;vault operator unseal
&lt;span class="c"&gt;# saisissez votre clé de déverrouillage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les conteneurs en cours d'exécution conservent leurs variables d'environnement et continuent de fonctionner. Mais tout nouveau déploiement échouera à récupérer les secrets tant que Vault n'est pas déverrouillé.&lt;/p&gt;

&lt;p&gt;Le déverrouillage automatique nécessite soit un service HSM hébergé dans le cloud, soit de stocker la clé de déverrouillage sur disque, ce qui en annule l'intérêt. Le déverrouillage manuel après les redémarrages est le bon choix pour un nœud unique en usage personnel. Les serveurs Hetzner ne redémarrent pas sans que vous le demandiez.&lt;/p&gt;

&lt;p&gt;Faites une sauvegarde de votre clé de déverrouillage — une fois perdue, elle est irrécupérable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/partie-6-deux-instances-ml4" class="crayons-btn crayons-btn--primary"&gt;Partie 6 — Deux instances →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pourquoi auto-héberger&lt;/li&gt;
&lt;li&gt;Le serveur&lt;/li&gt;
&lt;li&gt;Traefik et SSL&lt;/li&gt;
&lt;li&gt;La première instance Supabase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vault&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;li&gt;Deux instances&lt;/li&gt;
&lt;li&gt;Sécurité et test de charge&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Partie 4 — La première instance Supabase</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:13:05 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-4-la-premiere-instance-supabase-3205</link>
      <guid>https://dev.to/voieducode/partie-4-la-premiere-instance-supabase-3205</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 4 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-4-the-first-supabase-instance-1e7f"&gt;Part 4 — The first Supabase instance&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Le serveur est prêt, Docker Swarm tourne, Traefik est en place — on passe au déploiement de Supabase, la partie la plus riche en surprises. Je les documente au fil du récit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce qu'est un projet Supabase
&lt;/h2&gt;

&lt;p&gt;Avant d'écrire la moindre configuration, il est utile d'avoir une image claire de ce qu'on déploie. Un projet Supabase, c'est huit services Docker :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
    |
  Traefik (terminaison TLS, routage)
    |
  Kong (API gateway, port 8000)
    |
    +-- GoTrue   (auth, port 9999)
    +-- PostgREST (REST API, port 3000)
    +-- Realtime  (WebSockets, port 4000)
    +-- Storage   (files, port 5000)
    +-- Studio   (dashboard, port 3000)
    +-- postgres-meta (schema introspection, port 8080)

  PostgreSQL (port 5432, internal only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Kong et Studio sont les seuls services accessibles depuis internet (via Traefik). Studio est protégé par une authentification basique, comme nous le verrons ci-dessous. Tous les autres services résident sur un réseau overlay Docker interne. PostgreSQL n'est jamais publié sur l'hôte.&lt;/p&gt;


&lt;h2&gt;
  
  
  Les secrets à générer
&lt;/h2&gt;

&lt;p&gt;Avant de rédiger le fichier compose, on génère ces valeurs :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Postgres password&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16

&lt;span class="c"&gt;# JWT secret (must be at least 32 characters)&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32

&lt;span class="c"&gt;# For Studio's schema browser&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16   &lt;span class="c"&gt;# PG_META_CRYPTO_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Les clés &lt;code&gt;anon&lt;/code&gt; et &lt;code&gt;service_role&lt;/code&gt; sont des JWT standard signés avec votre secret JWT. Ce script les génère :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-jwt-secret-here"&lt;/span&gt;

&lt;span class="c"&gt;# Expiry: year 2035 (Unix timestamp)&lt;/span&gt;
&lt;span class="nv"&gt;EXPIRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2051222400

python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
import json, hmac, hashlib, base64

secret = "&lt;/span&gt;&lt;span class="nv"&gt;$JWT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"

def b64(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def enc(obj):
    return b64(json.dumps(obj, separators=(',', ':')).encode())

header = enc({"alg":"HS256","typ":"JWT"})

for role in ["anon", "service_role"]:
    payload = enc({
        "role": role,
        "iss": "supabase",
        "iat": 1772393548,
        "exp": &lt;/span&gt;&lt;span class="nv"&gt;$EXPIRY&lt;/span&gt;&lt;span class="sh"&gt;
    })
    msg = f"{header}.{payload}".encode()
    sig = b64(hmac.new(secret.encode(), msg, hashlib.sha256).digest())
    print(f"{role}: {header}.{payload}.{sig}")
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Le JWT &lt;code&gt;anon&lt;/code&gt; peut être exposé aux navigateurs sans risque. Le JWT &lt;code&gt;service_role&lt;/code&gt; contourne la sécurité au niveau des lignes (RLS) et doit rester secret.&lt;/p&gt;

&lt;p&gt;On stockera tout cela dans Vault à la partie 5. Pour l'instant, notez ces valeurs dans un endroit sûr.&lt;/p&gt;


&lt;h2&gt;
  
  
  Le docker-compose.yml
&lt;/h2&gt;

&lt;p&gt;Voici la définition complète du stack. Je détaille chaque point inattendu dans les sections qui suivent.&lt;/p&gt;

&lt;p&gt;Les tags d'images ci-dessous correspondent aux versions majeures actuelles. Pour les versions exactement épinglées de chaque composant, consultez la référence officielle d'auto-hébergement Supabase sur &lt;code&gt;supabase.com/docs/guides/self-hosting&lt;/code&gt;. Supabase y maintient une combinaison de versions testée et stable.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;overlay&lt;/span&gt;
  &lt;span class="na"&gt;traefik_default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storage_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/postgres:15&lt;/span&gt;          &lt;span class="c1"&gt;# use latest 15.x from supabase.com/docs/guides/self-hosting&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1g&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0'&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;

  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/gotrue:latest&lt;/span&gt;         &lt;span class="c1"&gt;# pin to a stable release tag; see note below&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_DB_DRIVER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_DB_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_EXP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3600'&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_AUD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authenticated&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_DEFAULT_GROUP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authenticated&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_ADMIN_ROLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_role&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_API_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_API_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9999'&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_SITE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SITE_URL}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOTRUE_EXTERNAL_URL}&lt;/span&gt;
      &lt;span class="na"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${API_EXTERNAL_URL}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOTRUE_MAILER_AUTOCONFIRM:-false}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_SMS_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;rest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/postgrest:v12&lt;/span&gt; &lt;span class="c1"&gt;# use latest stable v12&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_URI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public,storage,graphql_public&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_ANON_ROLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_USE_LEGACY_GUCS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;realtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/realtime:v2&lt;/span&gt;   &lt;span class="c1"&gt;# use latest stable v2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_ENC_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;DB_AFTER_CONNECT_QUERY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SET search_path TO _realtime&lt;/span&gt;
      &lt;span class="na"&gt;API_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SECRET_KEY_BASE}&lt;/span&gt;
      &lt;span class="na"&gt;APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime&lt;/span&gt;
      &lt;span class="na"&gt;FLY_APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime&lt;/span&gt;
      &lt;span class="na"&gt;FLY_ALLOC_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project1-realtime&lt;/span&gt;
      &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4000'&lt;/span&gt;
      &lt;span class="na"&gt;SEED_SELF_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
      &lt;span class="na"&gt;RUN_JANITOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
      &lt;span class="na"&gt;ENABLE_TAILSCALE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
      &lt;span class="na"&gt;DNS_NODES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;ERL_AFLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-proto_dist inet_tcp&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/storage-api:v1&lt;/span&gt; &lt;span class="c1"&gt;# use latest stable v1&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ANON_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_ANON_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;SERVICE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_SERVICE_ROLE_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres&lt;/span&gt;
      &lt;span class="na"&gt;FILE_STORAGE_BACKEND_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/storage&lt;/span&gt;
      &lt;span class="na"&gt;STORAGE_BACKEND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
      &lt;span class="na"&gt;FILE_SIZE_LIMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;52428800'&lt;/span&gt;
      &lt;span class="na"&gt;GLOBAL_S3_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stub&lt;/span&gt;
      &lt;span class="na"&gt;REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stub&lt;/span&gt;
      &lt;span class="na"&gt;TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stub&lt;/span&gt;
      &lt;span class="na"&gt;POSTGREST_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://rest:3000&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;DB_INSTALL_ROLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;storage_data:/var/lib/storage&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;kong&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/kong:2.8.1&lt;/span&gt;   &lt;span class="c1"&gt;# Kong version; only change if Supabase releases a new one&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;KONG_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;off'&lt;/span&gt;
      &lt;span class="na"&gt;KONG_DECLARATIVE_CONFIG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/kong/kong.yml&lt;/span&gt;
      &lt;span class="na"&gt;KONG_LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
      &lt;span class="na"&gt;KONG_PROXY_ACCESS_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stdout&lt;/span&gt;
      &lt;span class="na"&gt;KONG_PROXY_ERROR_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stderr&lt;/span&gt;
      &lt;span class="na"&gt;KONG_ADMIN_ACCESS_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stdout&lt;/span&gt;
      &lt;span class="na"&gt;KONG_ADMIN_ERROR_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stderr&lt;/span&gt;
      &lt;span class="na"&gt;KONG_SERVER_TOKENS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;off'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/root/supabase-vps-cluster/instances/project1/kong.yml:/var/lib/kong/kong.yml:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project1.yourdomain.com`)&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.tls.certresolver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;le&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.services.p1-kong.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8000'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.swarm.network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;

  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/postgres-meta:v0&lt;/span&gt;      &lt;span class="c1"&gt;# use latest stable v0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase_admin&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_SSL_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disable&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PG_META_CRYPTO_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.25'&lt;/span&gt;

  &lt;span class="na"&gt;studio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/studio:latest&lt;/span&gt;        &lt;span class="c1"&gt;# always use the latest Studio tag&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://kong:8000&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_PUBLIC_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${API_EXTERNAL_URL}&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_ANON_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_SERVICE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_SERVICE_ROLE_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;AUTH_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;STUDIO_PG_META_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://meta:8080&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_ORGANIZATION_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Default Organization&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_PROJECT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Default Project&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`studio.project1.yourdomain.com`)&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.tls.certresolver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;le&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.services.p1-studio.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.swarm.network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm,p1-studio-auth@swarm&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.middlewares.p1-studio-auth.basicauth.users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_HASHED_CREDENTIALS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Remplacez &lt;code&gt;YOUR_HASHED_CREDENTIALS&lt;/code&gt; par un hash bcrypt de votre mot de passe. Installez l'outil et générez le hash directement sur le serveur :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apache2-utils &lt;span class="nt"&gt;-y&lt;/span&gt;
htpasswd &lt;span class="nt"&gt;-nB&lt;/span&gt; admin
&lt;span class="c"&gt;# New password:&lt;/span&gt;
&lt;span class="c"&gt;# Re-type new password:&lt;/span&gt;
&lt;span class="c"&gt;# admin:$2y$05$...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Copiez le résultat, nom d'utilisateur inclus. Dans les labels Docker Compose, chaque &lt;code&gt;$&lt;/code&gt; doit être doublé, car Compose utilise &lt;code&gt;$&lt;/code&gt; pour l'interpolation de variables. La chaîne &lt;code&gt;admin:$2y$05$...&lt;/code&gt; devient donc &lt;code&gt;admin:$$2y$$05$$...&lt;/code&gt; dans le label.&lt;/p&gt;


&lt;h2&gt;
  
  
  kong.yml : la configuration de l'API gateway
&lt;/h2&gt;

&lt;p&gt;Le fichier compose monte en bind &lt;code&gt;/root/supabase-vps-cluster/instances/project1/kong.yml&lt;/code&gt; dans le conteneur Kong. C'est dans ce fichier que l'on définit les routes, l'authentification et la limitation de débit. Il n'est pas versionné dans git, car il contient vos clés API.&lt;/p&gt;

&lt;p&gt;Créez-le à cet emplacement sur le serveur :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;_format_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.1'&lt;/span&gt;
&lt;span class="na"&gt;_transform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;consumers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
    &lt;span class="na"&gt;keyauth_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_SUPABASE_ANON_KEY&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_role&lt;/span&gt;
    &lt;span class="na"&gt;keyauth_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;

&lt;span class="na"&gt;acls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_role&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/verify&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/verify&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open-callback&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/callback&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open-callback&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/callback&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-all&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate-limiting&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
          &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
          &lt;span class="na"&gt;limit_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ip&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rest-v1&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://rest:3000/&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rest-v1-all&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/rest/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime-v1-ws&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://realtime:4000/socket&lt;/span&gt;
    &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ws&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime-v1-ws&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/realtime/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage-v1&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://storage:5000/&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage-v1-all&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/storage/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Quelques points à noter. Les routes &lt;code&gt;auth-v1-open&lt;/code&gt; (&lt;code&gt;/verify&lt;/code&gt;, &lt;code&gt;/callback&lt;/code&gt;) sont intentionnellement laissées sans key-auth : ce sont les endpoints de redirection OAuth que les navigateurs appellent directement lors des flux de connexion et qui ne peuvent pas inclure un en-tête de clé API. Tout le reste exige une clé valide.&lt;/p&gt;

&lt;p&gt;Les permissions du fichier ont leur importance : &lt;code&gt;chmod 644 kong.yml&lt;/code&gt;. Kong tourne en tant qu'utilisateur non-root et échoue avec une erreur de permission si le fichier est en &lt;code&gt;600&lt;/code&gt; ou &lt;code&gt;700&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Après toute modification de ce fichier, Kong ne la prend pas en compte automatiquement. Un redémarrage forcé s'impose :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service update &lt;span class="nt"&gt;--force&lt;/span&gt; project1_kong
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Point inattendu 1 : les limites mémoire ne sont pas optionnelles
&lt;/h2&gt;

&lt;p&gt;Sans limites mémoire, les services se disputent la RAM sur un serveur de 4 Go et peuvent déclencher des arrêts OOM (Out of Memory : le noyau Linux interrompt le processus le plus gourmand quand la mémoire est épuisée) qui emportent d'autres conteneurs. Des limites strictes sont indispensables.&lt;/p&gt;

&lt;p&gt;Les valeurs auxquelles je suis arrivé après ajustement :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Limite mémoire&lt;/th&gt;
&lt;th&gt;Raison&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;db&lt;/td&gt;
&lt;td&gt;1 Go&lt;/td&gt;
&lt;td&gt;Cache de buffers Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kong&lt;/td&gt;
&lt;td&gt;512 Mo&lt;/td&gt;
&lt;td&gt;Plus que prévu, Kong met la config en cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;realtime&lt;/td&gt;
&lt;td&gt;512 Mo&lt;/td&gt;
&lt;td&gt;La VM Erlang/BEAM consomme ~200 Mo au repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;studio&lt;/td&gt;
&lt;td&gt;512 Mo&lt;/td&gt;
&lt;td&gt;Rendu côté serveur Next.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auth&lt;/td&gt;
&lt;td&gt;256 Mo&lt;/td&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rest&lt;/td&gt;
&lt;td&gt;256 Mo&lt;/td&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;256 Mo&lt;/td&gt;
&lt;td&gt;API Storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;meta&lt;/td&gt;
&lt;td&gt;256 Mo&lt;/td&gt;
&lt;td&gt;postgres-meta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Realtime a été la vraie surprise. La machine virtuelle BEAM (le runtime Erlang sur lequel Realtime est construit) a une empreinte mémoire de base élevée, autour de 200 Mo avant qu'aucune connexion ne soit établie. J'avais initialement fixé la limite à 256 Mo, ce qui paraissait généreux, et le service continuait d'atteindre cette limite. 512 Mo est la valeur correcte — c'est d'ailleurs ce que Supabase Cloud alloue, pour la même raison.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point inattendu 2 : Studio a besoin de trois variables non évidentes
&lt;/h2&gt;

&lt;p&gt;Studio est une application Next.js. Le rendu côté serveur s'exécute dans le conteneur ; le rendu côté client s'exécute dans le navigateur. Ces deux contextes ont besoin d'URL différentes :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SUPABASE_URL: http://kong:8000&lt;/code&gt; : pour le code côté serveur qui s'exécute dans Docker, il atteint Kong par nom de conteneur sur le réseau interne.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SUPABASE_PUBLIC_URL&lt;/code&gt; : l'URL HTTPS publique, pour le code côté navigateur.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; : Studio effectue des connexions Postgres directes pour son éditeur de requêtes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si l'une de ces variables est absente, Studio produit des erreurs 400/500 déconcertantes dans la console du navigateur, sans indication claire de la cause. J'ai dû lire le code source de Studio pour comprendre — rien dans les logs ne mettait sur la piste.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point inattendu 3 : la vérification d'état de Studio le tue
&lt;/h2&gt;

&lt;p&gt;L'image &lt;code&gt;supabase/studio&lt;/code&gt; embarque une vérification d'état Docker intégrée. Dans Swarm, un conteneur qui échoue à sa vérification d'état est tué et redémarré — et celle de Studio échouait systématiquement dans notre configuration.&lt;/p&gt;

&lt;p&gt;La solution : la désactiver.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Même problème avec &lt;code&gt;postgres-meta&lt;/code&gt;. Il embarque lui aussi une vérification d'état intégrée qui provoque un exit 137 (SIGKILL) dans Swarm — à désactiver également.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point inattendu 4 : on ne peut pas figer GOTRUE_MAILER_AUTOCONFIRM en dur
&lt;/h2&gt;

&lt;p&gt;Pour le développement et les tests de charge, on veut que l'inscription par e-mail soit confirmée automatiquement (sans e-mail de vérification). J'avais initialement défini cette valeur en dur dans le fichier compose :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Puis j'ai eu besoin de passer à &lt;code&gt;true&lt;/code&gt; : mise à jour du &lt;code&gt;.env&lt;/code&gt;, redéploiement — rien n'a changé. Le service continuait de lire &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Le problème vient du fait qu'une valeur fixée en dur dans le bloc &lt;code&gt;environment:&lt;/code&gt; est prioritaire sur une variable provenant du fichier &lt;code&gt;.env&lt;/code&gt;. La variable du &lt;code&gt;.env&lt;/code&gt; était tout simplement ignorée.&lt;/p&gt;

&lt;p&gt;La correction : utiliser la substitution de variable.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOTRUE_MAILER_AUTOCONFIRM:-false}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;La partie &lt;code&gt;:-false&lt;/code&gt; signifie "utiliser cette valeur si la variable n'est pas définie". Désormais, c'est le fichier &lt;code&gt;.env&lt;/code&gt; qui contrôle la valeur — comme ça aurait dû l'être dès le départ.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point inattendu 5 : DB_ENC_KEY doit faire exactement 16 octets
&lt;/h2&gt;

&lt;p&gt;Realtime utilise le chiffrement AES-128. AES-128 exige une clé de 16 octets. J'ai généré une clé avec &lt;code&gt;openssl rand -hex 32&lt;/code&gt;, ce qui donne 32 caractères hexadécimaux. Or 32 caractères hex représentent 16 octets, à raison de 2 caractères par octet. Ça devrait fonctionner, non ?&lt;/p&gt;

&lt;p&gt;Non. Realtime passe la chaîne de clé directement comme valeur de clé, sans la traiter comme un tableau d'octets encodé en hexadécimal. La commande &lt;code&gt;openssl rand -hex 32&lt;/code&gt; produit une chaîne de 32 caractères, qui est interprétée comme 32 octets. AES-128 n'en accepte que 16. Le service plante avec l'erreur "Bad key size".&lt;/p&gt;

&lt;p&gt;La valeur par défaut officielle pour Realtime en auto-hébergement est la chaîne littérale &lt;code&gt;supabaserealtime&lt;/code&gt;. Elle fait exactement 16 caractères, donc 16 octets. Utilisez cette valeur. N'essayez pas d'être créatif avec la génération de clé ici.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point inattendu 6 : le schéma _realtime
&lt;/h2&gt;

&lt;p&gt;Le dépôt Docker Compose officiel de Supabase inclut un fichier &lt;code&gt;docker/volumes/db/realtime.sql&lt;/code&gt; monté dans le conteneur Postgres, qui crée le schéma &lt;code&gt;_realtime&lt;/code&gt; automatiquement au premier démarrage. Si vous clonez le dépôt officiel, c'est pris en charge.&lt;/p&gt;

&lt;p&gt;Nous faisons la même chose : un petit fichier SQL monté dans &lt;code&gt;docker-entrypoint-initdb.d/&lt;/code&gt; gère la création du schéma lors des déploiements neufs. Mais créer le schéma ne suffit pas. Realtime v2.76+ a aussi besoin de migrations de base de données et d'un enregistrement de tenant initialisé. Le script &lt;code&gt;init-realtime.sh&lt;/code&gt; s'occupe de tout :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/init-realtime.sh project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Ce que fait le script : il crée le schéma &lt;code&gt;_realtime&lt;/code&gt; s'il est absent, exécute les migrations de base de données de Realtime, force le redémarrage du service pour que &lt;code&gt;SEED_SELF_HOST&lt;/code&gt; crée l'enregistrement de tenant, et vérifie que le tenant a bien été initialisé. On peut le relancer sans risque.&lt;/p&gt;


&lt;h2&gt;
  
  
  Point inattendu 7 : API_EXTERNAL_URL doit pointer vers Kong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;API_EXTERNAL_URL&lt;/code&gt; détermine les URL que GoTrue insère dans les e-mails (réinitialisations de mot de passe, confirmations) ainsi que l'URL publique qu'utilise Studio pour les appels API côté navigateur.&lt;/p&gt;

&lt;p&gt;J'avais pointé cette variable vers PostgREST, puisque PostgREST est l'API REST. logique en apparence. Sauf que PostgREST est un service interne. Kong est la passerelle qui expose tout, gère l'authentification et applique la limitation de débit. L'URL externe doit être l'adresse publique de Kong :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pointer vers PostgREST contourne Kong entièrement, ce qui casse l'authentification.&lt;/p&gt;


&lt;h2&gt;
  
  
  À propos des tags d'image GoTrue
&lt;/h2&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
⚠️ &lt;strong&gt;Évitez les release candidates GoTrue.&lt;/strong&gt; &lt;code&gt;:latest&lt;/code&gt; peut tirer une RC. Les RC de GoTrue ont eu des bugs d'ordonnancement de migration en base de données qui font échouer le service au démarrage. Si GoTrue refuse de démarrer et que les logs mentionnent des migrations, consultez la &lt;a href="https://github.com/supabase/gotrue/releases" rel="noopener noreferrer"&gt;page des releases GoTrue&lt;/a&gt; et épinglez le dernier tag stable.&lt;br&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Déploiement
&lt;/h2&gt;

&lt;p&gt;On crée le fichier &lt;code&gt;.env&lt;/code&gt; (on le migrera dans Vault à la partie 5). D'abord, les deux secrets restants à générer — ce sont des commandes shell, pas des valeurs &lt;code&gt;.env&lt;/code&gt; littérales :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64   &lt;span class="c"&gt;# copy this as SECRET_KEY_BASE&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16   &lt;span class="c"&gt;# copy this as PG_META_CRYPTO_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensuite, on crée le fichier avec les vraies valeurs :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# instances/project1/.env&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generated above&amp;gt;
&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generated above&amp;gt;
&lt;span class="nv"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;anon jwt from the script&amp;gt;
&lt;span class="nv"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;service_role jwt from the script&amp;gt;
&lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;span class="nv"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;span class="nv"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="nv"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;supabaserealtime
&lt;span class="nv"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;paste &lt;/span&gt;the 128-char hex string&amp;gt;
&lt;span class="nv"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;paste &lt;/span&gt;the 32-char hex string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Déploiement :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;instances/project1/.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; instances/project1/docker-compose.yml project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vérification que tous les services démarrent :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les huit services doivent afficher &lt;code&gt;1/1&lt;/code&gt; replica en l'espace d'une à deux minutes. Si l'un affiche &lt;code&gt;0/1&lt;/code&gt;, consultez les logs :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service logs &lt;span class="nt"&gt;--tail&lt;/span&gt; 50 project1_auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialisation de Realtime :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/init-realtime.sh project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test de l'endpoint API :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://kong.project1.yourdomain.com/health
&lt;span class="c"&gt;# {"status":"healthy"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Bilan
&lt;/h2&gt;

&lt;p&gt;Une instance Supabase fonctionnelle : Postgres, authentification, API REST, abonnements temps réel, stockage de fichiers, et un tableau de bord protégé par une authentification de base.&lt;/p&gt;

&lt;p&gt;Dans le prochain article, on migre tous ces secrets hors des fichiers plats et dans Vault. Et je vous raconte l'après-midi où j'ai accidentellement tout supprimé.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/partie-5-vault-et-lapres-midi-ou-jai-tout-efface-1df9" class="crayons-btn crayons-btn--primary"&gt;Partie 5 — Vault →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pourquoi auto-héberger&lt;/li&gt;
&lt;li&gt;Le serveur&lt;/li&gt;
&lt;li&gt;Traefik et SSL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La première instance Supabase&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Deux instances&lt;/li&gt;
&lt;li&gt;Sécurité et test de charge&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Partie 3 — Traefik et HTTPS</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:07:14 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-3-traefik-et-https-4b5f</link>
      <guid>https://dev.to/voieducode/partie-3-traefik-et-https-4b5f</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 3 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-3-traefik-and-ssl-3b29"&gt;Part 3 — Traefik and SSL&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nous avons besoin d'un proxy inverse. Il se place devant tous nos conteneurs, effectue la terminaison TLS et achemine les requêtes entrantes vers le bon service en fonction du nom d'hôte. &lt;a href="https://doc.traefik.io/traefik/" rel="noopener noreferrer"&gt;Traefik&lt;/a&gt; est le choix naturel pour les environnements Docker : il lit les labels des conteneurs et se configure automatiquement.&lt;/p&gt;

&lt;p&gt;Ajoutez un label à un conteneur pour indiquer "je veux une route sur ce domaine" et Traefik la crée — élégant, mais un problème de compatibilité m'a rattrapé dès le départ et m'a coûté un bon moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le problème de version
&lt;/h2&gt;

&lt;p&gt;Les versions récentes de Docker Engine ont modifié la façon dont l'API négocie les versions avec les clients. Traefik v2, la version que l'on trouve dans la plupart des tutoriels, ne gère pas cela correctement. Il se connecte au daemon Docker, semble démarrer sans problème, mais échoue silencieusement à détecter les services.&lt;/p&gt;

&lt;p&gt;Vos conteneurs tournent. Traefik tourne. Rien n'est routé. Aucun message d'erreur ne pointe clairement vers la cause.&lt;/p&gt;

&lt;p&gt;La solution : passer à Traefik v3, la version majeure actuelle, qui gère correctement la négociation de version.&lt;/p&gt;

&lt;p&gt;Si vous cherchez "Traefik Supabase Docker", la majorité des résultats montre encore une configuration Traefik v2. Gardez ça à l'esprit avant de copier quoi que ce soit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration DNS
&lt;/h2&gt;

&lt;p&gt;Avant de configurer Traefik, vous devez créer des enregistrements DNS pointant vers votre serveur. Chez votre fournisseur DNS, créez des enregistrements A pour chaque sous-domaine que vous comptez utiliser :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kong.project1.yourdomain.com    A    YOUR_VPS_IP
studio.project1.yourdomain.com  A    YOUR_VPS_IP
kong.project2.yourdomain.com    A    YOUR_VPS_IP
studio.project2.yourdomain.com  A    YOUR_VPS_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's Encrypt vérifie ces enregistrements lors de l'émission des certificats. La propagation DNS prend généralement quelques minutes, mais peut aller jusqu'à quelques heures selon votre fournisseur. Avant de déployer, vérifiez que la propagation est complète. Si &lt;code&gt;dig&lt;/code&gt; est disponible (&lt;code&gt;apt install dnsutils -y&lt;/code&gt;), exécutez &lt;code&gt;dig kong.project1.yourdomain.com +short&lt;/code&gt; et confirmez que votre IP VPS est bien retournée. Sinon, attendez et recommencez.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le stack Traefik
&lt;/h2&gt;

&lt;p&gt;Créez &lt;code&gt;traefik/docker-compose.yml&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik_default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;overlay&lt;/span&gt;
    &lt;span class="na"&gt;attachable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik:v3&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.swarm.network=traefik_default"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.swarm.exposedByDefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.email=your@email.com"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.le.acme.tlschallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--log.level=INFO"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--api.dashboard=false"&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;letsencrypt:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;constraints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node.role == manager&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;letsencrypt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quelques explications s'imposent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;exposedByDefault=false&lt;/code&gt; signifie que Traefik ignore tout conteneur qui ne s'inscrit pas explicitement avec &lt;code&gt;traefik.enable=true&lt;/code&gt;. Sans ça, chaque conteneur sur le réseau Traefik serait accessible publiquement — autant dire une porte ouverte.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;api.dashboard=false&lt;/code&gt; désactive l'interface web de Traefik. Le tableau de bord expose l'intégralité de votre configuration de routage : tous les noms de services, tous les domaines, tous les middlewares. Aucune raison d'exposer ça.&lt;/p&gt;

&lt;p&gt;Le volume &lt;code&gt;letsencrypt&lt;/code&gt; stocke les certificats. Ne le supprimez pas entre les déploiements. Let's Encrypt applique une limite de demandes de certificats par domaine et par semaine. Si vous videz le volume et redéployez à répétition, vous finirez par atteindre cette limite et vous ne pourrez plus obtenir de nouveau certificat pendant plusieurs jours.&lt;/p&gt;

&lt;p&gt;Le trafic HTTP (port 80) est redirigé automatiquement vers HTTPS via les règles d'entrypoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Un problème de placement de labels
&lt;/h2&gt;

&lt;p&gt;Dans un Docker Compose classique, les labels de service se placent au niveau du service :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avec &lt;code&gt;docker stack deploy&lt;/code&gt; en Docker Swarm, ils se placent dans le bloc &lt;code&gt;deploy&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;myapp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les labels placés au mauvais niveau sont ignorés silencieusement. Traefik ne routera pas le service et ne donnera aucune indication sur la raison. J'ai découvert ça à force de scruter une configuration d'apparence correcte, avant de finir par trouver l'erreur.&lt;/p&gt;




&lt;h2&gt;
  
  
  En-têtes de sécurité
&lt;/h2&gt;

&lt;p&gt;Traefik propose un système de middlewares : des composants réutilisables qui traitent les requêtes avant qu'elles n'atteignent un service. Nous définissons un middleware &lt;code&gt;security-headers&lt;/code&gt; qui ajoute HSTS, supprime l'en-tête Server, définit une politique de type de contenu, et ainsi de suite.&lt;/p&gt;

&lt;p&gt;Dans Swarm, un middleware défini dans un stack est accessible aux autres stacks via le suffixe &lt;code&gt;@swarm&lt;/code&gt;. Nous définissons le middleware des en-têtes dans les labels du stack project1 (présentés dans le billet suivant), et project2 y fait référence sous le nom &lt;code&gt;security-headers@swarm&lt;/code&gt;. Traefik le détecte automatiquement dès que project1 est déployé.&lt;/p&gt;

&lt;p&gt;Le middleware des en-têtes ressemble à ceci dans les labels du service :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.stsSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;63072000'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.stsIncludeSubdomains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.stsPreload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.forceSTSHeader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.contentTypeNosniff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.referrerPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;strict-origin-when-cross-origin&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.customFrameOptionsValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SAMEORIGIN&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.security-headers.headers.customResponseHeaders.Server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cette dernière ligne fixe l'en-tête de réponse Server à une chaîne vide. Par défaut, Kong annonce sa version dans chaque réponse — aucune raison de lui laisser ce plaisir.&lt;/p&gt;

&lt;p&gt;Une remarque sur la dépendance inter-stacks : comme le middleware &lt;code&gt;security-headers&lt;/code&gt; est défini dans le stack de project1, il disparaît si project1 est complètement arrêté — project2 se retrouverait alors sans middleware d'en-têtes. Pour un setup d'apprentissage, c'est acceptable ; la solution plus propre consiste à définir les middlewares partagés directement dans le stack Traefik.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comment Kong obtient sa route
&lt;/h2&gt;

&lt;p&gt;Seuls deux services de notre stack Supabase ont besoin d'une route publique : Kong (la passerelle API) et Studio (le tableau de bord). Tout le reste est interne.&lt;/p&gt;

&lt;p&gt;Les labels du service Kong dans notre fichier Compose ressemblent à ceci :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kong&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project1.yourdomain.com`)&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.tls.certresolver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;le&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.services.p1-kong.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8000'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.swarm.network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quelques subtilités à noter.&lt;/p&gt;

&lt;p&gt;Le nom du routeur est &lt;code&gt;p1-kong&lt;/code&gt;. Chaque nom de routeur doit être unique dans l'ensemble des stacks qui tournent dans Swarm. Si deux services enregistrent un routeur nommé &lt;code&gt;kong&lt;/code&gt;, Traefik en choisit un et ignore l'autre sans avertissement. Nous préfixons avec l'identifiant du projet. C'est important dans le billet 6, quand nous ajoutons la deuxième instance.&lt;/p&gt;

&lt;p&gt;Le label &lt;code&gt;traefik.swarm.network&lt;/code&gt; indique à Traefik quel réseau utiliser pour acheminer les requêtes. Kong est connecté à deux réseaux : le réseau Supabase interne et le réseau Traefik. Sans ce label, Traefik pourrait essayer de passer par le réseau interne, qu'il ne peut pas atteindre.&lt;/p&gt;

&lt;p&gt;Le &lt;code&gt;loadbalancer.server.port&lt;/code&gt; indique à Traefik vers quel port transférer les requêtes à l'intérieur du conteneur. Comme nous ne publions pas le port de Kong vers l'hôte, Traefik a besoin de connaître directement le port du conteneur.&lt;/p&gt;




&lt;h2&gt;
  
  
  Déployer Traefik
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; traefik/docker-compose.yml traefik
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vérifiez que le service a démarré :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;span class="c"&gt;# NAME              MODE         REPLICAS&lt;/span&gt;
&lt;span class="c"&gt;# traefik_traefik   replicated   1/1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Vérifier SSL plus tard
&lt;/h2&gt;

&lt;p&gt;Après avoir déployé un projet Supabase dans le billet suivant, vérifiez que le certificat a bien été émis :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://kong.project1.yourdomain.com/health
&lt;span class="c"&gt;# HTTP/2 200&lt;/span&gt;
&lt;span class="c"&gt;# strict-transport-security: max-age=63072000; includeSubDomains; preload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et que l'en-tête Server a disparu de la réponse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives à Traefik
&lt;/h2&gt;

&lt;p&gt;Cette série utilise Traefik parce qu'il s'intègre naturellement à Docker Swarm via les labels de conteneurs. Supabase documente désormais officiellement &lt;a href="https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https" rel="noopener noreferrer"&gt;Caddy et nginx comme alternatives plus simples&lt;/a&gt; — à consulter si vous préférez une configuration moins orientée labels.&lt;/p&gt;

&lt;p&gt;Conformément au guide proxy officiel de Supabase, nous routons le trafic Storage directement vers le conteneur storage via Traefik, en contournant Kong. Kong ajoute une surcharge inutile pour les uploads et downloads de fichiers volumineux. Vous verrez les labels Traefik de Storage dans le fichier compose du prochain billet.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://dev.to/voieducode/partie-4-la-premiere-instance-supabase-3205" class="crayons-btn crayons-btn--primary"&gt;Partie 4 — La première instance Supabase →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pourquoi auto-héberger&lt;/li&gt;
&lt;li&gt;Le serveur&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traefik et SSL&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;li&gt;La première instance Supabase&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Deux instances&lt;/li&gt;
&lt;li&gt;Sécurité et test de charge&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Partie 2 — Le serveur</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:06:37 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-2-le-serveur-19nj</link>
      <guid>https://dev.to/voieducode/partie-2-le-serveur-19nj</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 2 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-2-the-server-1kd8"&gt;Part 2 — The server&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ce billet couvre la création du serveur, sa sécurisation et l'installation correcte de Docker. Rien de compliqué en soi, mais à quelques endroits le choix évident est le mauvais.&lt;/p&gt;




&lt;h2&gt;
  
  
  Créer le serveur
&lt;/h2&gt;

&lt;p&gt;Rendez-vous sur hetzner.com, créez un compte, puis allez dans Cloud &amp;gt; New Server.&lt;/p&gt;

&lt;p&gt;Choisissez Ubuntu 24.04 LTS sur une CX22 (2 vCPU, 4 Go de RAM), dans un datacenter européen si c'est un critère pour vous. Ajoutez votre clé SSH publique pendant la configuration — pas de mot de passe root.&lt;/p&gt;

&lt;p&gt;Cliquez sur Create. En une trentaine de secondes, le serveur est prêt. Notez l'adresse IP, que nous appellerons YOUR_VPS_IP tout au long de la série.&lt;/p&gt;




&lt;h2&gt;
  
  
  Première connexion
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_VPS_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Mettez le système à jour avant de faire quoi que ce soit d'autre :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Définissez un nom d'hôte :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hostnamectl set-hostname supabase-vps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pare-feu
&lt;/h2&gt;

&lt;p&gt;Ubuntu est livré avec ufw. Configurez-le avant de l'activer, et dans cet ordre précis :&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
⚠️ &lt;strong&gt;Autorisez SSH en premier.&lt;/strong&gt; Activer ufw avant d'ouvrir le port 22 vous coupe immédiatement l'accès. C'est le genre de chose qui paraît évidente — jusqu'au moment où on se fait piéger.&lt;br&gt;

&lt;/div&gt;



&lt;p&gt;Vérifiez le résultat :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seuls les ports 22, 80 et 443 doivent être ouverts.&lt;/p&gt;

&lt;p&gt;Un point important à connaître sur Docker et ufw : Docker écrit des règles iptables directement (iptables est le système de filtrage de paquets Linux sous-jacent que ufw encapsule), en contournant ufw entièrement. Conséquence : si vous publiez un port dans un fichier Docker Compose avec &lt;code&gt;ports:&lt;/code&gt;, ce port sera accessible depuis internet même si ufw ne l'autorise pas. C'est pourquoi nous n'utiliserons jamais &lt;code&gt;ports:&lt;/code&gt; pour le service de base de données. Le pare-feu est la porte extérieure ; les ports de base de données n'y ont pas leur place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Durcissement SSH
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configurez ces paramètres :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;PermitRootLogin&lt;/span&gt; prohibit-password
&lt;span class="k"&gt;PasswordAuthentication&lt;/span&gt; &lt;span class="no"&gt;no&lt;/span&gt;
&lt;span class="k"&gt;PubkeyAuthentication&lt;/span&gt; &lt;span class="no"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prohibit-password&lt;/code&gt; autorise la connexion root mais uniquement par clé, jamais par mot de passe. Comme nous avons ajouté notre clé à la création du serveur, c'est parfaitement adapté.&lt;/p&gt;

&lt;p&gt;Redémarrez SSH :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avant de fermer votre session actuelle, ouvrez une nouvelle fenêtre de terminal et vérifiez que vous pouvez encore vous connecter. Testez toujours les modifications SSH avec une session parallèle. En cas de blocage, Hetzner propose une console de secours dans l'interface web, mais il est bien plus confortable de ne pas en avoir besoin.&lt;/p&gt;




&lt;h2&gt;
  
  
  fail2ban
&lt;/h2&gt;

&lt;p&gt;fail2ban lit les journaux d'authentification et bannit les adresses IP qui échouent trop souvent. Les paramètres par défaut nous conviennent :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban &lt;span class="nt"&gt;-y&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;fail2ban
systemctl start fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aucune modification de fichier de configuration n'est nécessaire. Le jail SSH est actif par défaut.&lt;/p&gt;

&lt;p&gt;Avec la configuration par défaut, cinq tentatives SSH échouées depuis une même adresse IP déclenchent un bannissement de dix minutes. Vous pouvez augmenter cette durée dans &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt; si vous préférez des bannissements plus longs. Les scanners automatisés qui cherchent des mots de passe faibles sont stoppés net.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation de Docker
&lt;/h2&gt;

&lt;p&gt;C'est ici que le choix évident est le mauvais.&lt;/p&gt;

&lt;p&gt;Le gestionnaire de paquets d'Ubuntu propose un paquet Docker, mais il est obsolète. Quant au paquet Snap, il présente des problèmes connus de permissions de fichiers, de chemins de volumes et de comportement au redémarrage en production. N'utilisez ni l'un ni l'autre.&lt;/p&gt;

&lt;p&gt;Installez Docker depuis le dépôt apt officiel de Docker :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt remove docker docker-engine docker.io containerd runc 2&amp;gt;/dev/null

apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg lsb-release

&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg &lt;span class="se"&gt;\&lt;/span&gt;
  | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg
&lt;span class="nb"&gt;chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.gpg] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

apt update
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vérifiez l'installation :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Docker version 29.x.x, build ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le numéro de version compte : il faut un Docker Engine récent pour que Traefik v3 fonctionne correctement. J'ai découvert ce problème à mes dépens après y avoir perdu un après-midi entier — on en reparle dans le billet suivant.&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker Swarm
&lt;/h2&gt;

&lt;p&gt;Nous utilisons &lt;a href="https://docs.docker.com/engine/swarm/" rel="noopener noreferrer"&gt;Docker Swarm&lt;/a&gt; plutôt que &lt;code&gt;docker compose&lt;/code&gt; seul. Sur une machine unique, cela peut sembler superflu, mais Swarm nous apporte des limites de ressources par service (indispensable quand on fait tourner 17 conteneurs sur 4 Go de RAM) et le redémarrage automatique en cas de défaillance d'un conteneur.&lt;/p&gt;

&lt;p&gt;Initialisez-le :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker swarm init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le serveur est maintenant un manager Swarm à nœud unique. La configuration s'arrête là.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloner le dépôt
&lt;/h2&gt;

&lt;p&gt;Toute la configuration vit dans un dépôt git. Créez un dépôt privé sur GitHub, puis clonez-le sur le VPS :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /root
git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git supabase-vps-cluster
&lt;span class="nb"&gt;cd &lt;/span&gt;supabase-vps-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le dépôt est vide à ce stade. Vous ajouterez les fichiers Compose et les scripts dans les billets suivants. L'essentiel, c'est que le répertoire est en place et que la connexion git est établie.&lt;/p&gt;

&lt;p&gt;Garder la configuration dans git signifie qu'on peut déployer via un simple push/pull, examiner les diffs avant de les appliquer, et revenir en arrière si quelque chose casse.&lt;/p&gt;

&lt;p&gt;La structure que nous construisons :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;supabase-vps-cluster/
├── instances/
│   ├── project1/
│   │   └── docker-compose.yml
│   └── project2/
│       └── docker-compose.yml
├── traefik/
│   └── docker-compose.yml
└── scripts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Les secrets (fichiers &lt;code&gt;.env&lt;/code&gt;, configuration de Kong) ne sont jamais versionnés. Ils sont générés sur le serveur à partir de Vault, que nous configurons dans le billet 5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bilan
&lt;/h2&gt;

&lt;p&gt;Le serveur tourne, sécurisé derrière trois ports, avec une authentification SSH par clé uniquement et une protection contre les attaques par force brute ; Docker CE est installé depuis la source officielle, Swarm initialisé.&lt;/p&gt;

&lt;p&gt;Dans le billet suivant, nous installons Traefik et mettons en place HTTPS pour chaque sous-domaine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/partie-3-traefik-et-https-4b5f" class="crayons-btn crayons-btn--primary"&gt;Partie 3 — Traefik et HTTPS →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pourquoi auto-héberger&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le serveur&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;li&gt;Traefik et SSL&lt;/li&gt;
&lt;li&gt;La première instance Supabase&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Deux instances&lt;/li&gt;
&lt;li&gt;Sécurité et test de charge&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Partie 1 — Pourquoi auto-héberger Supabase</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 15:06:10 +0000</pubDate>
      <link>https://dev.to/voieducode/partie-1-pourquoi-auto-heberger-supabase-3eo2</link>
      <guid>https://dev.to/voieducode/partie-1-pourquoi-auto-heberger-supabase-3eo2</guid>
      <description>&lt;p&gt;&lt;em&gt;Partie 1 sur 7 — Supabase en auto-hébergement : retour d'expérience&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version française de &lt;a href="https://dev.to/voieducode/part-1-i-rebuilt-supabase-from-scratch-to-understand-what-i-was-paying-for-3oip"&gt;Part 1 — I rebuilt Supabase from scratch to understand what I was paying for&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Le niveau gratuit de Supabase offre deux projets actifs : Postgres, authentification, stockage, abonnements en temps réel, un tableau de bord — le tout gratuitement, et ça fonctionne bien. Si bien, en fait, qu'on finit par ne plus penser à ce qui se passe vraiment.&lt;/p&gt;

&lt;p&gt;Quand on clique sur "Créer un projet", un backend complet apparaît en une trentaine de secondes. Il en sort une chaîne de connexion, une URL d'API, un jeu de clés — qu'on colle dans son application, et ça marche. C'est pratique, mais c'est aussi une forme d'aveuglement. Impossible de raisonner sur un système qu'on ne comprend pas — d'en estimer les limites, d'anticiper ses modes de défaillance, de le déboguer avec assurance. Alors on espère, tout simplement, que ça continue de fonctionner.&lt;/p&gt;

&lt;p&gt;J'utilise Supabase depuis quelques mois et je voulais mieux le comprendre. J'ai donc décidé de reconstruire la même chose depuis zéro sur un serveur bon marché, voir chaque composant, faire toutes les erreurs. Non pas pour économiser — le service géré est raisonnablement tarifé, et je le défendrai à la fin de cette série — mais pour comprendre.&lt;/p&gt;

&lt;p&gt;Cette série documente ce parcours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce qu'est Supabase
&lt;/h2&gt;

&lt;p&gt;Supabase n'est pas un programme unique. C'est un assemblage de projets open source qui tournent derrière une passerelle API. Les principaux que nous déploierons dans cette série :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; -- la base de données&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GoTrue&lt;/strong&gt; (désormais officiellement appelé Supabase Auth) -- serveur d'authentification (inscription, connexion, JWT)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgREST&lt;/strong&gt; -- génère une API REST à partir du schéma Postgres&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realtime&lt;/strong&gt; -- serveur WebSocket pour les mises à jour en direct de la base de données&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; -- service d'upload de fichiers avec un backend compatible S3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kong&lt;/strong&gt; -- passerelle API qui se place devant tous les services ci-dessus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Studio&lt;/strong&gt; -- le tableau de bord web&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;postgres-meta&lt;/strong&gt; -- fournit l'introspection de schéma utilisée par Studio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chacun est un processus distinct : conteneur dédié, configuration propre, historique de versions indépendant, bugs inclus. Supabase les assemble, les configure et les opère. Quand on utilise le service géré, tout ce travail est invisible.&lt;/p&gt;

&lt;p&gt;Quand on auto-héberge, il devient très visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ce qu'on va construire
&lt;/h2&gt;

&lt;p&gt;À l'issue de cette série, on aura un cluster opérationnel avec deux instances Supabase entièrement isolées sur un seul serveur : HTTPS automatique pour chaque sous-domaine, secrets stockés dans HashiCorp Vault plutôt que dans des fichiers, détection d'intrusion à l'exécution via Falco (un outil de sécurité basé sur eBPF qui surveille les appels système à l'intérieur des conteneurs au niveau du système d'exploitation), et un test de charge qui montre exactement à quel moment le serveur commence à peiner.&lt;/p&gt;

&lt;p&gt;Plus important encore, on aura une image claire de ce que le service géré fait à notre place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Internet
                 |
         +---------------+
         |    Traefik    |   TLS, routing
         +-------+-------+
                 |
        +--------+--------+
        |                 |
+---------------+  +---------------+
|   Project 1   |  |   Project 2   |
|  -----------  |  |  -----------  |
|  Kong         |  |  Kong         |
|  GoTrue       |  |  GoTrue       |
|  PostgREST    |  |  PostgREST    |
|  Realtime     |  |  Realtime     |
|  Storage      |  |  Storage      |
|  Studio       |  |  Studio       |
|  Postgres     |  |  Postgres     |
+---------------+  +---------------+

         +---------------+
         |     Vault     |   secrets, localhost only
         +---------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  À qui s'adresse cette série
&lt;/h2&gt;

&lt;p&gt;Cette série s'adresse aux développeurs qui apprennent en construisant. Il faut être à l'aise avec un terminal. Aucune expérience préalable de Docker Swarm, Vault ou Traefik n'est requise. Tout est expliqué depuis le début.&lt;/p&gt;

&lt;p&gt;Plusieurs soirées de travail sont à prévoir. J'y ai passé plus de temps que prévu, en partie à cause de mes erreurs (que je documenterai), et en partie parce que chaque composant s'est révélé plus intéressant que je ne le pensais.&lt;/p&gt;

&lt;p&gt;Quiconque gère une application en production avec de vrais utilisateurs et de l'argent réel devrait utiliser le service géré. Compter sur un seul serveur amateur pour quelque chose d'important n'est pas raisonnable. En auto-hébergement, c'est à nous de nous lever quand quelque chose tombe en panne. Cette série est là pour apprendre, pas pour remplacer un service dont on dépend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le matériel
&lt;/h2&gt;

&lt;p&gt;Le serveur est un Hetzner CX22 : 2 vCPU, 4 Go de RAM, 40 Go de SSD, situé à Falkenstein, en Allemagne. Il coûte 4,51 EUR par mois (TVA allemande incluse).&lt;/p&gt;

&lt;p&gt;Hetzner est un hébergeur allemand. Je l'ai choisi pour son coût et parce que la résidence des données dans l'UE compte dès lors que les utilisateurs sont en Europe.&lt;/p&gt;

&lt;p&gt;Le CX22 paraît petit, et pour une offre cloud gérée, il l'est. Mais Supabase en auto-hébergement consomme étonnamment peu de mémoire au repos :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Mémoire au repos&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;~77 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;~229 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;~8 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;~15 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime&lt;/td&gt;
&lt;td&gt;~168 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Studio&lt;/td&gt;
&lt;td&gt;~170 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traefik&lt;/td&gt;
&lt;td&gt;~30 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vault&lt;/td&gt;
&lt;td&gt;~140 Mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Deux instances Supabase complètes tiennent dans environ 2,2 Go — le serveur en dispose de 4.&lt;/p&gt;

&lt;p&gt;Ces chiffres ont été mesurés à vide ; la partie 7 montre ce qui se passe sous charge.&lt;/p&gt;

&lt;p&gt;Lors d'un test de charge réaliste, avec 50 utilisateurs simultanés et un temps de réflexion entre les actions, le CPU de la base de données a culminé à 0,67 %. Le serveur n'avait presque rien à faire.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mes faux pas
&lt;/h2&gt;

&lt;p&gt;Voici un aperçu des erreurs que cette série documente.&lt;/p&gt;

&lt;p&gt;J'ai choisi la mauvaise version de Traefik. Elle n'était pas compatible avec la version de Docker installée. J'ai passé un après-midi à comprendre pourquoi les requêtes n'étaient pas routées avant de trouver la note de compatibilité enfouie dans le changelog.&lt;/p&gt;

&lt;p&gt;Le service Realtime, je l'ai déployé avec une clé de chiffrement de mauvaise longueur. Il a planté immédiatement avec une erreur sur la taille de la clé, sans mentionner la longueur attendue.&lt;/p&gt;

&lt;p&gt;J'ai utilisé &lt;code&gt;vault kv put&lt;/code&gt; là où il fallait utiliser &lt;code&gt;vault kv patch&lt;/code&gt;. Résultat : tous mes secrets ont été remplacés par une seule nouvelle clé. Mon mot de passe Postgres est devenu le mot "change_me". Je m'en suis rendu compte quand chaque appel API a commencé à échouer avec des erreurs d'authentification.&lt;/p&gt;

&lt;p&gt;Un test de charge semblait échouer sur les lectures. Le problème ne venait pas du serveur — le test tournait depuis l'Ohio et le serveur est en Allemagne. La latence réseau est bien réelle.&lt;/p&gt;

&lt;p&gt;Chaque erreur m'a appris quelque chose sur le fonctionnement du système — et c'est précisément ce que le service géré prend en charge, silencieusement, sans qu'on le sache jamais.&lt;/p&gt;




&lt;h2&gt;
  
  
  Le bilan honnête
&lt;/h2&gt;

&lt;p&gt;Avant de consacrer un samedi à ça : le niveau gratuit offre déjà deux projets. Pour la plupart des usages personnels, l'auto-hébergement n'apporte pas plus de capacité. Ce qu'il apporte, c'est de la compréhension.&lt;/p&gt;

&lt;p&gt;Après avoir construit ce cluster, j'ai une idée plus claire de ce que Supabase fait réellement, pourquoi le plan Pro coûte ce qu'il coûte, et ce à quoi on renonce en choisissant la voie du service géré. Ça valait le temps investi.&lt;/p&gt;

&lt;p&gt;Une note avant de commencer : Supabase publie un &lt;a href="https://supabase.com/docs/guides/self-hosting/docker" rel="noopener noreferrer"&gt;guide officiel d'auto-hébergement&lt;/a&gt; basé sur Docker Compose simple. Cette série opte pour Docker Swarm, qui ajoute des limites mémoire par service et le redémarrage automatique des conteneurs — mieux adapté à l'exécution de plusieurs projets sur un seul serveur avec une RAM contrainte, ce qui est exactement notre cas ici.&lt;/p&gt;

&lt;p&gt;La suite : créer le serveur et le sécuriser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/partie-2-le-serveur-19nj" class="crayons-btn crayons-btn--primary"&gt;Partie 2 — Le serveur →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  La série complète
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pourquoi auto-héberger&lt;/strong&gt; ← cet article&lt;/li&gt;
&lt;li&gt;Le serveur&lt;/li&gt;
&lt;li&gt;Traefik et SSL&lt;/li&gt;
&lt;li&gt;La première instance Supabase&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Deux instances&lt;/li&gt;
&lt;li&gt;Sécurité et test de charge&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosting</category>
      <category>francais</category>
    </item>
    <item>
      <title>Part 7 — Security and the load test</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 13:42:46 +0000</pubDate>
      <link>https://dev.to/voieducode/part-7-security-and-the-load-test-1c0l</link>
      <guid>https://dev.to/voieducode/part-7-security-and-the-load-test-1c0l</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 7 of 7 — Self-hosting Supabase: a learning journey&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also available in French: &lt;a href="https://dev.to/voieducode/partie-7-securite-et-test-de-charge-3ejk"&gt;Partie 7 — Sécurité et test de charge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We have a working two-project cluster. Now two questions: is it actually secure, and what does it take to break it?&lt;/p&gt;




&lt;h2&gt;
  
  
  The security layers
&lt;/h2&gt;

&lt;p&gt;Security here is defense in depth. Multiple layers, each one adding friction for an attacker. No single layer is sufficient on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  ufw and fail2ban
&lt;/h3&gt;

&lt;p&gt;The outer layer. Only ports 22, 80, and 443 are open. fail2ban bans IP addresses after five failed SSH attempts. This stops automated scanners and brute-force attacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kong: key authentication and rate limiting
&lt;/h3&gt;

&lt;p&gt;Every request to the API must include a valid &lt;code&gt;apikey&lt;/code&gt; header. Without it, Kong returns 401 before the request reaches any backend service. GoTrue, PostgREST, Realtime, Storage, none of them see unauthenticated traffic.&lt;/p&gt;

&lt;p&gt;Rate limiting: 30 requests per minute per IP address by default (configured with &lt;code&gt;limit_by: ip&lt;/code&gt; in the Kong rate-limiting plugin). This limits the damage from credential stuffing attempts and protects GoTrue from being used as a bulk signup platform.&lt;/p&gt;

&lt;p&gt;Kong is configured via a YAML file (&lt;code&gt;kong.yml&lt;/code&gt;) that lives on the server and is never committed to git. The important sections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;key_names&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apikey"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate-limiting&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
      &lt;span class="na"&gt;limit_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ip&lt;/span&gt;
      &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Studio behind basic auth
&lt;/h3&gt;

&lt;p&gt;The Studio dashboard gives full admin access to your database. It should not be publicly accessible. We set this up in Part 4: Traefik's basic auth middleware protects the Studio route, and the password hash is embedded in the service labels. See Part 4 for the &lt;code&gt;htpasswd -nB&lt;/code&gt; command and the &lt;code&gt;$$&lt;/code&gt; escaping rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Falco: runtime intrusion detection
&lt;/h3&gt;

&lt;p&gt;ufw and Kong handle external threats. Falco watches what happens inside the running containers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://falco.org/docs/" rel="noopener noreferrer"&gt;Falco&lt;/a&gt; is a security tool that hooks into Linux syscalls to monitor container activity: file access, process execution, network connections, privilege changes. We use &lt;code&gt;falco-modern-bpf&lt;/code&gt;, which uses eBPF as its capture mechanism. It runs on the host, outside the container's control. Events that match rules trigger alerts.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://falco.org/repo/falcosecurity-packages.asc &lt;span class="se"&gt;\&lt;/span&gt;
  | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/falco-archive-keyring.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://download.falco.org/packages/deb stable main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/falcosecurity.list

apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install &lt;/span&gt;falco &lt;span class="nt"&gt;-y&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;falco-modern-bpf
systemctl start falco-modern-bpf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom rules go in &lt;code&gt;/etc/falco/rules.d/&lt;/code&gt;. For our cluster, useful rules include alerting on unexpected outbound connections from containers and on direct psql commands containing destructive statements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The alerting gotcha.&lt;/strong&gt; The &lt;code&gt;program_output&lt;/code&gt; directive pipes alerts to a script. You can verify it works by watching the Falco journal while triggering an event: &lt;code&gt;journalctl -u falco-modern-bpf -f&lt;/code&gt;. If you see events there but your handler script is never called, &lt;code&gt;program_output&lt;/code&gt; is broken in your version. The safe workaround is a separate systemd service that tails the journal directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/falco-alerter.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Falco alert handler&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;falco-modern-bpf.service&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash -c 'journalctl -f -u falco-modern-bpf -o cat | /root/supabase-vps-cluster/scripts/falco-alert.sh'&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The alert handler logs events to &lt;code&gt;/var/log/falco-alerts.log&lt;/code&gt; with a 5-minute cooldown per rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected noise.&lt;/strong&gt; Kong's nginx health checks run every 10 seconds. Each one spawns a shell subprocess and reads &lt;code&gt;/etc/passwd&lt;/code&gt;. Falco reports this as "Shell spawned in container" and "Sensitive file read in container." This is normal behavior, not an attack. The cooldown mechanism keeps the log readable. After 24 hours of observation you can tune the rules to whitelist the specific Kong process.&lt;/p&gt;




&lt;h2&gt;
  
  
  The security picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
    |
  ufw (ports 22/80/443 only)
    |
  Traefik (TLS, security headers)
    |
    +-- Studio (Traefik basic auth)
    +-- Kong (key-auth + rate limiting)
              |
              +-- internal services (not publicly reachable)

Host layer:
  SSH key-only auth, fail2ban
  Falco eBPF watching all container syscalls
  Vault on localhost only, UI disabled
  No published Postgres port
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The load test
&lt;/h2&gt;

&lt;p&gt;I wanted a concrete answer to the question: what is the actual limit of this server?&lt;/p&gt;

&lt;p&gt;I used Grafana Cloud k6 for the tests. Before running any of them, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://grafana.com/docs/k6/latest/" rel="noopener noreferrer"&gt;k6&lt;/a&gt; installed locally (&lt;code&gt;brew install k6&lt;/code&gt; on macOS, or download from k6.io for other platforms)&lt;/li&gt;
&lt;li&gt;A Grafana Cloud account (free at grafana.com/products/cloud). The free tier allows up to 50 concurrent virtual users and tests up to around 10 minutes long.&lt;/li&gt;
&lt;li&gt;Your project's anon key and service role key (from Vault, or from the &lt;code&gt;.env&lt;/code&gt; file if you have not set up Vault yet)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 1: re-authentication on every request
&lt;/h3&gt;

&lt;p&gt;Each virtual user signs in on every iteration. The test ramps to 50 concurrent users.&lt;/p&gt;

&lt;p&gt;The server broke at around 30 to 50 VUs.&lt;/p&gt;

&lt;p&gt;At 50 VUs, the database CPU hit 100% and stayed there. GoTrue started returning 504 timeouts. The problem is bcrypt, the intentionally slow password-hashing algorithm. Each login requires a bcrypt verification via PostgreSQL's pgcrypto extension. With 50 users re-authenticating every few seconds, the database was saturated by cryptographic work alone.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;At rest&lt;/th&gt;
&lt;th&gt;During test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;51% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;100% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This looks alarming. But no real application works like this. JWT tokens are valid for an hour. You authenticate once, use the token, refresh it when it expires. You do not re-authenticate on every API call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 2: cached JWT, pure CRUD
&lt;/h3&gt;

&lt;p&gt;Each VU logs in once during setup and caches the token for the entire test. Then it runs insert, read, delete in a loop with no re-authentication.&lt;/p&gt;

&lt;p&gt;No breaking point at 50 VUs. The Grafana Cloud free tier hit its test duration limit (about 5 minutes) before the server showed any stress.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;At rest&lt;/th&gt;
&lt;th&gt;During test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;0.02% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;9% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;13% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Database CPU went from 100% to 9%. The only change was caching the JWT. PostgREST is now the highest-CPU service and would eventually become the bottleneck at higher VU counts, but we did not reach that ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 3: realistic sessions
&lt;/h3&gt;

&lt;p&gt;Three user archetypes in parallel, with randomized think time between actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;70% casual users (10 to 30 second think time, mostly reads)&lt;/li&gt;
&lt;li&gt;20% active users (5 to 15 second think time, mixed)&lt;/li&gt;
&lt;li&gt;10% power users (2 to 8 second think time, more writes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each user logs in once per session. Random idle periods simulate switching tabs or stepping away.&lt;/p&gt;

&lt;p&gt;The test ran to full completion: 10 minutes 30 seconds, zero errors.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;At rest&lt;/th&gt;
&lt;th&gt;Peak during test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;0.02% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;0.67% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;0% CPU&lt;/td&gt;
&lt;td&gt;1.19% CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server was essentially idle.&lt;/p&gt;

&lt;p&gt;There was eventually a threshold breach: read latency at the 95th percentile exceeded 300ms. This was not the server. The test ran from Grafana Cloud's Ohio region to our server in Germany. The base round-trip time is 100 to 130ms. The cluster was healthy throughout, the latency was from the network, not from the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the numbers mean
&lt;/h3&gt;

&lt;p&gt;With 50 concurrent users and realistic think time, there are 3 to 8 database queries actually running at any moment. Users are not hammering submit, they are reading something, typing a reply, thinking about what to do next. Think time changes the math completely.&lt;/p&gt;

&lt;p&gt;The CX22 is fine for a hobby project with real users. The only scenario that saturates it is a continuous re-authentication hammer test, which no real application does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the managed service provides
&lt;/h2&gt;

&lt;p&gt;After going through all of this, I have a much clearer sense of what Supabase gives you on the free tier.&lt;/p&gt;

&lt;p&gt;I spent several evenings on setup, configuration, and debugging. I went through the Vault incident. I debugged Traefik routing issues, Realtime crashes, incorrect environment variable scope, and healthcheck behavior in Docker Swarm. I set up monitoring so I know what the server is doing. I am responsible for upgrades, backups, and incidents.&lt;/p&gt;

&lt;p&gt;Supabase does all of that for two free projects. The infrastructure behind even one free project is more complex than everything in this series. The team keeps GoTrue, PostgREST, Realtime, and the rest upgraded and running, continuously, at scale.&lt;/p&gt;

&lt;p&gt;The Pro plan, database with point-in-time recovery, automated backups, connection pooling via PgBouncer, uptime guarantees, is a fair price for what it eliminates from your life. I understand that now because I have seen what it eliminates.&lt;/p&gt;

&lt;p&gt;Self-host if you want to learn. Use the managed service if you want to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why we are building this&lt;/li&gt;
&lt;li&gt;The server&lt;/li&gt;
&lt;li&gt;Traefik and SSL&lt;/li&gt;
&lt;li&gt;The first Supabase instance&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Two instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and the load test&lt;/strong&gt;, you are here&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
    <item>
      <title>Part 6 — Two instances</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 13:39:27 +0000</pubDate>
      <link>https://dev.to/voieducode/part-6-two-instances-26ll</link>
      <guid>https://dev.to/voieducode/part-6-two-instances-26ll</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 6 of 7 — Self-hosting Supabase: a learning journey&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also available in French: &lt;a href="https://dev.to/voieducode/partie-6-deux-instances-ml4"&gt;Partie 6 — Deux instances&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Supabase's free tier gives you two active projects. I was already using both. Adding a second instance to the self-hosted cluster was not about needing more capacity, it was about understanding isolation. When Supabase runs two projects on the same infrastructure, how does it keep them separate? This post answers that question by actually doing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What isolation means here
&lt;/h2&gt;

&lt;p&gt;When I say the two projects are isolated, I mean:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network isolation.&lt;/strong&gt; Each project has its own Docker overlay network. In the compose file you write &lt;code&gt;internal:&lt;/code&gt; as the network name, but when you deploy with &lt;code&gt;docker stack deploy ... project1&lt;/code&gt;, Swarm automatically prefixes it with the stack name. The network becomes &lt;code&gt;project1_internal&lt;/code&gt; at runtime. Services in project1 cannot reach services in project2 through their internal networks, even though both compose files define a network called &lt;code&gt;internal&lt;/code&gt;. The one shared network is &lt;code&gt;traefik_default&lt;/code&gt;, where both projects' Kong and Studio containers coexist for routing purposes. In theory, containers on the same overlay network can reach each other, so the isolation is not absolute at the network layer. In practice, each service only listens on its own port and requires its own project's API key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data isolation.&lt;/strong&gt; Each project has its own Postgres container with its own volume. The databases have no shared storage, no shared connection, no way to reach each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication isolation.&lt;/strong&gt; The JWT secrets are different. The user tables are different. A token issued by project1 is not valid on project2, and vice versa. The API keys (anon key, service role key) are different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing isolation.&lt;/strong&gt; Different subdomains, different TLS certificates.&lt;/p&gt;

&lt;p&gt;The only shared resources are the Traefik reverse proxy (which sits outside both stacks) and the physical server's CPU and RAM.&lt;/p&gt;




&lt;h2&gt;
  
  
  The compose file is nearly identical
&lt;/h2&gt;

&lt;p&gt;The project2 compose file is a copy of project1's with these changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Different values come from a separate Vault path (&lt;code&gt;secret/project2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Different Traefik router names (this is critical)&lt;/li&gt;
&lt;li&gt;Different subdomain rules in the Traefik labels&lt;/li&gt;
&lt;li&gt;Different &lt;code&gt;FLY_ALLOC_ID&lt;/code&gt; for Realtime (&lt;code&gt;project2-realtime&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The network names are not something you change manually, Swarm adds the stack name as a prefix automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Router names must be unique
&lt;/h2&gt;

&lt;p&gt;This is the one configuration detail that will break your second instance if you miss it.&lt;/p&gt;

&lt;p&gt;Traefik identifies routes by router names. If two services register a router with the same name, Traefik logs an error ("Router defined multiple times with different configurations") and the affected routes return 404. In a busy log stream this is easy to miss.&lt;/p&gt;

&lt;p&gt;In project1 we named our routers &lt;code&gt;p1-kong&lt;/code&gt; and &lt;code&gt;p1-studio&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.routers.p1-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project1.yourdomain.com`)&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.p1-studio.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`studio.project1.yourdomain.com`)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In project2 they must be different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.routers.p2-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project2.yourdomain.com`)&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.p2-studio.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`studio.project2.yourdomain.com`)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to service names and middleware names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traefik.http.services.p2-kong.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8000'&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.middlewares.p2-studio-auth.basicauth.users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;traefik.http.routers.p2-studio.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm,p2-studio-auth@swarm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefix everything with the project identifier. It takes 30 seconds to do this carefully.&lt;/p&gt;




&lt;h2&gt;
  
  
  Separate Vault secrets
&lt;/h2&gt;

&lt;p&gt;Store project2's secrets under a separate path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secret/project2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;different anon jwt&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;different service_role jwt&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project2.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project2.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project2.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"supabaserealtime"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a separate read-only token for project2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write project2-readonly vault-policy-project2.hcl
vault token create &lt;span class="nt"&gt;-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project2-readonly &lt;span class="nt"&gt;-ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8760h &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auth.client_token'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/project2-token.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the token at &lt;code&gt;/root/project2-token.txt&lt;/code&gt;, and add it as a separate GitHub Actions secret if you use automated deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Memory across two instances
&lt;/h2&gt;

&lt;p&gt;Running two full stacks, here is roughly how the 4 GB is used:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Project 1&lt;/th&gt;
&lt;th&gt;Project 2&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;~77 MB&lt;/td&gt;
&lt;td&gt;~77 MB&lt;/td&gt;
&lt;td&gt;~154 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong&lt;/td&gt;
&lt;td&gt;~229 MB&lt;/td&gt;
&lt;td&gt;~185 MB&lt;/td&gt;
&lt;td&gt;~414 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;td&gt;~12 MB&lt;/td&gt;
&lt;td&gt;~12 MB&lt;/td&gt;
&lt;td&gt;~24 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;td&gt;~17 MB&lt;/td&gt;
&lt;td&gt;~17 MB&lt;/td&gt;
&lt;td&gt;~34 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime&lt;/td&gt;
&lt;td&gt;~168 MB&lt;/td&gt;
&lt;td&gt;~168 MB&lt;/td&gt;
&lt;td&gt;~336 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;~18 MB&lt;/td&gt;
&lt;td&gt;~18 MB&lt;/td&gt;
&lt;td&gt;~36 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;postgres-meta&lt;/td&gt;
&lt;td&gt;~68 MB&lt;/td&gt;
&lt;td&gt;~68 MB&lt;/td&gt;
&lt;td&gt;~136 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Studio&lt;/td&gt;
&lt;td&gt;~170 MB&lt;/td&gt;
&lt;td&gt;~170 MB&lt;/td&gt;
&lt;td&gt;~340 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Subtotal for both projects: about 1.5 GB. Add Traefik (30 MB), Vault (140 MB), and the OS (around 300 MB) and you are at roughly 2.0 to 2.5 GB out of 4 GB available.&lt;/p&gt;

&lt;p&gt;A third instance would probably fit. I have not tried it yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploy project2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/fetch-env-from-vault.sh project2
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;instances/project2/.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; instances/project2/docker-compose.yml project2
bash scripts/init-realtime.sh project2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify both stacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see 17 or more services with all replicas at &lt;code&gt;1/1&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verifying the isolation
&lt;/h2&gt;

&lt;p&gt;Create a user on project1 and verify it does not exist on project2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://kong.project1.yourdomain.com/auth/v1/signup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: PROJECT1_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"test@example.com","password":"TestPass123!"}'&lt;/span&gt;

&lt;span class="c"&gt;# Try the same credentials on project2&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://kong.project2.yourdomain.com/auth/v1/token?grant_type&lt;span class="o"&gt;=&lt;/span&gt;password &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: PROJECT2_ANON_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email":"test@example.com","password":"TestPass123!"}'&lt;/span&gt;
&lt;span class="c"&gt;# {"error":"invalid_grant","error_description":"Invalid login credentials"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The auth systems are completely separate. This is also how Supabase keeps different customers' data isolated on their shared infrastructure. The approach is simpler than I expected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/part-7-security-and-the-load-test-1c0l" class="crayons-btn crayons-btn--primary"&gt;Part 7 — Security and the load test →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The full series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why we are building this&lt;/li&gt;
&lt;li&gt;The server&lt;/li&gt;
&lt;li&gt;Traefik and SSL&lt;/li&gt;
&lt;li&gt;The first Supabase instance&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two instances&lt;/strong&gt;, you are here&lt;/li&gt;
&lt;li&gt;Security and the load test&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
    <item>
      <title>Part 5 — Vault, and the afternoon I deleted everything</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 13:38:53 +0000</pubDate>
      <link>https://dev.to/voieducode/part-5-vault-and-the-afternoon-i-deleted-everything-4kdj</link>
      <guid>https://dev.to/voieducode/part-5-vault-and-the-afternoon-i-deleted-everything-4kdj</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 5 of 7 — Self-hosting Supabase: a learning journey&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also available in French: &lt;a href="https://dev.to/voieducode/partie-5-vault-et-lapres-midi-ou-jai-tout-efface-1df9"&gt;Partie 5 — Vault, et l'après-midi où j'ai tout effacé&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to tell you about the afternoon I replaced all my Supabase secrets with the word &lt;code&gt;change_me&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My Postgres password: &lt;code&gt;change_me&lt;/code&gt;. My JWT secret: &lt;code&gt;change_me&lt;/code&gt;. My service role key: &lt;code&gt;change_me&lt;/code&gt;. Everything.&lt;/p&gt;

&lt;p&gt;The services started. The health checks passed (Traefik only checks HTTP status codes). Then every API call started failing with authentication errors. I SSH'd in, checked the running environment, and saw it immediately. One wrong command had replaced the entire secret store with a single key-value pair.&lt;/p&gt;

&lt;p&gt;Not a rogue script. The Supabase Docker Compose template uses &lt;code&gt;${POSTGRES_PASSWORD:-change_me}&lt;/code&gt; as a fallback default for every secret variable — a placeholder that is supposed to be overridden before deployment. When my fetch script regenerated the &lt;code&gt;.env&lt;/code&gt; from Vault, it found only one key. Everything else fell back to the template default. I explain exactly how this happened in the next section.&lt;/p&gt;

&lt;p&gt;The good news is that HashiCorp Vault keeps a full version history. I recovered everything from version 7 of my secret. But it was a stressful 20 minutes, and I will explain exactly how to not do this mistake.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why bother with Vault
&lt;/h2&gt;

&lt;p&gt;The alternative to Vault is keeping secrets in &lt;code&gt;.env&lt;/code&gt; files on the server. This works, but it has problems.&lt;/p&gt;

&lt;p&gt;The obvious one: if you ever commit a &lt;code&gt;.env&lt;/code&gt; file to git by mistake, your credentials are in version history permanently. Deleting the file does not help. The history is there.&lt;/p&gt;

&lt;p&gt;The less obvious one: with two projects running, you have two &lt;code&gt;.env&lt;/code&gt; files. You need a system for rotating secrets and for knowing which version of which secret was deployed at what time. Flat files do not give you any of that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.hashicorp.com/vault/docs" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt; solves all of it. Secrets are encrypted at rest. Access is controlled by tokens with specific permissions. Every change is versioned. You can recover any previous version of any secret.&lt;/p&gt;

&lt;p&gt;We run Vault on the same server as our Docker stacks, bound only to localhost. It is never accessible from the internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing Vault
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;jq&lt;/code&gt; first. The fetch script uses it to parse Vault's JSON output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;jq &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-O-&lt;/span&gt; https://apt.releases.hashicorp.com/gpg | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/hashicorp-archive-keyring.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  https://apt.releases.hashicorp.com &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-cs&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/hashicorp.list

apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install &lt;/span&gt;vault &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create the configuration:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/vault
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/vault/config.hcl &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
ui             = false
disable_mlock  = true

storage "file" {
  path = "/opt/vault/data"
}

listener "tcp" {
  address       = "127.0.0.1:8200"
  tls_cert_file = "/etc/vault/tls/vault.crt"
  tls_key_file  = "/etc/vault/tls/vault.key"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two things to explain here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;disable_mlock = true&lt;/code&gt;: By default, Vault locks all its memory pages to prevent secrets from being swapped to disk. mlock does not allocate extra memory, but it pins all of Vault's resident pages in RAM so they cannot be swapped out. On this server I observed Vault using about 376 MB with mlock enabled versus about 140 MB without it, likely because the kernel reclaims unused pages more aggressively when they are not pinned. On our 4 GB server with 17 running containers, disabling mlock is a sensible trade-off. For a single node without hardware security modules, the swap risk is low.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;address = "127.0.0.1:8200"&lt;/code&gt;: Vault listens only on localhost. It is accessible only from the server itself, not from the network.&lt;/p&gt;

&lt;p&gt;Vault requires TLS even for localhost connections. Generate a self-signed certificate:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/vault/tls
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /etc/vault/tls/vault.key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; /etc/vault/tls/vault.crt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=vault-localhost"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-addext&lt;/span&gt; &lt;span class="s2"&gt;"subjectAltName=IP:127.0.0.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create the systemd unit and start Vault:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/vault.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=HashiCorp Vault
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/vault server -config=/etc/vault/config.hcl
ExecReload=/bin/kill --signal HUP &lt;/span&gt;&lt;span class="nv"&gt;$MAINPID&lt;/span&gt;&lt;span class="sh"&gt;
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
LimitMEMLOCK=infinity

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;vault
systemctl start vault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Initialize Vault:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault operator init &lt;span class="nt"&gt;-key-shares&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-key-threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This prints an unseal key and a root token. Save them both somewhere safe and offline. A password manager's secure note is fine. If you lose the unseal key, your encrypted data is permanently inaccessible. There is no recovery path.&lt;/p&gt;

&lt;p&gt;Unseal Vault:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200 &lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  vault operator unseal YOUR_UNSEAL_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Storing secrets
&lt;/h2&gt;

&lt;p&gt;Enable the KV (Key-Value) version 2 secrets engine:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_ROOT_TOKEN

vault secrets &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret kv-v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Store project1's secrets:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv put secret/project1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-anon-jwt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-service-role-jwt"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project1.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project1.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://kong.project1.yourdomain.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"supabaserealtime"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The mistake: put versus patch
&lt;/h2&gt;

&lt;p&gt;KV v2 has two commands for writing secrets:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vault kv put&lt;/code&gt; replaces the entire secret with exactly what you specify. If you run &lt;code&gt;vault kv put secret/project1 GOTRUE_MAILER_AUTOCONFIRM=true&lt;/code&gt;, the result is a secret containing exactly one key: &lt;code&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/code&gt;. Everything else is gone from the current version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vault kv patch&lt;/code&gt; merges new keys into the existing secret. Running &lt;code&gt;vault kv patch secret/project1 GOTRUE_MAILER_AUTOCONFIRM=true&lt;/code&gt; adds the key while keeping everything else intact.&lt;/p&gt;

&lt;p&gt;I needed to add &lt;code&gt;GOTRUE_MAILER_AUTOCONFIRM=true&lt;/code&gt; to enable email autoconfirm for a load test. I used &lt;code&gt;vault kv put&lt;/code&gt;. Every other key was wiped from the current version.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
⚠️ &lt;strong&gt;Use &lt;code&gt;vault kv patch&lt;/code&gt; when adding or updating individual keys.&lt;/strong&gt; Reserve &lt;code&gt;vault kv put&lt;/code&gt; for when you intentionally want to replace the entire secret.&lt;br&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Recovery from version history
&lt;/h2&gt;

&lt;p&gt;KV v2 keeps previous versions of a secret (up to 10 by default, configurable). List the versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv metadata get secret/project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read a specific version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault kv get &lt;span class="nt"&gt;-version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;7 secret/project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found all my original secrets in version 7 and used &lt;code&gt;vault kv patch&lt;/code&gt; to restore them into the current version. Twenty minutes of stress, entirely recoverable.&lt;/p&gt;

&lt;p&gt;This version history is not just a nice-to-have. It is the reason to use KV v2 rather than KV v1.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fetching secrets for deployment
&lt;/h2&gt;

&lt;p&gt;We use a script that reads from Vault and writes a &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200
&lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /root/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;-token&lt;/span&gt;.txt&lt;span class="si"&gt;)&lt;/span&gt;

vault kv get &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json secret/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.data | to_entries[] | "\(.key)=\(.value)"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/supabase-vps-cluster/instances/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deployment sequence becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/fetch-env-from-vault.sh project1
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;instances/project1/.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; instances/project1/docker-compose.yml project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Per-project tokens
&lt;/h2&gt;

&lt;p&gt;The root Vault token has unrestricted access to everything. We do not use it for deployments. Instead, we create a policy that grants read-only access to a single project's secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vault-policy-project1.hcl&lt;/span&gt;
&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"secret/data/project1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"secret/metadata/project1"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write project1-readonly vault-policy-project1.hcl
vault token create &lt;span class="nt"&gt;-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project1-readonly &lt;span class="nt"&gt;-ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8760h &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auth.client_token'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /root/project1-token.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deploy script reads from &lt;code&gt;/root/project1-token.txt&lt;/code&gt;. If you use GitHub Actions to automate deployments, store this token as a repository secret (&lt;code&gt;VAULT_TOKEN_PROJECT1&lt;/code&gt;) and pass it to the fetch script. It can read project1's secrets and nothing else.&lt;/p&gt;




&lt;h2&gt;
  
  
  One limitation: reboots
&lt;/h2&gt;

&lt;p&gt;When the VPS reboots, Vault starts in a sealed state. All requests are refused until you unseal it manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@YOUR_VPS_IP
&lt;span class="nv"&gt;VAULT_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://127.0.0.1:8200 &lt;span class="nv"&gt;VAULT_SKIP_VERIFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;vault operator unseal
&lt;span class="c"&gt;# enter your unseal key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running containers keep their environment variables and continue working. But any new deployment would fail to fetch secrets until Vault is unsealed.&lt;/p&gt;

&lt;p&gt;Auto-unsealing requires either a cloud-hosted HSM service or storing the unseal key on disk, which defeats the purpose. Manual unsealing after reboots is the right choice for a single-node hobby setup. Hetzner servers do not reboot unless you tell them to.&lt;/p&gt;

&lt;p&gt;Back up your unseal key. You cannot recover it once it is lost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/part-6-two-instances-26ll" class="crayons-btn crayons-btn--primary"&gt;Part 6 — Two instances →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The full series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why we are building this&lt;/li&gt;
&lt;li&gt;The server&lt;/li&gt;
&lt;li&gt;Traefik and SSL&lt;/li&gt;
&lt;li&gt;The first Supabase instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vault&lt;/strong&gt;, you are here&lt;/li&gt;
&lt;li&gt;Two instances&lt;/li&gt;
&lt;li&gt;Security and the load test&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
    <item>
      <title>Part 4 — The first Supabase instance</title>
      <dc:creator>Dinh Doan Van Bien</dc:creator>
      <pubDate>Fri, 06 Mar 2026 13:38:16 +0000</pubDate>
      <link>https://dev.to/voieducode/part-4-the-first-supabase-instance-1e7f</link>
      <guid>https://dev.to/voieducode/part-4-the-first-supabase-instance-1e7f</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of 7 — Self-hosting Supabase: a learning journey&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Also available in French: &lt;a href="https://dev.to/voieducode/partie-4-la-premiere-instance-supabase-3205"&gt;Partie 4 — La première instance Supabase&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We have a server, Docker Swarm, and Traefik running. Now we deploy Supabase. This is the part with the most surprises. I will document each one as we get to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What one Supabase project is
&lt;/h2&gt;

&lt;p&gt;Before writing any configuration, it helps to have a clear picture of what we are deploying. One Supabase project is eight Docker services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
    |
  Traefik (TLS termination, routing)
    |
  Kong (API gateway, port 8000)
    |
    +-- GoTrue   (auth, port 9999)
    +-- PostgREST (REST API, port 3000)
    +-- Realtime  (WebSockets, port 4000)
    +-- Storage   (files, port 5000)
    +-- Studio   (dashboard, port 3000)
    +-- postgres-meta (schema introspection, port 8080)

  PostgreSQL (port 5432, internal only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Kong and Studio are the only services accessible from the internet (through Traefik). Studio is protected with basic auth, as we will see below. All other services are on an internal Docker overlay network. PostgreSQL is never published to the host.&lt;/p&gt;


&lt;h2&gt;
  
  
  The secrets you need to generate
&lt;/h2&gt;

&lt;p&gt;Before writing the compose file, generate these values:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Postgres password&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16

&lt;span class="c"&gt;# JWT secret (must be at least 32 characters)&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32

&lt;span class="c"&gt;# For Studio's schema browser&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16   &lt;span class="c"&gt;# PG_META_CRYPTO_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The anon key and service_role key are standard JWTs signed with your JWT secret. You can generate them with this script:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-jwt-secret-here"&lt;/span&gt;

&lt;span class="c"&gt;# Expiry: year 2035 (Unix timestamp)&lt;/span&gt;
&lt;span class="nv"&gt;EXPIRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2051222400

python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
import json, hmac, hashlib, base64

secret = "&lt;/span&gt;&lt;span class="nv"&gt;$JWT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"

def b64(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

def enc(obj):
    return b64(json.dumps(obj, separators=(',', ':')).encode())

header = enc({"alg":"HS256","typ":"JWT"})

for role in ["anon", "service_role"]:
    payload = enc({
        "role": role,
        "iss": "supabase",
        "iat": 1772393548,
        "exp": &lt;/span&gt;&lt;span class="nv"&gt;$EXPIRY&lt;/span&gt;&lt;span class="sh"&gt;
    })
    msg = f"{header}.{payload}".encode()
    sig = b64(hmac.new(secret.encode(), msg, hashlib.sha256).digest())
    print(f"{role}: {header}.{payload}.{sig}")
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;anon&lt;/code&gt; JWT is safe to expose to browsers. The &lt;code&gt;service_role&lt;/code&gt; JWT bypasses row-level security and must be kept secret.&lt;/p&gt;

&lt;p&gt;We will store all of these in Vault in Part 5. For now, write them somewhere safe.&lt;/p&gt;


&lt;h2&gt;
  
  
  The docker-compose.yml
&lt;/h2&gt;

&lt;p&gt;Here is the complete stack definition. I will explain each surprising part after.&lt;/p&gt;

&lt;p&gt;The image tags below point to current major versions. For exact pinned versions of each component, check the official Supabase self-hosting reference at &lt;code&gt;supabase.com/docs/guides/self-hosting&lt;/code&gt;. They maintain a tested, stable combination of versions there.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;overlay&lt;/span&gt;
  &lt;span class="na"&gt;traefik_default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storage_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/postgres:15&lt;/span&gt;          &lt;span class="c1"&gt;# use latest 15.x from supabase.com/docs/guides/self-hosting&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1g&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1.0'&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;

  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/gotrue:latest&lt;/span&gt;         &lt;span class="c1"&gt;# pin to a stable release tag; see note below&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_DB_DRIVER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_DB_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_EXP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3600'&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_AUD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authenticated&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_DEFAULT_GROUP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authenticated&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_JWT_ADMIN_ROLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_role&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_API_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_API_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9999'&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_SITE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SITE_URL}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOTRUE_EXTERNAL_URL}&lt;/span&gt;
      &lt;span class="na"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${API_EXTERNAL_URL}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOTRUE_MAILER_AUTOCONFIRM:-false}&lt;/span&gt;
      &lt;span class="na"&gt;GOTRUE_SMS_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;rest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/postgrest:v12&lt;/span&gt; &lt;span class="c1"&gt;# use latest stable v12&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_URI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_SCHEMA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public,storage,graphql_public&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_ANON_ROLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_DB_USE_LEGACY_GUCS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;realtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/realtime:v2&lt;/span&gt;   &lt;span class="c1"&gt;# use latest stable v2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_ENC_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;DB_AFTER_CONNECT_QUERY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SET search_path TO _realtime&lt;/span&gt;
      &lt;span class="na"&gt;API_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SECRET_KEY_BASE}&lt;/span&gt;
      &lt;span class="na"&gt;APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime&lt;/span&gt;
      &lt;span class="na"&gt;FLY_APP_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime&lt;/span&gt;
      &lt;span class="na"&gt;FLY_ALLOC_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;project1-realtime&lt;/span&gt;
      &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;4000'&lt;/span&gt;
      &lt;span class="na"&gt;SEED_SELF_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
      &lt;span class="na"&gt;RUN_JANITOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
      &lt;span class="na"&gt;ENABLE_TAILSCALE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
      &lt;span class="na"&gt;DNS_NODES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;ERL_AFLAGS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;-proto_dist inet_tcp&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/storage-api:v1&lt;/span&gt; &lt;span class="c1"&gt;# use latest stable v1&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ANON_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_ANON_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;SERVICE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_SERVICE_ROLE_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres&lt;/span&gt;
      &lt;span class="na"&gt;FILE_STORAGE_BACKEND_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/storage&lt;/span&gt;
      &lt;span class="na"&gt;STORAGE_BACKEND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
      &lt;span class="na"&gt;FILE_SIZE_LIMIT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;52428800'&lt;/span&gt;
      &lt;span class="na"&gt;GLOBAL_S3_BUCKET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stub&lt;/span&gt;
      &lt;span class="na"&gt;REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stub&lt;/span&gt;
      &lt;span class="na"&gt;TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stub&lt;/span&gt;
      &lt;span class="na"&gt;POSTGREST_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://rest:3000&lt;/span&gt;
      &lt;span class="na"&gt;PGRST_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;DB_INSTALL_ROLES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;storage_data:/var/lib/storage&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;

  &lt;span class="na"&gt;kong&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/supabase/kong:2.8.1&lt;/span&gt;   &lt;span class="c1"&gt;# Kong version; only change if Supabase releases a new one&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;KONG_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;off'&lt;/span&gt;
      &lt;span class="na"&gt;KONG_DECLARATIVE_CONFIG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/kong/kong.yml&lt;/span&gt;
      &lt;span class="na"&gt;KONG_LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
      &lt;span class="na"&gt;KONG_PROXY_ACCESS_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stdout&lt;/span&gt;
      &lt;span class="na"&gt;KONG_PROXY_ERROR_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stderr&lt;/span&gt;
      &lt;span class="na"&gt;KONG_ADMIN_ACCESS_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stdout&lt;/span&gt;
      &lt;span class="na"&gt;KONG_ADMIN_ERROR_LOG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/stderr&lt;/span&gt;
      &lt;span class="na"&gt;KONG_SERVER_TOKENS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;off'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/root/supabase-vps-cluster/instances/project1/kong.yml:/var/lib/kong/kong.yml:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`kong.project1.yourdomain.com`)&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.tls.certresolver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;le&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-kong.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.services.p1-kong.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;8000'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.swarm.network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;

  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/postgres-meta:v0&lt;/span&gt;      &lt;span class="c1"&gt;# use latest stable v0&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase_admin&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_DB_SSL_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disable&lt;/span&gt;
      &lt;span class="na"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PG_META_CRYPTO_KEY}&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.25'&lt;/span&gt;

  &lt;span class="na"&gt;studio&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;supabase/studio:latest&lt;/span&gt;        &lt;span class="c1"&gt;# always use the latest Studio tag&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;HOSTNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://kong:8000&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_PUBLIC_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${API_EXTERNAL_URL}&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_ANON_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;SUPABASE_SERVICE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SUPABASE_SERVICE_ROLE_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;AUTH_JWT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JWT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;STUDIO_PG_META_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://meta:8080&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_ORGANIZATION_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Default Organization&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_PROJECT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Default Project&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512m&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.5'&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;traefik.enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.entrypoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`studio.project1.yourdomain.com`)&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.tls.certresolver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;le&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.services.p1-studio.loadbalancer.server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000'&lt;/span&gt;
        &lt;span class="na"&gt;traefik.swarm.network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik_default&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.routers.p1-studio.middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-headers@swarm,p1-studio-auth@swarm&lt;/span&gt;
        &lt;span class="na"&gt;traefik.http.middlewares.p1-studio-auth.basicauth.users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_HASHED_CREDENTIALS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Replace &lt;code&gt;YOUR_HASHED_CREDENTIALS&lt;/code&gt; with a bcrypt hash of your password. Install the tool and generate the hash on the server:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apache2-utils &lt;span class="nt"&gt;-y&lt;/span&gt;
htpasswd &lt;span class="nt"&gt;-nB&lt;/span&gt; admin
&lt;span class="c"&gt;# New password:&lt;/span&gt;
&lt;span class="c"&gt;# Re-type new password:&lt;/span&gt;
&lt;span class="c"&gt;# admin:$2y$05$...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Copy the output (including the username). In Docker Compose labels, every &lt;code&gt;$&lt;/code&gt; must be doubled because Compose uses &lt;code&gt;$&lt;/code&gt; for variable interpolation. The string &lt;code&gt;admin:$2y$05$...&lt;/code&gt; becomes &lt;code&gt;admin:$$2y$$05$$...&lt;/code&gt; in the label.&lt;/p&gt;


&lt;h2&gt;
  
  
  kong.yml: the API gateway configuration
&lt;/h2&gt;

&lt;p&gt;The compose file bind-mounts &lt;code&gt;/root/supabase-vps-cluster/instances/project1/kong.yml&lt;/code&gt; into the Kong container. This file is where you define routes, authentication, and rate limits. It is not committed to git because it contains your API keys.&lt;/p&gt;

&lt;p&gt;Create it at that path on the server:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;_format_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2.1'&lt;/span&gt;
&lt;span class="na"&gt;_transform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;consumers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
    &lt;span class="na"&gt;keyauth_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_SUPABASE_ANON_KEY&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_role&lt;/span&gt;
    &lt;span class="na"&gt;keyauth_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;

&lt;span class="na"&gt;acls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;consumer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_role&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/verify&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/verify&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open-callback&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/callback&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open-callback&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/callback&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-all&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate-limiting&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;minute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
          &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
          &lt;span class="na"&gt;limit_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ip&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rest-v1&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://rest:3000/&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rest-v1-all&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/rest/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime-v1-ws&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://realtime:4000/socket&lt;/span&gt;
    &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ws&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;realtime-v1-ws&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/realtime/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage-v1&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://storage:5000/&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;storage-v1-all&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/storage/v1/&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;key-auth&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acl&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;hide_groups_header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;anon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few things to note. The &lt;code&gt;auth-v1-open&lt;/code&gt; routes (&lt;code&gt;/verify&lt;/code&gt;, &lt;code&gt;/callback&lt;/code&gt;) are intentionally left without key-auth -- these are the OAuth redirect endpoints that browsers call directly during login flows and cannot include an API key header. Everything else requires a valid key.&lt;/p&gt;

&lt;p&gt;The file permissions matter: &lt;code&gt;chmod 644 kong.yml&lt;/code&gt;. Kong runs as a non-root user and will fail with a permission error on files set to &lt;code&gt;600&lt;/code&gt; or &lt;code&gt;700&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After any change to this file, Kong does not pick it up automatically. Force a restart:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service update &lt;span class="nt"&gt;--force&lt;/span&gt; project1_kong
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Surprise 1: memory limits are not optional
&lt;/h2&gt;

&lt;p&gt;Without memory limits, services compete for RAM on a 4 GB server and can trigger OOM kills (the Linux kernel forcibly terminating a process that exceeds its memory allowance), taking down other containers. You want hard limits.&lt;/p&gt;

&lt;p&gt;The numbers I landed on after tuning:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Memory limit&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;db&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;Postgres buffer cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kong&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;td&gt;More than expected, Kong caches config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;realtime&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;td&gt;Erlang/BEAM VM uses ~200 MB at idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;studio&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;td&gt;Next.js server-side rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;auth&lt;/td&gt;
&lt;td&gt;256 MB&lt;/td&gt;
&lt;td&gt;GoTrue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rest&lt;/td&gt;
&lt;td&gt;256 MB&lt;/td&gt;
&lt;td&gt;PostgREST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;storage&lt;/td&gt;
&lt;td&gt;256 MB&lt;/td&gt;
&lt;td&gt;Storage API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;meta&lt;/td&gt;
&lt;td&gt;256 MB&lt;/td&gt;
&lt;td&gt;postgres-meta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Realtime was the one that surprised me most. The BEAM virtual machine (Erlang's runtime, which Realtime is built on) has a large baseline footprint, around 200 MB before any connections are established. I set it initially to 256 MB, which looked generous, and it kept to hit the limit. 512 MB is correct. That is what Supabase Cloud allocates, for the same reason.&lt;/p&gt;


&lt;h2&gt;
  
  
  Surprise 2: Studio needs three non-obvious variables
&lt;/h2&gt;

&lt;p&gt;Studio is a Next.js application. Server-side rendering runs inside the container; client-side rendering runs in the browser. These two contexts need different URLs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SUPABASE_URL: http://kong:8000&lt;/code&gt;, for server-side code running inside Docker, it reaches Kong by container name on the internal network&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SUPABASE_PUBLIC_URL&lt;/code&gt;, the public HTTPS URL, for browser-side code&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;, Studio makes direct Postgres connections for its query runner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these are missing, Studio produces confusing 400/500 errors in the browser console with no obvious indication of what is wrong. I had to read the Studio source code to understand why. It is not obvious to the user when these variables are missing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Surprise 3: Studio's healthcheck kills it
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;supabase/studio&lt;/code&gt; image includes a built-in Docker healthcheck. In Swarm, a container that fails its healthcheck gets killed and restarted. Studio's healthcheck was failing on our setup.&lt;/p&gt;

&lt;p&gt;Disable it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same problem with &lt;code&gt;postgres-meta&lt;/code&gt;. It also has a built-in healthcheck that triggers exit 137 (SIGKILL) in Swarm. Disable that one too.&lt;/p&gt;


&lt;h2&gt;
  
  
  Surprise 4: you cannot hardcode GOTRUE_MAILER_AUTOCONFIRM
&lt;/h2&gt;

&lt;p&gt;For development and load testing, you want email signup to auto-confirm (skip the verification email). I initially set this in the compose file as a hardcoded string:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then I needed to change it to &lt;code&gt;true&lt;/code&gt;. I updated the &lt;code&gt;.env&lt;/code&gt; file. Redeployed. Nothing changed. The service was still reading &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem is that a hardcoded string in the &lt;code&gt;environment:&lt;/code&gt; block has priority over a variable from the &lt;code&gt;.env&lt;/code&gt; file. The &lt;code&gt;.env&lt;/code&gt; variable was being ignored.&lt;/p&gt;

&lt;p&gt;The fix is to use variable substitution:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GOTRUE_MAILER_AUTOCONFIRM:-false}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;:-false&lt;/code&gt; part means "use this value if the variable is not set." Now the &lt;code&gt;.env&lt;/code&gt; file controls the value. This is how it should be from the start.&lt;/p&gt;


&lt;h2&gt;
  
  
  Surprise 5: DB_ENC_KEY must be exactly 16 bytes
&lt;/h2&gt;

&lt;p&gt;Realtime uses AES-128 encryption, which requires a 16-byte key. I generated a key with &lt;code&gt;openssl rand -hex 32&lt;/code&gt;, which gives 32 hexadecimal characters. But 32 hex characters represent 16 bytes, 2 hex chars per byte. That should work. Right?&lt;/p&gt;

&lt;p&gt;No. Realtime passes the key string directly as the key value, not as a hex-encoded byte array. The string &lt;code&gt;openssl rand -hex 32&lt;/code&gt; gives a 32-character string, which is treated as 32 bytes. AES-128 needs 16 bytes. The service crashes with "Bad key size."&lt;/p&gt;

&lt;p&gt;The official default for self-hosted Realtime is the literal string &lt;code&gt;supabaserealtime&lt;/code&gt;. It is exactly 16 characters, therefore 16 bytes. Use this value. Do not be creative with key generation here.&lt;/p&gt;


&lt;h2&gt;
  
  
  Surprise 6: the _realtime schema
&lt;/h2&gt;

&lt;p&gt;The official Supabase Docker Compose repository includes a file &lt;code&gt;docker/volumes/db/realtime.sql&lt;/code&gt; that is mounted into the Postgres container and creates the &lt;code&gt;_realtime&lt;/code&gt; schema automatically on first boot. If you clone the official repo, this is handled for you.&lt;/p&gt;

&lt;p&gt;We do the same thing: a small SQL file mounted into &lt;code&gt;docker-entrypoint-initdb.d/&lt;/code&gt; handles schema creation on fresh deployments. But creating the schema is not enough. Realtime v2.76+ also needs database migrations and a seeded tenant record. The &lt;code&gt;init-realtime.sh&lt;/code&gt; script handles all of this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/init-realtime.sh project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What the script does: creates the &lt;code&gt;_realtime&lt;/code&gt; schema if absent, runs Realtime's database migrations, force-restarts the service so &lt;code&gt;SEED_SELF_HOST&lt;/code&gt; creates the tenant record, and verifies the tenant was seeded correctly. It is safe to run multiple times.&lt;/p&gt;


&lt;h2&gt;
  
  
  Surprise 7: API_EXTERNAL_URL must point to Kong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;API_EXTERNAL_URL&lt;/code&gt; drives the URLs that GoTrue puts into emails (password resets, confirmations) and the public URL that Studio uses for browser-side API calls.&lt;/p&gt;

&lt;p&gt;I pointed it at PostgREST, because PostgREST is the REST API. That seems sensible. But PostgREST is an internal service. Kong is the gateway that fronts everything, handles authentication, and enforces rate limits. The external URL must be Kong's public address:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pointing it at PostgREST bypasses Kong entirely, which breaks authentication.&lt;/p&gt;


&lt;h2&gt;
  
  
  A note on GoTrue image tags
&lt;/h2&gt;


&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
⚠️ &lt;strong&gt;Avoid GoTrue release candidates.&lt;/strong&gt; &lt;code&gt;:latest&lt;/code&gt; can pull an RC. GoTrue RCs have had database migration ordering bugs that cause the service to fail at startup with a cryptic error. If GoTrue fails to start and the logs mention migrations, check the &lt;a href="https://github.com/supabase/gotrue/releases" rel="noopener noreferrer"&gt;GoTrue releases page&lt;/a&gt; and pin to the most recent stable tag.&lt;br&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deploy
&lt;/h2&gt;

&lt;p&gt;Create the &lt;code&gt;.env&lt;/code&gt; file (we will move this to Vault in Post 5). First, generate the two remaining secrets. These are shell commands, not literal &lt;code&gt;.env&lt;/code&gt; values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 64   &lt;span class="c"&gt;# copy this as SECRET_KEY_BASE&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 16   &lt;span class="c"&gt;# copy this as PG_META_CRYPTO_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the file with real values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# instances/project1/.env&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generated above&amp;gt;
&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;generated above&amp;gt;
&lt;span class="nv"&gt;SUPABASE_ANON_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;anon jwt from the script&amp;gt;
&lt;span class="nv"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;service_role jwt from the script&amp;gt;
&lt;span class="nv"&gt;API_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;span class="nv"&gt;GOTRUE_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://kong.project1.yourdomain.com
&lt;span class="nv"&gt;GOTRUE_MAILER_AUTOCONFIRM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="nv"&gt;DB_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;supabaserealtime
&lt;span class="nv"&gt;SECRET_KEY_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;paste &lt;/span&gt;the 128-char hex string&amp;gt;
&lt;span class="nv"&gt;PG_META_CRYPTO_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;paste &lt;/span&gt;the 32-char hex string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source &lt;/span&gt;instances/project1/.env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker stack deploy &lt;span class="nt"&gt;-c&lt;/span&gt; instances/project1/docker-compose.yml project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check that all services come up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service &lt;span class="nb"&gt;ls&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All eight should show &lt;code&gt;1/1&lt;/code&gt; replicas within a minute or two. If any show &lt;code&gt;0/1&lt;/code&gt;, check the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker service logs &lt;span class="nt"&gt;--tail&lt;/span&gt; 50 project1_auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initialize Realtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash scripts/init-realtime.sh project1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the API endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://kong.project1.yourdomain.com/health
&lt;span class="c"&gt;# {"status":"healthy"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Where we are
&lt;/h2&gt;

&lt;p&gt;A working Supabase instance: Postgres, authentication, REST API, real-time subscriptions, file storage, and a dashboard protected with basic auth.&lt;/p&gt;

&lt;p&gt;In the next post, we move all those secrets out of flat files and into Vault, and I will tell you about the afternoon I accidentally deleted everything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/voieducode/part-5-vault-and-the-afternoon-i-deleted-everything-4kdj" class="crayons-btn crayons-btn--primary"&gt;Part 5 — Vault →&lt;/a&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The full series
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why we are building this&lt;/li&gt;
&lt;li&gt;The server&lt;/li&gt;
&lt;li&gt;Traefik and SSL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first Supabase instance&lt;/strong&gt;, you are here&lt;/li&gt;
&lt;li&gt;Vault&lt;/li&gt;
&lt;li&gt;Two instances&lt;/li&gt;
&lt;li&gt;Security and the load test&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>supabase</category>
      <category>docker</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
