DEV Community

Cover image for Partie 4 — La première instance Supabase
Dinh Doan Van Bien
Dinh Doan Van Bien

Posted on • Edited on • Originally published at supabase.voieduco.de

Partie 4 — La première instance Supabase

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

Version française de Part 4 — The first Supabase instance.

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.


Ce qu'est un projet Supabase

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 :

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)
Enter fullscreen mode Exit fullscreen mode

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.


Les secrets à générer

Avant de rédiger le fichier compose, on génère ces valeurs :

# Postgres password
openssl rand -hex 16

# JWT secret (must be at least 32 characters)
openssl rand -hex 32

# For Studio's schema browser
openssl rand -hex 16   # PG_META_CRYPTO_KEY
Enter fullscreen mode Exit fullscreen mode

Les clés anon et service_role sont des JWT standard signés avec votre secret JWT. Ce script les génère :

JWT_SECRET="your-jwt-secret-here"

# Expiry: year 2035 (Unix timestamp)
EXPIRY=2051222400

python3 - << EOF
import json, hmac, hashlib, base64

secret = "$JWT_SECRET"

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": $EXPIRY
    })
    msg = f"{header}.{payload}".encode()
    sig = b64(hmac.new(secret.encode(), msg, hashlib.sha256).digest())
    print(f"{role}: {header}.{payload}.{sig}")
EOF
Enter fullscreen mode Exit fullscreen mode

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

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


Le docker-compose.yml

Voici la définition complète du stack. Je détaille chaque point inattendu dans les sections qui suivent.

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 supabase.com/docs/guides/self-hosting. Supabase y maintient une combinaison de versions testée et stable.

version: '3.8'

networks:
  internal:
    driver: overlay
  traefik_default:
    external: true
    name: traefik_default

volumes:
  db_data:
  storage_data:

services:

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

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

  rest:
    image: ghcr.io/supabase/postgrest:v12 # use latest stable v12
    depends_on:
      - db
    environment:
      PGRST_DB_URI: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres
      PGRST_DB_SCHEMA: public,storage,graphql_public
      PGRST_DB_ANON_ROLE: anon
      PGRST_JWT_SECRET: ${JWT_SECRET}
      PGRST_DB_USE_LEGACY_GUCS: 'false'
    networks:
      - internal
    deploy:
      resources:
        limits:
          memory: 256m
          cpus: '0.5'

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

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

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

  meta:
    image: supabase/postgres-meta:v0      # use latest stable v0
    depends_on:
      - db
    environment:
      PG_META_PORT: 8080
      PG_META_DB_HOST: db
      PG_META_DB_PORT: 5432
      PG_META_DB_NAME: postgres
      PG_META_DB_USER: supabase_admin
      PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
      PG_META_DB_SSL_MODE: disable
      PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
    healthcheck:
      disable: true
    networks:
      - internal
    deploy:
      resources:
        limits:
          memory: 256m
          cpus: '0.25'

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

Remplacez YOUR_HASHED_CREDENTIALS par un hash bcrypt de votre mot de passe. Installez l'outil et générez le hash directement sur le serveur :

apt install apache2-utils -y
htpasswd -nB admin
# New password:
# Re-type new password:
# admin:$2y$05$...
Enter fullscreen mode Exit fullscreen mode

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


kong.yml : la configuration de l'API gateway

Le fichier compose monte en bind /root/supabase-vps-cluster/instances/project1/kong.yml 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.

Créez-le à cet emplacement sur le serveur :

_format_version: '2.1'
_transform: true

consumers:
  - username: anon
    keyauth_credentials:
      - key: YOUR_SUPABASE_ANON_KEY
  - username: service_role
    keyauth_credentials:
      - key: YOUR_SUPABASE_SERVICE_ROLE_KEY

acls:
  - consumer: anon
    group: anon
  - consumer: service_role
    group: admin

services:
  - name: auth-v1-open
    url: http://auth:9999/verify
    routes:
      - name: auth-v1-open
        strip_path: true
        paths:
          - /auth/v1/verify
    plugins:
      - name: cors

  - name: auth-v1-open-callback
    url: http://auth:9999/callback
    routes:
      - name: auth-v1-open-callback
        strip_path: true
        paths:
          - /auth/v1/callback
    plugins:
      - name: cors

  - name: auth-v1
    url: http://auth:9999/
    routes:
      - name: auth-v1-all
        strip_path: true
        paths:
          - /auth/v1/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: false
      - name: acl
        config:
          hide_groups_header: true
          allow:
            - admin
            - anon
      - name: rate-limiting
        config:
          minute: 30
          policy: local
          limit_by: ip

  - name: rest-v1
    url: http://rest:3000/
    routes:
      - name: rest-v1-all
        strip_path: true
        paths:
          - /rest/v1/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: true
      - name: acl
        config:
          hide_groups_header: true
          allow:
            - admin
            - anon

  - name: realtime-v1-ws
    url: http://realtime:4000/socket
    protocol: ws
    routes:
      - name: realtime-v1-ws
        strip_path: true
        paths:
          - /realtime/v1/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: false
      - name: acl
        config:
          hide_groups_header: true
          allow:
            - admin
            - anon

  - name: storage-v1
    url: http://storage:5000/
    routes:
      - name: storage-v1-all
        strip_path: true
        paths:
          - /storage/v1/
    plugins:
      - name: cors
      - name: key-auth
        config:
          hide_credentials: true
      - name: acl
        config:
          hide_groups_header: true
          allow:
            - admin
            - anon
Enter fullscreen mode Exit fullscreen mode

Quelques points à noter. Les routes auth-v1-open (/verify, /callback) 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.

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

Après toute modification de ce fichier, Kong ne la prend pas en compte automatiquement. Un redémarrage forcé s'impose :

docker service update --force project1_kong
Enter fullscreen mode Exit fullscreen mode

Point inattendu 1 : les limites mémoire ne sont pas optionnelles

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.

Les valeurs auxquelles je suis arrivé après ajustement :

Service Limite mémoire Raison
db 1 Go Cache de buffers Postgres
kong 512 Mo Plus que prévu, Kong met la config en cache
realtime 512 Mo La VM Erlang/BEAM consomme ~200 Mo au repos
studio 512 Mo Rendu côté serveur Next.js
auth 256 Mo GoTrue
rest 256 Mo PostgREST
storage 256 Mo API Storage
meta 256 Mo postgres-meta

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.


Point inattendu 2 : Studio a besoin de trois variables non évidentes

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 :

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

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.


Point inattendu 3 : la vérification d'état de Studio le tue

L'image supabase/studio 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.

La solution : la désactiver.

healthcheck:
  disable: true
Enter fullscreen mode Exit fullscreen mode

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


Point inattendu 4 : on ne peut pas figer GOTRUE_MAILER_AUTOCONFIRM en dur

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 :

GOTRUE_MAILER_AUTOCONFIRM: 'false'
Enter fullscreen mode Exit fullscreen mode

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

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

La correction : utiliser la substitution de variable.

GOTRUE_MAILER_AUTOCONFIRM: ${GOTRUE_MAILER_AUTOCONFIRM:-false}
Enter fullscreen mode Exit fullscreen mode

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


Point inattendu 5 : DB_ENC_KEY doit faire exactement 16 octets

Realtime utilise le chiffrement AES-128. AES-128 exige une clé de 16 octets. J'ai généré une clé avec openssl rand -hex 32, 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 ?

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 openssl rand -hex 32 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".

La valeur par défaut officielle pour Realtime en auto-hébergement est la chaîne littérale supabaserealtime. 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.


Point inattendu 6 : le schéma _realtime

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

Nous faisons la même chose : un petit fichier SQL monté dans docker-entrypoint-initdb.d/ 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 init-realtime.sh s'occupe de tout :

bash scripts/init-realtime.sh project1
Enter fullscreen mode Exit fullscreen mode

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


Point inattendu 7 : API_EXTERNAL_URL doit pointer vers Kong

API_EXTERNAL_URL 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.

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 :

API_EXTERNAL_URL=https://kong.project1.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Pointer vers PostgREST contourne Kong entièrement, ce qui casse l'authentification.


À propos des tags d'image GoTrue


⚠️ Évitez les release candidates GoTrue. :latest 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 page des releases GoTrue et épinglez le dernier tag stable.

Déploiement

On crée le fichier .env (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 .env littérales :

openssl rand -hex 64   # copy this as SECRET_KEY_BASE
openssl rand -hex 16   # copy this as PG_META_CRYPTO_KEY
Enter fullscreen mode Exit fullscreen mode

Ensuite, on crée le fichier avec les vraies valeurs :

# instances/project1/.env
POSTGRES_PASSWORD=<generated above>
JWT_SECRET=<generated above>
SUPABASE_ANON_KEY=<anon jwt from the script>
SUPABASE_SERVICE_ROLE_KEY=<service_role jwt from the script>
API_EXTERNAL_URL=https://kong.project1.yourdomain.com
GOTRUE_EXTERNAL_URL=https://kong.project1.yourdomain.com
SITE_URL=https://kong.project1.yourdomain.com
GOTRUE_MAILER_AUTOCONFIRM=false
DB_ENC_KEY=supabaserealtime
SECRET_KEY_BASE=<paste the 128-char hex string>
PG_META_CRYPTO_KEY=<paste the 32-char hex string>
Enter fullscreen mode Exit fullscreen mode

Déploiement :

set -a && source instances/project1/.env && set +a
docker stack deploy -c instances/project1/docker-compose.yml project1
Enter fullscreen mode Exit fullscreen mode

Vérification que tous les services démarrent :

docker service ls | grep project1
Enter fullscreen mode Exit fullscreen mode

Les huit services doivent afficher 1/1 replica en l'espace d'une à deux minutes. Si l'un affiche 0/1, consultez les logs :

docker service logs --tail 50 project1_auth
Enter fullscreen mode Exit fullscreen mode

Initialisation de Realtime :

bash scripts/init-realtime.sh project1
Enter fullscreen mode Exit fullscreen mode

Test de l'endpoint API :

curl -s https://kong.project1.yourdomain.com/health
# {"status":"healthy"}
Enter fullscreen mode Exit fullscreen mode

Bilan

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.

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é.

Partie 5 — Vault →


La série complète

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

Top comments (0)