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
igor da8f02310a docs: bring the README in line with the last commits, release 1.6.3 (#14) 2026-10-09 17:46:54 +00:00
igor ccc49ca7d1 fix: keep the nginx ingress annotations when the ingress controller is traefik (#13) 2026-10-09 17:43:21 +00:00
4 changed files with 22 additions and 12 deletions
+14 -7
View File
@@ -1,6 +1,6 @@
# supaspin
Version 1.6.2
Version 1.6.4
Create, list, show and delete throwaway [Supabase](https://supabase.com) instances on a
single-node [microk8s](https://microk8s.io) server, from a terminal menu.
@@ -16,7 +16,7 @@ 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 |
| 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 |
| Disk | fixed-size ext4 image files, mounted at boot: **db 2 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 **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 |
| Secrets | generated fresh per instance: JWT secret, anon and service keys, Postgres password, Studio password, and the internal keys |
@@ -33,7 +33,9 @@ of instances at once. Numbers are reused: create picks the lowest free one.
- A Debian/Ubuntu server with **microk8s** (or any Kubernetes with `kubectl` and `helm` on the
path), with an ingress controller installed: **ingress-nginx** or **Traefik** (the microk8s
`ingress` addon is either, depending on the version).
`ingress` addon is either, depending on the version). With **Traefik**, its CRDs must be present
on the cluster — they come with the Traefik installation itself; supaspin creates `Middleware`
objects and does not install cluster-wide types.
- With **Let's Encrypt** enabled: **cert-manager** installed, DNS `A` records for every hostname
slot pointing at the server, and ports 80 and 443 forwarded to it (Let's Encrypt fetches a test
file over port 80 to issue each certificate). Without it: no cert-manager, no public DNS — each
@@ -77,7 +79,7 @@ sudo supaspin
| Server LAN IP | address shown for the Postgres and Studio ports (detected) |
| Port bind | `lan` = LAN IP only (default; keeps Postgres and the login-less Studio off a public interface), `all` = every interface incl. localhost (for a box behind NAT running its own apps), `local` = localhost only |
| Ingress class | usually `public` on microk8s |
| Ingress controller | `traefik` or `nginx`; Traefik instances get a Middleware that caps uploads at 50 MiB, nginx gets the equivalent annotations |
| Ingress controller | `traefik` or `nginx`; every instance's Ingress carries the nginx annotations (50 MiB upload cap, long timeouts) and, in Traefik mode, additionally points at a `Middleware` object with the same 50 MiB cap - so the cap is enforced no matter which of the two actually routes the hostname |
| Let's Encrypt | `y` fetches a real certificate per hostname (needs public DNS, ports 80/443, cert-manager); `n` gives every instance a self-signed certificate and creates no cert-manager objects |
| Let's Encrypt e-mail | for certificate expiry notices (asked only when Let's Encrypt is on) |
| Hostname slots | comma-separated; a name with a number in it keys that slot (`supa4.didgibot.com` = slot 4, so instance 4 gets it with ports 5444/3004), a plain name takes its position in the list |
@@ -115,12 +117,12 @@ sudo supaspin env N # the full .env (contains secrets)
sudo supaspin tunnel N # the tunnel commands, step by step
sudo supaspin push-env N TARGET [--port P] [--key FILE] [--dry-run] # see "Copying an instance's values"
sudo supaspin delete N [--yes]
sudo supaspin upgrade [N] # apply this version's memory caps to running instances
sudo supaspin upgrade [N] # apply this version's memory caps and log retention to running instances
sudo supaspin suspend N # stop an instance, keep its data (see "Suspending an instance")
sudo supaspin resume N # start it again
sudo supaspin mail # change the saved default mail settings
sudo supaspin setup # change settings
supaspin --version # prints "supaspin 1.3.1"
supaspin --version # prints "supaspin X.Y.Z"
```
### Connecting your app
@@ -300,7 +302,9 @@ every delivery with an error, and vector was killed against its 1024 MiB ceiling
minutes with the undeliverable backlog. The fix is log retention: every instance gets a pg_cron
job (set up on create, upgrade and resume) that deletes analytics logs older than **2 days** every
night. pg_cron in this image only runs jobs from the `postgres` database, so the job lives there
and reaches the log tables in `_supabase` over a local dblink connection. That bounds both the
and reaches the log tables in `_supabase` over a local dblink connection. The dblink extension
and the helper function live in the `extensions` schema, where the app's database owner has no
rights, so instance code can never call them. That bounds both the
db image and vector's backlog, so vector is back to the 512 MiB ceiling the production supabase
charts carry.
@@ -317,6 +321,9 @@ pods are replaced service by service and nothing else about the instance changes
- **Certificate stays pending.** The hostname's DNS record must point at the server and port 80 must
reach the ingress: `kubectl get certificate,order,challenge -n supaspin-N`.
- **"no free hostname slot".** Delete an instance or add a slot in Settings (and its DNS record).
- **Create failed** with `no matches for kind "Middleware" in version "traefik.io/v1alpha1"`. The
config says Traefik but the cluster has no Traefik CRDs: either install Traefik (its CRDs come
with it) or set the ingress controller to `nginx` in Settings.
- **Create failed.** `sudo supaspin delete N`, then create again.
- **Port already in use.** Postgres and Studio ports are fixed per number (`5440+N`, `3000+N`);
another program is using it. Free the port or use another instance number.
+1 -1
View File
@@ -1 +1 @@
1.6.2
1.6.4
+5 -2
View File
@@ -39,7 +39,7 @@ import tty
import yaml
VERSION = "1.6.2"
VERSION = "1.6.4"
CONFIG_PATH = os.environ.get("SUPASPIN_CONFIG", "/etc/supaspin/config.yaml")
STATE_DIR = os.environ.get("SUPASPIN_STATE", "/var/lib/supaspin")
CHART_REPO = "https://supabase-community.github.io/supabase-kubernetes"
@@ -53,7 +53,10 @@ CERTS_NS = "supaspin-certs"
# 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).
VOLUMES = {"db": 2048, "storage": 1024, "functions": 256,
# 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
# write-ahead log may take up to 1 GB on top. That peaks near 4.5 GB; 2 GB filled up on instance 4.
VOLUMES = {"db": 6144, "storage": 1024, "functions": 256,
"snippets": 256, "deno": 256, "pgsodium": 256}
# 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.
+2 -2
View File
@@ -143,7 +143,7 @@ class Values(unittest.TestCase):
def test_volume_caps(self):
p = self.v["persistence"]
self.assertEqual(p["db"]["size"], "2048Mi")
self.assertEqual(p["db"]["size"], "6144Mi")
self.assertEqual(p["storage"]["size"], "1024Mi")
for vol in ("functions", "snippets", "deno", "pgsodium"):
self.assertEqual(p[vol]["size"], "256Mi")
@@ -478,7 +478,7 @@ class Manifests(unittest.TestCase):
pv = sp.pv_manifest(CFG, 2, "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"]["capacity"]["storage"], "2048Mi")
self.assertEqual(pv["spec"]["capacity"]["storage"], "6144Mi")
self.assertEqual(pv["spec"]["persistentVolumeReclaimPolicy"], "Retain")
self.assertEqual(pv["spec"]["nodeAffinity"]["required"]["nodeSelectorTerms"][0]
["matchExpressions"][0]["values"], ["node1"])