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
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 36 additions and 24 deletions
+13 -6
View File
@@ -1,6 +1,6 @@
# supaspin # supaspin
Version 1.6.1 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.
@@ -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 - 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 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 - 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 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 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) | | 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 | | 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 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 | `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) | | 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 | | 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 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 push-env N TARGET [--port P] [--key FILE] [--dry-run] # see "Copying an instance's values"
sudo supaspin delete N [--yes] 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 suspend N # stop an instance, keep its data (see "Suspending an instance")
sudo supaspin resume N # start it again sudo supaspin resume N # start it again
sudo supaspin mail # change the saved default mail settings sudo supaspin mail # change the saved default mail settings
sudo supaspin setup # change settings sudo supaspin setup # change settings
supaspin --version # prints "supaspin 1.3.1" supaspin --version # prints "supaspin X.Y.Z"
``` ```
### Connecting your app ### 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 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 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 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 db image and vector's backlog, so vector is back to the 512 MiB ceiling the production supabase
charts carry. 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 - **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`. 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). - **"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. - **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`); - **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. another program is using it. Free the port or use another instance number.
+1 -1
View File
@@ -1 +1 @@
1.6.1 1.6.3
+14 -11
View File
@@ -39,7 +39,7 @@ import tty
import yaml import yaml
VERSION = "1.6.1" 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"
@@ -189,24 +189,27 @@ def buffering_middleware(n):
def ingress_annotations(cfg, n): def ingress_annotations(cfg, n):
"""Ingress annotations for instance n, shaped for the configured ingress controller. """Ingress annotations for instance n.
With Traefik the upload cap lives in a Middleware object created next to the instance Both controllers' annotations are always present: an annotation only takes effect
and the ingress points at it; with nginx it is proxy-body-size, and the chart's own when a controller of that family actually routes the ingress, so on a Traefik
rewrite-target default must be nullified or every API path breaks. The cert-manager cluster the nginx keys sit inert (and vice versa) and nothing needs reconfiguring
annotation orders a certificate from the supaspin-letsencrypt issuer; without it the when the router changes. The upload cap therefore lives in two places that agree:
TLS secret keeps whatever supaspin put there (a saved or a self-signed certificate).""" proxy-body-size for nginx, and the Middleware object the traefik annotation points
at. The chart's own rewrite-target default must be nullified or every API path
breaks. The cert-manager annotation orders a certificate from the
supaspin-letsencrypt issuer; without it the TLS secret keeps whatever supaspin put
there (a saved or a self-signed certificate)."""
anns = {} anns = {}
if cfg.get("lets_encrypt", True): if cfg.get("lets_encrypt", True):
anns["cert-manager.io/cluster-issuer"] = ISSUER anns["cert-manager.io/cluster-issuer"] = ISSUER
if cfg.get("ingress_controller", "nginx") == "traefik":
anns["traefik.ingress.kubernetes.io/router.middlewares"] = \
f"{namespace(n)}-{buffering_middleware(n)}@kubernetescrd"
else:
# The chart default rewrites every path to "/", which breaks all APIs; null removes it. # The chart default rewrites every path to "/", which breaks all APIs; null removes it.
anns["nginx.ingress.kubernetes.io/rewrite-target"] = None anns["nginx.ingress.kubernetes.io/rewrite-target"] = None
anns["nginx.ingress.kubernetes.io/proxy-body-size"] = "50m" anns["nginx.ingress.kubernetes.io/proxy-body-size"] = "50m"
anns["nginx.ingress.kubernetes.io/proxy-read-timeout"] = "3600" anns["nginx.ingress.kubernetes.io/proxy-read-timeout"] = "3600"
anns["nginx.ingress.kubernetes.io/proxy-send-timeout"] = "3600" anns["nginx.ingress.kubernetes.io/proxy-send-timeout"] = "3600"
if cfg.get("ingress_controller", "nginx") == "traefik":
anns["traefik.ingress.kubernetes.io/router.middlewares"] = \
f"{namespace(n)}-{buffering_middleware(n)}@kubernetescrd"
return anns return anns
+6 -4
View File
@@ -216,13 +216,15 @@ class IngressShaping(unittest.TestCase):
self.assertEqual(a["nginx.ingress.kubernetes.io/proxy-read-timeout"], "3600") self.assertEqual(a["nginx.ingress.kubernetes.io/proxy-read-timeout"], "3600")
self.assertNotIn("traefik.ingress.kubernetes.io/router.middlewares", a) self.assertNotIn("traefik.ingress.kubernetes.io/router.middlewares", a)
def test_traefik_native(self): def test_traefik_also_keeps_nginx_annotations(self):
# The nginx keys must be present in traefik mode too: while nginx routes the
# ingress they enforce the cap, and they are inert the day Traefik takes over.
a = self.ann({"ingress_controller": "traefik"}) a = self.ann({"ingress_controller": "traefik"})
self.assertEqual(a["traefik.ingress.kubernetes.io/router.middlewares"], self.assertEqual(a["traefik.ingress.kubernetes.io/router.middlewares"],
"supaspin-2-supa2-buffering@kubernetescrd") "supaspin-2-supa2-buffering@kubernetescrd")
for nginx_key in ("rewrite-target", "proxy-body-size", "proxy-read-timeout", self.assertIsNone(a["nginx.ingress.kubernetes.io/rewrite-target"])
"proxy-send-timeout"): self.assertEqual(a["nginx.ingress.kubernetes.io/proxy-body-size"], "50m")
self.assertNotIn(f"nginx.ingress.kubernetes.io/{nginx_key}", a) self.assertEqual(a["nginx.ingress.kubernetes.io/proxy-read-timeout"], "3600")
self.assertEqual(a["cert-manager.io/cluster-issuer"], "supaspin-letsencrypt") self.assertEqual(a["cert-manager.io/cluster-issuer"], "supaspin-letsencrypt")
def test_lets_encrypt_off_drops_cert_manager_annotation(self): def test_lets_encrypt_off_drops_cert_manager_annotation(self):