Author SHA1 Message Date
Igor eca8fe423b docs: bring the README in line with the last commits, release 1.6.3
The Traefik/nginx annotations are no longer either/or since 1.6.2: document that both sets are always on the Ingress, that Traefik mode needs the Traefik CRDs on the cluster (with the exact create error when they are missing), that 'supaspin upgrade' also applies log retention, and that the log-retention dblink helper lives in the extensions schema on purpose. Also stops hardcoding a version number in the --version example.
2026-10-09 20:46:27 +03:00
4 changed files with 17 additions and 24 deletions
+5 -5
View File
@@ -1,6 +1,6 @@
# supaspin # supaspin
Version 1.6.5 Version 1.6.3
Create, list, show and delete throwaway [Supabase](https://supabase.com) instances on a Create, list, show and delete throwaway [Supabase](https://supabase.com) instances on a
single-node [microk8s](https://microk8s.io) server, from a terminal menu. single-node [microk8s](https://microk8s.io) server, from a terminal menu.
@@ -16,8 +16,8 @@ endangering them. Nothing in it is specific to any one app.
| Web API | `https://<slot hostname>` (auth, rest, graphql, realtime, storage, functions); the certificate is from Let's Encrypt when that is enabled, otherwise self-signed | | Web API | `https://<slot hostname>` (auth, rest, graphql, realtime, storage, functions); the certificate is from Let's Encrypt when that is enabled, otherwise self-signed |
| Postgres | `<LAN IP>:544N` (instance 1 → 5441, 2 → 5442, …); which addresses listen is set by **Port bind** | | Postgres | `<LAN IP>:544N` (instance 1 → 5441, 2 → 5442, …); which addresses listen is set by **Port bind** |
| Studio (admin UI) | `http://<LAN IP>:300N` (3001, 3002, …), not on the public hostname | | Studio (admin UI) | `http://<LAN IP>:300N` (3001, 3002, …), not on the public hostname |
| Disk | fixed-size ext4 image files, mounted at boot: **db 12 GB, storage 1 GB, every other volume 256 MB** | | Disk | fixed-size ext4 image files, mounted at boot: **db 2 GB, storage 1 GB, every other volume 256 MB** |
| Memory | limits add up to **11 GiB** per instance, requests to 2.375 GiB (table below) | | Memory | limits add up to **9 GiB** per instance, requests to 1.875 GiB (table below) |
| Priority | pods run at priority -100 (`supaspin-low`); when the server runs short of memory they are evicted first and never evict anyone else | | Priority | pods run at priority -100 (`supaspin-low`); when the server runs short of memory they are evicted first and never evict anyone else |
| Secrets | generated fresh per instance: JWT secret, anon and service keys, Postgres password, Studio password, and the internal keys | | Secrets | generated fresh per instance: JWT secret, anon and service keys, Postgres password, Studio password, and the internal keys |
| Uploads | stored in the instance's own storage image (no MinIO); deleted with the instance | | Uploads | stored in the instance's own storage image (no MinIO); deleted with the instance |
@@ -270,13 +270,13 @@ stores the new memory limits and leaves it asleep; they take effect when you res
| Service | Request | Limit | Highest use measured | | Service | Request | Limit | Highest use measured |
|---|---|---|---| |---|---|---|---|
| db | 1024 MiB | 4096 MiB | 273 MiB | | db | 512 MiB | 2048 MiB | 273 MiB |
| analytics | 128 MiB | 1536 MiB | 694 MiB | | analytics | 128 MiB | 1536 MiB | 694 MiB |
| vector | 128 MiB | 512 MiB | 290 MiB | | vector | 128 MiB | 512 MiB | 290 MiB |
| kong | 128 MiB | 768 MiB | 269 MiB | | kong | 128 MiB | 768 MiB | 269 MiB |
| realtime | 128 MiB | 768 MiB | 300 MiB | | realtime | 128 MiB | 768 MiB | 300 MiB |
| auth, functions, imgproxy, meta, rest, storage, studio | 128 MiB each | 512 MiB each | 203 MiB (studio) | | auth, functions, imgproxy, meta, rest, storage, studio | 128 MiB each | 512 MiB each | 203 MiB (studio) |
| **Total** | **2.375 GiB** | **11 GiB** | | | **Total** | **1.875 GiB** | **9 GiB** | |
The request is what the scheduler sets aside for a service and can never be given to anything else. The request is what the scheduler sets aside for a service and can never be given to anything else.
The limit is the ceiling the kernel kills the container at. They do different jobs here. The limit is the ceiling the kernel kills the container at. They do different jobs here.
+1 -1
View File
@@ -1 +1 @@
1.6.5 1.6.3
+4 -10
View File
@@ -39,7 +39,7 @@ import tty
import yaml import yaml
VERSION = "1.6.5" VERSION = "1.6.3"
CONFIG_PATH = os.environ.get("SUPASPIN_CONFIG", "/etc/supaspin/config.yaml") CONFIG_PATH = os.environ.get("SUPASPIN_CONFIG", "/etc/supaspin/config.yaml")
STATE_DIR = os.environ.get("SUPASPIN_STATE", "/var/lib/supaspin") STATE_DIR = os.environ.get("SUPASPIN_STATE", "/var/lib/supaspin")
CHART_REPO = "https://supabase-community.github.io/supabase-kubernetes" CHART_REPO = "https://supabase-community.github.io/supabase-kubernetes"
@@ -53,12 +53,7 @@ CERTS_NS = "supaspin-certs"
# Image cap per volume, in MiB. The chart's separate imgproxy volume is not used: imgproxy has to # Image cap per volume, in MiB. The chart's separate imgproxy volume is not used: imgproxy has to
# read the uploaded files, so it mounts the storage volume instead (see instance_values). # read the uploaded files, so it mounts the storage volume instead (see instance_values).
# The db image is 6 GB because of the analytics logs: browser-test traffic wrote about 1.2 GB of VOLUMES = {"db": 2048, "storage": 1024, "functions": 256,
# logs a day on instance 2 (2026-10-14), the nightly cleanup keeps LOG_RETENTION of them, and the
# write-ahead log may take up to 1 GB on top. That peaks near 4.5 GB; 2 GB filled up on instance 4.
# Doubled to 12 GB on 2026-11-03 at Igor's request: instance 4 had reached 4.7 GB of its 6 GB and
# instances kept having to be recreated when the image filled up.
VOLUMES = {"db": 12288, "storage": 1024, "functions": 256,
"snippets": 256, "deno": 256, "pgsodium": 256} "snippets": 256, "deno": 256, "pgsodium": 256}
# The user Postgres runs as inside supabase/postgres 17.6.1.136 (`id postgres` in the image). # The user Postgres runs as inside supabase/postgres 17.6.1.136 (`id postgres` in the image).
# Production's older image used 2000; do not copy that number. # Production's older image used 2000; do not copy that number.
@@ -76,8 +71,7 @@ DB_UID, DB_GID = 100, 101
# realtime 269 / 287 / 300 MiB against 384 # realtime 269 / 287 / 300 MiB against 384
# kong 260 / 269 / 265 MiB against 512 # kong 260 / 269 / 265 MiB against 512
# studio 203, storage 187, rest 162 MiB against 256 # studio 203, storage 187, rest 162 MiB against 256
# db 163 / 175 / 273 MiB against 2048 - doubled to 1024 / 4096 on 2026-11-03 at Igor's # db 163 / 175 / 273 MiB against 2048 - left alone, it has room
# request, together with the db image, so a larger database has room to work in
# On 2026-10-06 instance 3 filled its 2 GB database image with logs: analytics (Logflare) never # On 2026-10-06 instance 3 filled its 2 GB database image with logs: analytics (Logflare) never
# deletes what it stored, the db died of it, and vector's undelivered backlog was killed against # deletes what it stored, the db died of it, and vector's undelivered backlog was killed against
# the 1024 MiB ceiling every few minutes. The fix is setup_log_retention: a pg_cron job deletes # the 1024 MiB ceiling every few minutes. The fix is setup_log_retention: a pg_cron job deletes
@@ -87,7 +81,7 @@ DB_UID, DB_GID = 100, 101
# would take the memory away from everything else on the node straight away. # would take the memory away from everything else on the node straight away.
SERVICES = ["db", "analytics", "kong", "auth", "functions", "imgproxy", "meta", SERVICES = ["db", "analytics", "kong", "auth", "functions", "imgproxy", "meta",
"realtime", "rest", "storage", "studio", "vector"] "realtime", "rest", "storage", "studio", "vector"]
MEMORY = {"db": (1024, 4096), "analytics": (128, 1536), "kong": (128, 768), "realtime": (128, 768), MEMORY = {"db": (512, 2048), "analytics": (128, 1536), "kong": (128, 768), "realtime": (128, 768),
"vector": (128, 512)} "vector": (128, 512)}
DEFAULT_MEMORY = (128, 512) DEFAULT_MEMORY = (128, 512)
+7 -8
View File
@@ -109,10 +109,10 @@ class Values(unittest.TestCase):
def test_memory_caps_add_up(self): def test_memory_caps_add_up(self):
req, lim = sp.total_memory_mib() req, lim = sp.total_memory_mib()
# db 4096, analytics 1536, kong 768, realtime 768, vector 512, the other seven 512 each # db 2048, analytics 1536, kong 768, realtime 768, vector 512, the other seven 512 each
self.assertEqual(lim, 4096 + 1536 + 768 + 768 + 512 + 7 * 512) self.assertEqual(lim, 2048 + 1536 + 768 + 768 + 512 + 7 * 512)
self.assertEqual(lim / 1024, 11.0) self.assertEqual(lim / 1024, 9.0)
self.assertEqual(req, 1024 + 11 * 128) # 2.375 GiB self.assertEqual(req, 512 + 11 * 128) # 1.875 GiB
# Measured on three running instances on 2026-09-22; the ceiling has to stay clear of the # Measured on three running instances on 2026-09-22; the ceiling has to stay clear of the
# highest use, or the kernel kills the container in a loop the way it did on instance 1. # highest use, or the kernel kills the container in a loop the way it did on instance 1.
self.assertGreaterEqual(sp.MEMORY["analytics"][1], 1536) # used 694 MiB, died at 768 self.assertGreaterEqual(sp.MEMORY["analytics"][1], 1536) # used 694 MiB, died at 768
@@ -125,9 +125,8 @@ class Values(unittest.TestCase):
self.assertGreaterEqual(sp.DEFAULT_MEMORY[1], 512) # studio used 203 against 256 self.assertGreaterEqual(sp.DEFAULT_MEMORY[1], 512) # studio used 203 against 256
# The requests stay where they were: the node is shared with production and reserving # The requests stay where they were: the node is shared with production and reserving
# what the instances really use would leave production nothing to schedule into. # what the instances really use would leave production nothing to schedule into.
# Only db's went up, from 512 to 1024 MiB, with its limit (2026-11-03).
self.assertTrue(all(m[0] == 128 for s, m in sp.MEMORY.items() if s != "db")) self.assertTrue(all(m[0] == 128 for s, m in sp.MEMORY.items() if s != "db"))
self.assertEqual(req / 1024, 2.375) self.assertEqual(req / 1024, 1.875)
for svc in sp.SERVICES: for svc in sp.SERVICES:
self.assertEqual(self.v["deployment"][svc]["priorityClassName"], "supaspin-low") self.assertEqual(self.v["deployment"][svc]["priorityClassName"], "supaspin-low")
self.assertIn("memory", self.v["deployment"][svc]["resources"]["limits"]) self.assertIn("memory", self.v["deployment"][svc]["resources"]["limits"])
@@ -144,7 +143,7 @@ class Values(unittest.TestCase):
def test_volume_caps(self): def test_volume_caps(self):
p = self.v["persistence"] p = self.v["persistence"]
self.assertEqual(p["db"]["size"], "12288Mi") self.assertEqual(p["db"]["size"], "2048Mi")
self.assertEqual(p["storage"]["size"], "1024Mi") self.assertEqual(p["storage"]["size"], "1024Mi")
for vol in ("functions", "snippets", "deno", "pgsodium"): for vol in ("functions", "snippets", "deno", "pgsodium"):
self.assertEqual(p[vol]["size"], "256Mi") self.assertEqual(p[vol]["size"], "256Mi")
@@ -479,7 +478,7 @@ class Manifests(unittest.TestCase):
pv = sp.pv_manifest(CFG, 2, "db") pv = sp.pv_manifest(CFG, 2, "db")
self.assertEqual(pv["spec"]["claimRef"], {"namespace": "supaspin-2", "name": "supa2-supabase-db"}) self.assertEqual(pv["spec"]["claimRef"], {"namespace": "supaspin-2", "name": "supa2-supabase-db"})
self.assertEqual(pv["spec"]["local"]["path"], "/data/supabase/2/db/data") self.assertEqual(pv["spec"]["local"]["path"], "/data/supabase/2/db/data")
self.assertEqual(pv["spec"]["capacity"]["storage"], "12288Mi") self.assertEqual(pv["spec"]["capacity"]["storage"], "2048Mi")
self.assertEqual(pv["spec"]["persistentVolumeReclaimPolicy"], "Retain") self.assertEqual(pv["spec"]["persistentVolumeReclaimPolicy"], "Retain")
self.assertEqual(pv["spec"]["nodeAffinity"]["required"]["nodeSelectorTerms"][0] self.assertEqual(pv["spec"]["nodeAffinity"]["required"]["nodeSelectorTerms"][0]
["matchExpressions"][0]["values"], ["node1"]) ["matchExpressions"][0]["values"], ["node1"])