DEV Community

Dinh Doan Van Bien
Dinh Doan Van Bien

Posted on

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 maintenant au déploiement de Supabase. C'est 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 est le seul service accessible depuis internet (via Traefik). Tous les autres 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. Le runtime Erlang/BEAM 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 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 pourquoi. Ce n'est pas évident pour l'utilisateur.


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é. La vérification d'état de Studio échouait 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. Il faut la 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. J'ai mis à jour le fichier .env et redéployé. 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. C'est ainsi que cela aurait dû être dès le départ.


Point inattendu 5 : DB_ENC_KEY doit faire exactement 16 octets

Realtime utilise le chiffrement AES-128-ECB. 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.

Cette série construit un fichier compose de zéro. Ce montage n'est pas présent, donc _realtime n'est jamais créé. Realtime v2.76+ en a besoin pour sa configuration multi-tenant et plante au démarrage sans indiquer clairement ce qui manque.

Exécutez ceci une seule fois après le premier déploiement :

docker exec $(docker ps --filter name=project1_db --format '{{.Names}}' | head -1) \
  psql -U postgres -d postgres \
  -c "CREATE SCHEMA IF NOT EXISTS _realtime;"

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

Ce que fait le script : il crée le schéma _realtime, renomme le tenant que SEED_SELF_HOST crée de realtime-dev en realtime (un décalage de nommage entre la logique de seed et le nom de l'application), puis force le redémarrage du service. Il est idempotent.


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. Cela semblait logique. Mais 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'ordre 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

Où en sommes-nous

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, vous êtes ici
  5. Vault
  6. Deux instances
  7. Sécurité et test de charge

Top comments (0)