Author SHA1 Message Date
Igor K.andClaude Sonnet 5.5 b1ae4f1daa fix: make the database disk image 6 GB instead of 2 GB
- VOLUMES["db"] goes from 2048 to 6144 MiB, so every new instance gets a 6 GB database image.
  Instance 4 filled its 2 GB image with analytics logs on 2026-10-14: Postgres crashed and could
  not finish crash recovery without free space. Instance 2 had already been grown to 6 GB by hand.
- Measured on instance 2: about 1.2 GB of logs a day under browser-test traffic, kept for
  LOG_RETENTION (2 days), plus up to 1 GB of write-ahead log. That peaks near 4.5 GB.
- Only instances created after this change get the larger image; existing ones keep their size.
- README disk line, the two unit tests that assert the db volume size, and the version
  (1.6.3 -> 1.6.4 in VERSION, supaspin and README.md) are updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 08:46:50 -04:00
4 changed files with 17 additions and 21 deletions
+5 -5
View File
@@ -1,6 +1,6 @@
# supaspin # supaspin
Version 1.6.5 Version 1.6.4
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 6 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.4
+4 -7
View File
@@ -39,7 +39,7 @@ import tty
import yaml import yaml
VERSION = "1.6.5" VERSION = "1.6.4"
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"
@@ -56,9 +56,7 @@ CERTS_NS = "supaspin-certs"
# The db image is 6 GB because of the analytics logs: browser-test traffic wrote about 1.2 GB of # The db image is 6 GB because of the analytics logs: browser-test traffic wrote about 1.2 GB of
# logs a day on instance 2 (2026-10-14), the nightly cleanup keeps LOG_RETENTION of them, and the # 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. # 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 VOLUMES = {"db": 6144, "storage": 1024, "functions": 256,
# 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 +74,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 +84,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"], "6144Mi")
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"], "6144Mi")
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"])