--- # Deploy Supabase on OpenShift via the supabase-community Helm chart. # # Prerequisites: # - KUBECONFIG set in environment (or ocp_kubeconfig host var) # - cert-manager installed with a ClusterIssuer matching supabase_tls_cluster_issuer # - A ReadWriteOnce-capable StorageClass set via supabase_storage_class # - Vault variables defined (see meta/argument_specs.yml for the full list) # # Architecture: # - Kong API gateway is the single ingress point (Route: supabase_studio_host) # - Studio dashboard is served through Kong # - DB runs as root — anyuid SCC granted to the db ServiceAccount # - Vector reads /var/log/pods — privileged SCC granted to vector SA # - All secrets are created as K8s Secrets and referenced via secretRef # # OpenShift SCC notes: # - supabase-db ServiceAccount → anyuid SCC (postgres runs as root) # - supabase-vector ServiceAccount → privileged SCC (hostPath to /var/log/pods) # ------------------------------------------------------------------ # Step 0: Resolve secrets from Vault (generate and store if missing) # ------------------------------------------------------------------ - name: Resolve Supabase secrets from Vault ansible.builtin.include_tasks: vault_secrets.yml # ------------------------------------------------------------------ # Step 1: Namespace # ------------------------------------------------------------------ - name: Create Supabase namespace kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Namespace metadata: name: "{{ supabase_namespace }}" # ------------------------------------------------------------------ # Step 2: SCC grants (ClusterRoleBindings before helm so pods start clean) # ------------------------------------------------------------------ - name: Create ClusterRole granting anyuid SCC (supabase db) kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: supabase-db-anyuid-scc rules: - apiGroups: [security.openshift.io] resources: [securitycontextconstraints] verbs: [use] resourceNames: [anyuid] - name: Bind anyuid SCC to supabase db ServiceAccount kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: supabase-db-anyuid-scc subjects: - kind: ServiceAccount name: "{{ supabase_db_fullname }}" namespace: "{{ supabase_namespace }}" roleRef: kind: ClusterRole name: supabase-db-anyuid-scc apiGroup: rbac.authorization.k8s.io - name: Create ClusterRole granting anyuid SCC (supabase kong) kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: supabase-kong-anyuid-scc rules: - apiGroups: [security.openshift.io] resources: [securitycontextconstraints] verbs: [use] resourceNames: [anyuid] - name: Bind anyuid SCC to supabase kong ServiceAccount kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: supabase-kong-anyuid-scc subjects: - kind: ServiceAccount name: "{{ supabase_kong_fullname }}" namespace: "{{ supabase_namespace }}" roleRef: kind: ClusterRole name: supabase-kong-anyuid-scc apiGroup: rbac.authorization.k8s.io - name: Create ClusterRole granting anyuid SCC (supabase functions) kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: supabase-functions-anyuid-scc rules: - apiGroups: [security.openshift.io] resources: [securitycontextconstraints] verbs: [use] resourceNames: [anyuid] - name: Bind anyuid SCC to supabase functions ServiceAccount kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: supabase-functions-anyuid-scc subjects: - kind: ServiceAccount name: "{{ supabase_functions_fullname }}" namespace: "{{ supabase_namespace }}" roleRef: kind: ClusterRole name: supabase-functions-anyuid-scc apiGroup: rbac.authorization.k8s.io - name: Create ClusterRole granting privileged SCC (supabase vector) kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: supabase-vector-privileged-scc rules: - apiGroups: [security.openshift.io] resources: [securitycontextconstraints] verbs: [use] resourceNames: [privileged] - name: Bind privileged SCC to supabase vector ServiceAccount kubernetes.core.k8s: state: present definition: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: supabase-vector-privileged-scc subjects: - kind: ServiceAccount name: "{{ supabase_vector_fullname }}" namespace: "{{ supabase_namespace }}" roleRef: kind: ClusterRole name: supabase-vector-privileged-scc apiGroup: rbac.authorization.k8s.io # ------------------------------------------------------------------ # Step 3: Kubernetes Secrets (no_log — referenced by helm via secretRef) # ------------------------------------------------------------------ - name: Create JWT secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_jwt }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: anonKey: "{{ __supabase_anon_key }}" serviceKey: "{{ __supabase_service_key }}" secret: "{{ __supabase_jwt_secret }}" no_log: "{{ supabase_no_log }}" - name: Create DB secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_db }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: password: "{{ __supabase_db_password }}" database: "{{ supabase_db_name }}" no_log: "{{ supabase_no_log }}" - name: Create dashboard secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_dashboard }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: username: "{{ __supabase_dashboard_username }}" password: "{{ __supabase_dashboard_password }}" openAiApiKey: "{{ __supabase_openai_api_key }}" no_log: "{{ supabase_no_log }}" - name: Create analytics secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_analytics }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: publicAccessToken: "{{ __supabase_analytics_public_token }}" privateAccessToken: "{{ __supabase_analytics_private_token }}" no_log: "{{ supabase_no_log }}" - name: Create realtime secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_realtime }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: secretKeyBase: "{{ __supabase_realtime_secret_key_base }}" no_log: "{{ supabase_no_log }}" - name: Create meta secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_meta }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: cryptoKey: "{{ __supabase_meta_crypto_key }}" no_log: "{{ supabase_no_log }}" - name: Create SMTP secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: "{{ __supabase_secret_smtp }}" namespace: "{{ supabase_namespace }}" type: Opaque stringData: username: "{{ vault_supabase_smtp_username | default('') }}" # set in host_vars password: "{{ vault_supabase_smtp_password | default('') }}" # set in host_vars no_log: "{{ supabase_no_log }}" # ------------------------------------------------------------------ # Step 4: Optional Docker Hub pull secret # Avoids anonymous rate limiting for docker.io images (kong, vector, etc.) # ------------------------------------------------------------------ - name: Create Docker Hub pull secret kubernetes.core.k8s: state: present definition: apiVersion: v1 kind: Secret metadata: name: supabase-dockerhub namespace: "{{ supabase_namespace }}" type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: >- {{ {'auths': {'https://index.docker.io/v1/': { 'username': supabase_dockerhub_username, 'password': vault_supabase_dockerhub_token, 'auth': (supabase_dockerhub_username + ':' + vault_supabase_dockerhub_token) | b64encode }}} | to_json | b64encode }} no_log: "{{ supabase_no_log }}" when: supabase_dockerhub_enabled | bool # ------------------------------------------------------------------ # Step 5: Helm install / upgrade # ------------------------------------------------------------------ - name: Deploy Supabase via Helm kubernetes.core.helm: release_name: "{{ supabase_release_name }}" chart_ref: supabase chart_repo_url: "{{ supabase_chart_repo_url }}" chart_version: "{{ supabase_chart_version | default(omit, true) }}" release_namespace: "{{ supabase_namespace }}" create_namespace: false wait: false values: # Use predictable names so SCC bindings and Route target can reference them deployment: db: fullnameOverride: "{{ supabase_db_fullname }}" kong: fullnameOverride: "{{ supabase_kong_fullname }}" functions: fullnameOverride: "{{ supabase_functions_fullname }}" vector: fullnameOverride: "{{ supabase_vector_fullname }}" # Secrets via secretRef — keep secret material out of Helm values secret: jwt: secretRef: "{{ __supabase_secret_jwt }}" db: secretRef: "{{ __supabase_secret_db }}" dashboard: secretRef: "{{ __supabase_secret_dashboard }}" analytics: secretRef: "{{ __supabase_secret_analytics }}" realtime: secretRef: "{{ __supabase_secret_realtime }}" meta: secretRef: "{{ __supabase_secret_meta }}" smtp: secretRef: "{{ __supabase_secret_smtp }}" # Storage — all PVCs use the configured StorageClass persistence: db: storageClassName: "{{ supabase_storage_class }}" size: "{{ supabase_db_storage_size }}" functions: storageClassName: "{{ supabase_storage_class }}" size: "{{ supabase_functions_storage_size }}" imgproxy: storageClassName: "{{ supabase_storage_class }}" size: "{{ supabase_imgproxy_storage_size }}" minio: storageClassName: "{{ supabase_storage_class }}" size: "{{ supabase_minio_storage_size }}" # Explicit docker.io registry prefix on all docker.io images. # OpenShift ImageContentSourcePolicy rewrites bare image refs (e.g. kong/kong) # to the configured mirror; supplying the full docker.io/ hostname forces the # pull against the real registry and lets the pull secret take effect. image: analytics: repository: docker.io/supabase/logflare auth: repository: docker.io/supabase/gotrue db: repository: docker.io/supabase/postgres functions: repository: docker.io/supabase/edge-runtime imgproxy: repository: docker.io/darthsim/imgproxy kong: repository: docker.io/kong/kong meta: repository: docker.io/supabase/postgres-meta realtime: repository: docker.io/supabase/realtime rest: repository: docker.io/postgrest/postgrest storage: repository: docker.io/supabase/storage-api studio: repository: docker.io/supabase/studio vector: repository: docker.io/timberio/vector # Docker Hub pull secret (only wired in when enabled) imagePullSecrets: "{{ [{'name': 'supabase-dockerhub'}] if supabase_dockerhub_enabled | bool else [] }}" # Disable chart's nginx ingress — we create an OCP Route below ingress: enabled: false # Studio public URL (used by auth callbacks and the browser) environment: studio: SUPABASE_PUBLIC_URL: "https://{{ supabase_studio_host }}" STUDIO_DEFAULT_ORGANIZATION: "{{ supabase_org_name }}" STUDIO_DEFAULT_PROJECT: "{{ supabase_project_name }}" NEXT_PUBLIC_ENABLE_LOGS: "true" # SMTP auth: environment: GOTRUE_SMTP_HOST: "{{ supabase_smtp_host }}" GOTRUE_SMTP_PORT: "{{ supabase_smtp_port | string }}" GOTRUE_SMTP_ADMIN_EMAIL: "{{ supabase_smtp_admin_email }}" GOTRUE_SMTP_SENDER_NAME: "{{ supabase_smtp_sender_name }}" GOTRUE_MAILER_AUTOCONFIRM: "{{ 'false' if supabase_smtp_enabled else 'true' }}" # ------------------------------------------------------------------ # Step 6: OpenShift Route (edge TLS, cert-manager certificate) # ------------------------------------------------------------------ - name: Create TLS Certificate for Supabase kubernetes.core.k8s: state: present definition: apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: supabase-tls namespace: "{{ supabase_namespace }}" spec: secretName: supabase-tls issuerRef: name: "{{ supabase_tls_cluster_issuer }}" kind: ClusterIssuer dnsNames: - "{{ supabase_studio_host }}" - name: Create OpenShift Route for Supabase (Kong gateway) kubernetes.core.k8s: state: present definition: apiVersion: route.openshift.io/v1 kind: Route metadata: name: supabase namespace: "{{ supabase_namespace }}" annotations: cert-manager.io/cluster-issuer: "{{ supabase_tls_cluster_issuer }}" spec: host: "{{ supabase_studio_host }}" to: kind: Service name: "{{ supabase_kong_fullname }}" weight: 100 port: targetPort: http tls: termination: edge insecureEdgeTerminationPolicy: Redirect certificate: "" key: "" caCertificate: "" # ------------------------------------------------------------------ # Step 7: Wait for Kong deployment to be healthy # ------------------------------------------------------------------ - name: Wait for Kong deployment to be ready kubernetes.core.k8s_info: api_version: apps/v1 kind: Deployment namespace: "{{ supabase_namespace }}" name: "{{ supabase_kong_fullname }}" register: __supabase_kong_deploy until: >- __supabase_kong_deploy.resources | length > 0 and (__supabase_kong_deploy.resources[0].status.readyReplicas | default(0)) >= 1 retries: "{{ __supabase_wait_retries }}" delay: 10 # ------------------------------------------------------------------ # Step 8: Summary # ------------------------------------------------------------------ - name: Display Supabase deployment summary ansible.builtin.debug: msg: - "Supabase deployment complete!" - " Namespace : {{ supabase_namespace }}" - " Studio URL : https://{{ supabase_studio_host }}" - " Kong service : {{ supabase_kong_fullname }}:8000" - " DB PVC : {{ supabase_db_storage_size }} ({{ supabase_storage_class }})" - " SMTP enabled : {{ supabase_smtp_enabled }}" - "" - " Login with vault_supabase_dashboard_username / vault_supabase_dashboard_password"