feat: supaspin, on-demand throwaway Supabase instances on microk8s
Adds a terminal app that creates, lists, shows and deletes numbered Supabase instances on a single-node microk8s server, for testing and staging copies of any app. Nothing in it is specific to one app. - supaspin: single-file Python app (menu and commands: create, list, show, env, tunnel, delete, mail, setup). Instance N gets namespace supaspin-N, hostname slot N, Postgres on <LAN IP>:544N and Studio on <LAN IP>:300N. Lowest free number is reused; a refused create says why. - Disk: fixed-size ext4 image files, loop-mounted from /etc/fstab. db 2 GB, storage 1 GB, functions/snippets/deno/pgsodium 256 MB each. imgproxy mounts the storage volume (the chart's own imgproxy volume cannot see uploads, so image resizing failed). Empty mount points are made read-only until mounted so a pod started early after a reboot cannot initialise a database there. - Memory: limits 5.625 GiB, requests 1.875 GiB per instance, all 12 services. Half of a production setup, except analytics (768 MiB) and realtime (384 MiB), which were killed for running out of memory at half of it. - Priority class supaspin-low (-100, never preempts) on every instance pod. - Public HTTPS through the existing nginx ingress with Let's Encrypt. The Studio and pg-meta routes are cut out of the Kong gateway config by a post-renderer, which also pins the db and studio pods to fixed host ports. - Certificates: delete keeps the TLS secret (with its cert-manager issuer annotations) in namespace supaspin-certs and create restores it, so create/delete cycles do not hit Let's Encrypt's 5-per-week limit. - Secrets generated per instance: JWT secret, anon and service keys, Postgres and Studio passwords, realtime/meta/logflare keys. The rendered values file is removed after install; secrets are never written through kubectl apply. - Instance .env (mode 600) with the usual Supabase variable names, plus an SSH tunnel command and DATABASE_URL help. - Delete is idempotent and finds the real mounts instead of trusting a list, so half-created and older instances clean up completely. A failed create offers to run it. - Mail settings: saved defaults changeable from the menu; create asks to use them or enter different ones for that instance. - install.sh: installs the app; accepts a Gitea token or a local checkout because git.didgibot.com requires sign-in even for public repositories. uninstall.sh: removes the app and, after confirmation, every instance. - tests: 30 unittest cases (numbering, ports, JWT signing, values, .env, fstab handling, post-renderer, certificate secret handling). - README: install, first-run setup, menu, tunnel, troubleshooting, uninstall. Pinned chart: supabase-community/supabase-kubernetes 0.8.0 (Postgres 17.6.1.136). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
11802ce8cd
commit
cfbc4f1612
@@ -0,0 +1,88 @@
|
||||
# Generic Helm values for one supaspin instance (upstream supabase-kubernetes chart).
|
||||
# Every dollar-brace placeholder below (for example the ANON_KEY one) is replaced by supaspin
|
||||
# with a quoted string before Helm sees this file.
|
||||
# Memory limits, priority class and volume sizes are added by supaspin itself (see
|
||||
# instance_values() in the supaspin script) so they stay in one place next to the caps.
|
||||
secret:
|
||||
jwt:
|
||||
anonKey: ${ANON_KEY}
|
||||
serviceKey: ${SERVICE_KEY}
|
||||
secret: ${JWT_SECRET}
|
||||
db:
|
||||
password: ${DB_PASSWORD}
|
||||
database: postgres
|
||||
analytics:
|
||||
publicAccessToken: ${LOGFLARE_PUBLIC}
|
||||
privateAccessToken: ${LOGFLARE_PRIVATE}
|
||||
dashboard:
|
||||
username: ${DASHBOARD_USERNAME}
|
||||
password: ${DASHBOARD_PASSWORD}
|
||||
s3:
|
||||
keyId: ${S3_KEY_ID}
|
||||
accessKey: ${S3_ACCESS_KEY}
|
||||
realtime:
|
||||
secretKeyBase: ${REALTIME_SECRET_KEY_BASE}
|
||||
dbEncKey: ${REALTIME_DB_ENC_KEY}
|
||||
meta:
|
||||
cryptoKey: ${META_CRYPTO_KEY}
|
||||
smtp:
|
||||
username: ${SMTP_USER}
|
||||
password: ${SMTP_PASS}
|
||||
|
||||
deployment:
|
||||
analytics:
|
||||
enabled: true
|
||||
vector:
|
||||
enabled: true
|
||||
|
||||
environment:
|
||||
auth:
|
||||
- {name: API_EXTERNAL_URL, value: ${PUBLIC_URL}}
|
||||
- {name: GOTRUE_API_HOST, value: "0.0.0.0"}
|
||||
- {name: GOTRUE_API_PORT, value: "9999"}
|
||||
- {name: GOTRUE_SITE_URL, value: ${PUBLIC_URL}}
|
||||
- {name: GOTRUE_URI_ALLOW_LIST, value: "*"}
|
||||
- {name: GOTRUE_DISABLE_SIGNUP, value: "false"}
|
||||
- {name: GOTRUE_JWT_DEFAULT_GROUP_NAME, value: authenticated}
|
||||
- {name: GOTRUE_JWT_ADMIN_ROLES, value: service_role}
|
||||
- {name: GOTRUE_JWT_AUD, value: authenticated}
|
||||
- {name: GOTRUE_JWT_EXP, value: "3600"}
|
||||
- {name: GOTRUE_EXTERNAL_EMAIL_ENABLED, value: "true"}
|
||||
- {name: GOTRUE_MAILER_AUTOCONFIRM, value: "true"}
|
||||
- {name: GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED, value: "false"}
|
||||
- {name: GOTRUE_SMTP_ADMIN_EMAIL, value: ${SMTP_ADMIN_EMAIL}}
|
||||
- {name: GOTRUE_SMTP_HOST, value: ${SMTP_HOST}}
|
||||
- {name: GOTRUE_SMTP_PORT, value: ${SMTP_PORT}}
|
||||
- {name: GOTRUE_EXTERNAL_PHONE_ENABLED, value: "false"}
|
||||
- {name: GOTRUE_SMS_AUTOCONFIRM, value: "false"}
|
||||
- {name: GOTRUE_SMTP_SENDER_NAME, value: ${SMTP_SENDER_NAME}}
|
||||
- {name: GOTRUE_MAILER_URLPATHS_INVITE, value: /auth/v1/verify}
|
||||
- {name: GOTRUE_MAILER_URLPATHS_CONFIRMATION, value: /auth/v1/verify}
|
||||
- {name: GOTRUE_MAILER_URLPATHS_RECOVERY, value: /auth/v1/verify}
|
||||
- {name: GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE, value: /auth/v1/verify}
|
||||
studio:
|
||||
- {name: HOSTNAME, value: "::"}
|
||||
- {name: STUDIO_PORT, value: "3000"}
|
||||
- {name: DEFAULT_ORGANIZATION_NAME, value: Default Organization}
|
||||
- {name: DEFAULT_PROJECT_NAME, value: ${PROJECT_NAME}}
|
||||
- {name: SUPABASE_PUBLIC_URL, value: ${PUBLIC_URL}}
|
||||
- {name: NEXT_ANALYTICS_BACKEND_PROVIDER, value: postgres}
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: ${INGRESS_CLASS}
|
||||
annotations:
|
||||
# The chart default rewrites every path to "/", which breaks all APIs; null removes it.
|
||||
nginx.ingress.kubernetes.io/rewrite-target: null
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 50m
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
cert-manager.io/cluster-issuer: ${CLUSTER_ISSUER}
|
||||
tls:
|
||||
- hosts: [${HOST}]
|
||||
secretName: ${TLS_SECRET}
|
||||
hosts:
|
||||
- host: ${HOST}
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
Reference in New Issue
Block a user