- 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>
- Traefik is a first-class ingress controller choice: per-instance Middleware
caps uploads at 50 MiB, nginx keeps its proxy-* annotations; picked in setup
- Let's Encrypt is optional: off skips the ClusterIssuer and gives each instance
a 10-year self-signed certificate instead of a Let's Encrypt one
- New port_bind setting: lan (default, LAN IP only), all (LAN + localhost),
local (localhost only)
- Hostname slots are keyed by the digit in the name (supa4 -> slot 4), so the
instance number, ports and hostname stay in step on every machine
- The log-retention job installs dblink and its helper into the extensions
schema instead of public: in public they break the app's
REVOKE ALL ON ALL ROUTINES IN SCHEMA public for the non-superuser postgres
role (42501 permission denied for function dblink_connect_u)
1.3.3 put the dblink job into the SQL but still ran psql against
_supabase, where pg_cron refuses the extension; the psql session now runs
in postgres, the only database pg_cron accepts in this image
version 1.3.4
the supabase image pins cron.database_name to postgres, so pg_cron refuses
jobs scheduled from _supabase, where the log tables live; 1.3.2's upgrade
failed on the first live run with 'can only create extension in database
postgres'. The job now runs in postgres and deletes the _analytics
log_events_* rows in _supabase over a local dblink connection, which needs
no password because the image trusts local connections for supabase_admin.
version 1.3.3
- every instance gets a pg_cron job (set up on create, upgrade and resume)
that deletes _analytics.log_events_* rows older than 2 days and runs the
first cleanup right away; Logflare never deletes stored logs, and instance
3's 2 GB db image died 100% full of them on 2026-10-06, after which
analytics 500'd every delivery and vector was OOM-killed by its
undeliverable backlog every few minutes
- vector's memory limit back to 512 MiB, the ceiling the production
supabase charts carry; the retention job bounds its backlog, so 1024 MiB
is not needed anymore
- version 1.3.2
1.3.0 put suspend and resume in the main menu and on the command line, which
meant remembering a separate command and picking the instance separately for
each of the two. They belong where you are already looking at the instance.
The menu of one instance now ends with a single line above "Back", worded for the
state that instance is in when the menu is drawn:
Instance 2 - supa2.didgibot.com - running
...
Suspend it (stop it, keep its data)
Instance 2 - supa2.didgibot.com - suspended
...
Resume it (start it again with its data)
The state is read again every time the menu is drawn, so right after suspending
an instance the same line reads "Resume it". The instance's state is in the menu
title, and the instance picker names it next to every instance, so you can see
which ones are awake before choosing one.
- The main menu's two entries "Suspend instance" and "Resume instance" are gone.
"Show instance" is now "Show instance (and suspend or resume it)": one way in.
- Asking for the tunnel command of a sleeping instance says it is suspended and
to resume it from this menu, instead of printing a command that cannot work.
- The line printed after suspending, and the one `supaspin list` prints under a
sleeping instance, point at the menu line rather than at a command.
`supaspin suspend N` and `supaspin resume N` still work for scripting.
Verified on instance 2 (supa2.didgibot.com): suspended from the instance menu,
the menu came back reading "Resume it" and the picker showed "(suspended)",
resumed from the same line, 12/12 pods with 0 restarts and its 55 tables and
11 MB of data intact.
Tests: 136 pass (6 new). Version 1.3.0 -> 1.3.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An instance you are not using had to be deleted, and deleting it destroys its
database. Now it can be put to sleep instead.
sudo supaspin suspend N # stops all twelve pods, keeps everything else
sudo supaspin resume N # starts them again with the same data
Suspending gives the server back, per instance: the 1.875 GiB it had reserved and
the 1.5-2.5 GiB it was actually using, its twelve pod slots out of the 110 a node
allows, and its two server ports (Postgres 544N, Studio 300N).
It keeps: the database and the uploads (the disk images stay mounted), the .env
file with its passwords and keys, the HTTPS certificate, which goes on renewing
itself while the instance sleeps, and the hostname, which answers "service
unavailable" until the instance is resumed. Disk space is not given back - that is
the data being kept.
How: every service takes its pod count from a `replicaCount` setting, so suspend
writes zero into all twelve and re-applies the chart, resume writes one. This goes
through the instance's stored settings rather than scaling the running pods by
hand, so it survives anything that re-applies the chart later.
`supaspin upgrade` now understands a sleeping instance: it stores the new memory
limits for it and leaves it asleep instead of waiting for pods that by design do
not exist. `supaspin list` reports each instance as running, suspended or partly
suspended, and prints no tunnel commands for a sleeping one, because nothing is
listening on its ports.
`suspend` and `resume` take one instance number and have no "all" form: stopping
every instance is not something to type by accident, and resuming them all at once
would start 36 pods in one go.
Reading the stored settings and handing them back to Helm is now shared by
upgrade, suspend and resume.
Verified on instance 2 (supa2.didgibot.com): suspended to zero pods, memory
reserved on the node fell from 25766 MiB to 23846 MiB (exactly the 1920 MiB it
had), ports 3002 and 5442 freed, then resumed to 12/12 pods with 0 restarts, its
55 tables and 11 MB of data intact and the gateway answering again.
Tests: 130 pass (17 new). Version 1.2.1 -> 1.3.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository is owned by the organisation `public`, not by the user `igor`.
Gitea forwards the old address for anything a browser or git does, so the install
command and the clone lines kept working and the wrong address went unnoticed.
An API call is the exception. A POST to `/api/v1/repos/igor/supaspin/...` is
answered with a redirect that curl does not follow for a POST, so the command
returns an empty body and no error - which reads as a rejected token. Creating
the v1.2.0 tag and opening its pull request both failed this way first.
- CLAUDE.md: the tagging rule now names `public` and says why an API address
cannot use the forwarded one.
- README.md, install.sh: the install, token and clone commands now use the
address the server actually serves.
Version 1.2.0 -> 1.2.1. No change to what the program does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two logging services were being killed by the kernel in a loop on instance 1
(supa1.didgibot.com): vector 297 times, analytics 36 times, both exit code 137
(out of memory). The old ceilings sat right on top of what the services normally
use, so any backlog crossed them.
Measured across the three running instances on 2026-09-22:
- analytics 612 / 694 / 662 MiB against a 768 MiB ceiling -> 1536 MiB
- vector 287 / 290 MiB against 512 MiB -> 1024 MiB
- realtime 269 / 287 / 300 MiB against 384 MiB -> 768 MiB
- kong 260 / 269 / 265 MiB against 512 MiB -> 768 MiB
- studio 203, storage 187, rest 162 MiB against 256 MiB -> 512 MiB
- db 163 / 175 / 273 MiB against 2048 MiB -> unchanged
Limits only. The requests stay at 128 MiB (512 for db): the server is one machine
shared with the production workloads and already has 80% of its memory reserved,
so raising the requests would leave production nothing to schedule into. Raising a
limit reserves nothing - it only moves the point at which the kernel kills.
Per-instance totals go from 5.875 GiB to 9.5 GiB of limit; reserved memory is
unchanged at 1.875 GiB.
New numbers only reach instances created afterwards, so this also adds
`sudo supaspin upgrade [N]`: it reads back the values Helm stored at install
time, replaces the memory block in them and applies the chart again. Pods are
replaced service by service; databases, uploads, certificates and .env files are
untouched. Without a number it does every instance and reports which ones failed.
Also available from the menu.
Fixes a rollout that could never finish, found by the first upgrade: Studio binds
port 3001 (3002, 3003) on the server itself so it can be reached over an SSH
tunnel, and one port can be held by one pod at a time. The default rollout starts
the replacement pod before stopping the old one, so the replacement never got the
port and stayed Pending for ever. The Studio deployment now stops the old pod
first. Postgres was never affected: a StatefulSet already replaces its pod that
way.
The values file that carries the instance's secrets and the post-renderer
arguments are now built by shared helpers used by both create and upgrade.
Tests: 113 pass (12 new). Version 1.1.2 -> 1.2.0 in VERSION, the VERSION constant
and README.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- vector reads the log files of every pod on the node, so its memory need follows the node's log volume. At
256 MiB it was killed for running out of memory 122 times on instance 1 and used 89% on instance 2.
The limit is now 512 MiB (request unchanged at 128 MiB) for new instances; the total limit per instance
goes from 5.625 GiB to 5.875 GiB, requests stay at 1.875 GiB.
- README memory table and the test for the totals updated
- version 1.1.1 -> 1.1.2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- install.sh said "Installed supaspin supaspin 1.1.0." because --version now includes the name; it now says
"Installed supaspin 1.1.1."
- CLAUDE.md: tells AI agents to read Igor's global rules first, and states the versioning rule (number in VERSION,
code and README together, semantic versioning, major bumps only with Igor's yes, a git tag vX.Y.Z after every merge)
- version 1.1.0 -> 1.1.1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- list and tunnel print plain-language tunnel instructions for every instance: keep it open, run it in the
background, close it, with a notice that background/close work on Mac and Linux only, not Windows
- push-env N TARGET puts an instance's .env values into another .env file, on this server or over SSH (Mac, Linux,
Windows): existing keys are replaced, missing keys added, commented-out lines ignored, every other line kept.
A temporary copy of the target is compared after writing and put back on any mismatch. --dry-run shows the
changes without writing. SSH failures are explained with how to set SSH up.
- VERSION file (1.1.0), version line in the README, `--version` prints "supaspin 1.1.0"; a test keeps the file, the
code constant and the README in step
- remove an unused parameter of run() and an unreachable second help branch in main()
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>