vibecarbon 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/carbon/db/Dockerfile +14 -3
- package/carbon/docker-compose.dns01.prod.yml +42 -0
- package/carbon/k8s/base/backup/configmap-walg.yaml +47 -0
- package/carbon/k8s/base/backup/cronjob.yaml +67 -92
- package/carbon/k8s/base/backup/kustomization.yaml +1 -0
- package/carbon/k8s/base/backup/network-policy.yaml +9 -14
- package/carbon/k8s/base/backup/rbac.yaml +40 -0
- package/carbon/k8s/base/network-policies.yaml +33 -0
- package/carbon/k8s/values/supabase.values.yaml +144 -0
- package/package.json +3 -3
- package/src/backup.js +2 -36
- package/src/create.js +9 -4
- package/src/deploy.js +12 -0
- package/src/lib/backup-s3.js +0 -31
- package/src/lib/cloudflare.js +0 -59
- package/src/lib/config.js +1 -42
- package/src/lib/deploy/acme.js +57 -0
- package/src/lib/deploy/admin-user.js +105 -0
- package/src/lib/deploy/bundle.js +17 -0
- package/src/lib/deploy/compose/ha.js +94 -91
- package/src/lib/deploy/compose/index.js +50 -248
- package/src/lib/deploy/k8s/ha/index.js +12 -148
- package/src/lib/deploy/k8s/index.js +1 -21
- package/src/lib/deploy/k8s/k3s.js +314 -128
- package/src/lib/deploy/orchestrator.js +70 -35
- package/src/lib/deploy/utils.js +20 -0
- package/src/lib/hetzner-guided-setup.js +0 -7
- package/src/lib/iac/index.js +0 -9
- package/src/lib/images.js +17 -0
- package/src/lib/licensing/index.js +1 -59
- package/src/lib/licensing/tiers.js +0 -8
- package/src/lib/pod-backups.js +53 -0
- package/src/lib/providers/index.js +0 -47
- package/src/lib/ssh.js +12 -0
- package/src/restore.js +98 -265
- package/src/scale.js +43 -20
- package/carbon/backup/Dockerfile +0 -75
- package/carbon/backup/backup.sh +0 -95
- package/carbon/runtime.Dockerfile +0 -17
- package/src/lib/deploy/compose/acme-verify.js +0 -121
- package/src/lib/deploy/index.js +0 -119
- package/src/lib/kubectl.js +0 -81
package/carbon/db/Dockerfile
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
# syntax=docker/dockerfile:1
|
|
2
2
|
FROM supabase/postgres:15.8.1.085
|
|
3
3
|
|
|
4
|
-
# Install WAL-G v3.0.5
|
|
4
|
+
# Install WAL-G v3.0.5. This image IS the backup tooling now (both compose
|
|
5
|
+
# and k8s): wal-g runs co-located with PGDATA in this container for continuous
|
|
6
|
+
# WAL archiving, base backups, and restore — no separate backup image.
|
|
5
7
|
# Earlier v3.0.3 URL 404'd in 2026-04-26 (alpine variant retired upstream),
|
|
6
8
|
# and curl without --fail piped a 404 HTML body into tar which surfaced
|
|
7
9
|
# as "not in gzip format" instead of an honest HTTP error. --fail makes
|
|
8
10
|
# curl exit non-zero on 4xx/5xx so the failure is unmistakable.
|
|
9
11
|
#
|
|
12
|
+
# Build target MUST match the base image's glibc: supabase/postgres:15.8.1.085
|
|
13
|
+
# is Ubuntu 20.04 (glibc 2.31). The ubuntu-22.04 wal-g build is linked against
|
|
14
|
+
# glibc 2.35 and dies at runtime with `version 'GLIBC_2.3{2,3,4}' not found`
|
|
15
|
+
# — which breaks BOTH base backups and WAL archiving (the archive_command
|
|
16
|
+
# shells out to wal-g), and is invisible at deploy time because we only assert
|
|
17
|
+
# archive_mode=on, never that wal-g actually runs. The ubuntu-20.04 build links
|
|
18
|
+
# against glibc 2.31 and runs on this base. (RCA 2026-05-30: e2e k8s/k8s-ha
|
|
19
|
+
# backup step — wal-g exited 1 with the GLIBC error inside the db pod.)
|
|
20
|
+
#
|
|
10
21
|
# Single RUN with `apt-get purge -y --auto-remove curl` collapses the
|
|
11
22
|
# install + cleanup into one layer so the curl binary doesn't end up
|
|
12
23
|
# in the published image (saving ~5MB and reducing attack surface).
|
|
13
24
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
14
25
|
curl \
|
|
15
26
|
ca-certificates \
|
|
16
|
-
&& curl -fL https://github.com/wal-g/wal-g/releases/download/v3.0.5/wal-g-pg-ubuntu-
|
|
17
|
-
&& mv wal-g-pg-ubuntu-
|
|
27
|
+
&& curl -fL https://github.com/wal-g/wal-g/releases/download/v3.0.5/wal-g-pg-ubuntu-20.04-amd64.tar.gz | tar -xz \
|
|
28
|
+
&& mv wal-g-pg-ubuntu-20.04-amd64 /usr/local/bin/wal-g \
|
|
18
29
|
&& chmod +x /usr/local/bin/wal-g \
|
|
19
30
|
&& apt-get purge -y --auto-remove curl \
|
|
20
31
|
&& rm -rf /var/lib/apt/lists/*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# DNS-01 ACME override — applied ONLY for managed-DNS deploys (cloudflare / hetzner).
|
|
2
|
+
#
|
|
3
|
+
# Activated by the deploy bundler (src/lib/deploy/bundle.js -> composeFileFlags)
|
|
4
|
+
# when the deploy manages DNS via an API. It is appended AFTER
|
|
5
|
+
# docker-compose.prod.yml, so the `command:` below REPLACES the base Traefik
|
|
6
|
+
# command — Compose replaces sequences, it does not merge them. That means the
|
|
7
|
+
# full command list must be restated here; keep every non-ACME flag in sync
|
|
8
|
+
# with docker-compose.prod.yml. Only the two `httpchallenge` lines are swapped
|
|
9
|
+
# for the three `dnschallenge` lines.
|
|
10
|
+
#
|
|
11
|
+
# Why: cloudflare/hetzner expose an API lego (Traefik's ACME client) uses to
|
|
12
|
+
# solve the DNS-01 challenge by writing a TXT record. This removes the HTTP-01
|
|
13
|
+
# race against A-record propagation, so the deploy no longer needs the
|
|
14
|
+
# DNS-propagation poll or the Traefik cert self-heal. `manual` DNS keeps the
|
|
15
|
+
# base HTTP-01 path (this override is not applied for it).
|
|
16
|
+
#
|
|
17
|
+
# Tokens: lego picks the provider from ACME_DNS_PROVIDER and reads its token.
|
|
18
|
+
# Hetzner uses the consolidated Cloud API token HETZNER_API_TOKEN (lego v4.27+,
|
|
19
|
+
# Traefik >= 3.6.6; we pin v3.6.11) — the same token as server ops. Cloudflare
|
|
20
|
+
# uses CF_DNS_API_TOKEN. Both are passed through; only the active provider's
|
|
21
|
+
# token is read. All three are populated in .env by the bundler.
|
|
22
|
+
services:
|
|
23
|
+
traefik:
|
|
24
|
+
command:
|
|
25
|
+
- "--api.dashboard=true"
|
|
26
|
+
- "--providers.docker=true"
|
|
27
|
+
- "--providers.docker.endpoint=tcp://docker-socket-proxy:2375"
|
|
28
|
+
- "--providers.docker.exposedbydefault=false"
|
|
29
|
+
- "--providers.file.directory=/etc/traefik/dynamic"
|
|
30
|
+
- "--entrypoints.web.address=:80"
|
|
31
|
+
- "--entrypoints.websecure.address=:443"
|
|
32
|
+
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
|
|
33
|
+
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=${ACME_DNS_PROVIDER}"
|
|
34
|
+
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
|
|
35
|
+
- "--certificatesresolvers.letsencrypt.acme.email={{ADMIN_EMAIL}}"
|
|
36
|
+
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
|
37
|
+
- "--certificatesresolvers.letsencrypt.acme.caserver=${ACME_CA_SERVER:-https://acme-v02.api.letsencrypt.org/directory}"
|
|
38
|
+
- "--log.level=WARN"
|
|
39
|
+
environment:
|
|
40
|
+
ACME_DNS_PROVIDER: ${ACME_DNS_PROVIDER:-}
|
|
41
|
+
CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:-}
|
|
42
|
+
HETZNER_API_TOKEN: ${HETZNER_API_TOKEN:-}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Fault-tolerant WAL-archive wrapper mounted into the supabase-db container at
|
|
2
|
+
# /etc/postgresql/wal-archive.sh (see k8s/values/supabase.values.yaml
|
|
3
|
+
# deployment.db.volumes/volumeMounts). Postgres' archive_command invokes it as
|
|
4
|
+
# `bash /etc/postgresql/wal-archive.sh %p` (enabled via ALTER SYSTEM in
|
|
5
|
+
# applyK3sManifests). wal-g reads WALG_S3_PREFIX / AWS_* from the db
|
|
6
|
+
# container's env.
|
|
7
|
+
#
|
|
8
|
+
# IMPORTANT: keep this script body byte-for-byte in sync with
|
|
9
|
+
# carbon/volumes/db/wal-archive.sh (the compose mount). Same retry + exit-0
|
|
10
|
+
# semantics: a persistent S3 outage degrades PITR for the failed segment but
|
|
11
|
+
# keeps the DB up instead of filling the disk.
|
|
12
|
+
apiVersion: v1
|
|
13
|
+
kind: ConfigMap
|
|
14
|
+
metadata:
|
|
15
|
+
name: vibecarbon-wal-archive
|
|
16
|
+
namespace: vibecarbon
|
|
17
|
+
labels:
|
|
18
|
+
app: vibecarbon-backup
|
|
19
|
+
component: backup
|
|
20
|
+
data:
|
|
21
|
+
wal-archive.sh: |
|
|
22
|
+
#!/bin/bash
|
|
23
|
+
# Fault-tolerant archive_command for Postgres + wal-g.
|
|
24
|
+
# Invoked by postgres as: wal-archive.sh <wal-file-path>
|
|
25
|
+
# (%p — absolute path to the WAL segment). Retries with backoff, then
|
|
26
|
+
# logs WAL_ARCHIVE_FAILED and exits 0 so PG recycles the segment rather
|
|
27
|
+
# than pinning it and filling the disk. Keep in sync with
|
|
28
|
+
# carbon/volumes/db/wal-archive.sh.
|
|
29
|
+
set -u
|
|
30
|
+
WAL_PATH="${1:?archive_command requires a WAL file path}"
|
|
31
|
+
RETRIES="${WAL_ARCHIVE_RETRIES:-3}"
|
|
32
|
+
SLEEP_BASE="${WAL_ARCHIVE_SLEEP:-2}"
|
|
33
|
+
|
|
34
|
+
attempt=1
|
|
35
|
+
while [ "$attempt" -le "$RETRIES" ]; do
|
|
36
|
+
if /usr/local/bin/wal-g wal-push "$WAL_PATH"; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
if [ "$attempt" -lt "$RETRIES" ]; then
|
|
40
|
+
sleep_for=$((SLEEP_BASE ** attempt))
|
|
41
|
+
sleep "$sleep_for"
|
|
42
|
+
fi
|
|
43
|
+
attempt=$((attempt + 1))
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
echo "WAL_ARCHIVE_FAILED: wal-g wal-push '$WAL_PATH' failed after $RETRIES attempts; allowing PG to recycle (PITR gap for this segment)" >&2
|
|
47
|
+
exit 0
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# Nightly wal-g base backup.
|
|
2
|
+
#
|
|
3
|
+
# wal-g backup-push needs direct PGDATA filesystem access, which only the
|
|
4
|
+
# supabase-db pod has. So instead of a separate backup container that talks to
|
|
5
|
+
# postgres over the network (the old pg_dump design), this CronJob runs a tiny
|
|
6
|
+
# kubectl image and `kubectl exec`s wal-g INSIDE the db pod — where wal-g, the
|
|
7
|
+
# WALG_*/AWS_* env, and PGDATA already live (see k8s/values/supabase.values.yaml
|
|
8
|
+
# + the WAL-archiving setup in applyK3sManifests). No bespoke backup image, no
|
|
9
|
+
# sideload, no registry push.
|
|
10
|
+
#
|
|
11
|
+
# HA-safe: the command guards on pg_is_in_recovery()=f, so it only takes a base
|
|
12
|
+
# backup on the PRIMARY. On the standby (in recovery) it no-ops, and after a
|
|
13
|
+
# failover the new primary's CronJob transparently starts taking base backups —
|
|
14
|
+
# no per-cluster config.
|
|
1
15
|
apiVersion: batch/v1
|
|
2
16
|
kind: CronJob
|
|
3
17
|
metadata:
|
|
@@ -23,98 +37,59 @@ spec:
|
|
|
23
37
|
restartPolicy: OnFailure
|
|
24
38
|
serviceAccountName: backup-sa
|
|
25
39
|
containers:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
name: vibecarbon-secrets
|
|
80
|
-
key: S3_REGION
|
|
81
|
-
optional: true
|
|
82
|
-
- name: S3_BACKUP_BUCKET
|
|
83
|
-
valueFrom:
|
|
84
|
-
secretKeyRef:
|
|
85
|
-
name: vibecarbon-secrets
|
|
86
|
-
key: S3_BACKUP_BUCKET
|
|
87
|
-
optional: true
|
|
88
|
-
- name: BACKUP_RETENTION_DAYS
|
|
89
|
-
valueFrom:
|
|
90
|
-
configMapKeyRef:
|
|
91
|
-
name: vibecarbon-config
|
|
92
|
-
key: BACKUP_RETENTION_DAYS
|
|
93
|
-
optional: true
|
|
94
|
-
- name: STORAGE_BACKEND
|
|
95
|
-
value: file
|
|
96
|
-
# Use pg_dump (network protocol) instead of WAL-G in this
|
|
97
|
-
# CronJob — WAL-G's backup-push needs direct filesystem access
|
|
98
|
-
# to PGDATA, which only the supabase-db pod has. The backup
|
|
99
|
-
# pod runs separately and connects via PGHOST, so it has to
|
|
100
|
-
# use the libpq-based pg_dump path. Switching WAL-G on for
|
|
101
|
-
# k8s would require a sidecar inside supabase-db.
|
|
102
|
-
- name: BACKUP_MODE
|
|
103
|
-
value: pg_dump
|
|
104
|
-
- name: PROJECT_NAME
|
|
105
|
-
valueFrom:
|
|
106
|
-
configMapKeyRef:
|
|
107
|
-
name: vibecarbon-config
|
|
108
|
-
key: PROJECT_NAME
|
|
109
|
-
resources:
|
|
110
|
-
requests:
|
|
111
|
-
cpu: 100m
|
|
112
|
-
memory: 256Mi
|
|
113
|
-
limits:
|
|
114
|
-
cpu: 500m
|
|
115
|
-
memory: 512Mi
|
|
116
|
-
securityContext:
|
|
117
|
-
allowPrivilegeEscalation: false
|
|
40
|
+
- name: backup
|
|
41
|
+
# Pinned public kubectl image — no build/sideload/push. Bump the
|
|
42
|
+
# tag deliberately; `latest` would risk a silent client/server
|
|
43
|
+
# skew on the scheduled run. Needs bash (the script below uses
|
|
44
|
+
# `set -euo pipefail`) + kubectl on PATH, so the distroless
|
|
45
|
+
# registry.k8s.io/kubectl is unusable. Was bitnami/kubectl:1.31
|
|
46
|
+
# until Bitnami sunset their public Docker Hub catalog (2025) and
|
|
47
|
+
# the tag vanished (`not found` → ImagePullBackOff); alpine/k8s
|
|
48
|
+
# is the maintained drop-in (alpine base, bash + kubectl).
|
|
49
|
+
image: alpine/k8s:1.31.12
|
|
50
|
+
imagePullPolicy: IfNotPresent
|
|
51
|
+
env:
|
|
52
|
+
- name: BACKUP_RETENTION_DAYS
|
|
53
|
+
valueFrom:
|
|
54
|
+
configMapKeyRef:
|
|
55
|
+
name: vibecarbon-config
|
|
56
|
+
key: BACKUP_RETENTION_DAYS
|
|
57
|
+
optional: true
|
|
58
|
+
command:
|
|
59
|
+
- /bin/bash
|
|
60
|
+
- -c
|
|
61
|
+
- |
|
|
62
|
+
set -euo pipefail
|
|
63
|
+
RETAIN="${BACKUP_RETENTION_DAYS:-7}"
|
|
64
|
+
# Exec wal-g inside the db pod. The guard makes this a no-op on
|
|
65
|
+
# a standby (in recovery) so only the primary takes base backups.
|
|
66
|
+
kubectl exec -n vibecarbon statefulset/supabase-supabase-db -- bash -c '
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
# wal-g backup-push connects to Postgres (pg_backup_start) using
|
|
69
|
+
# libpq env. Unset, PGUSER defaults to the container OS user
|
|
70
|
+
# "root" → "role \"root\" does not exist". And only the cluster
|
|
71
|
+
# superuser may call pg_backup_start — supabase_admin is the sole
|
|
72
|
+
# superuser ("postgres" gets "permission denied for function
|
|
73
|
+
# pg_backup_start"). So pin the libpq connection explicitly.
|
|
74
|
+
# (wal-push/archiving and backup-fetch/restore do not connect to
|
|
75
|
+
# PG, which is why this only bit backup-push. RCA 2026-05-30.)
|
|
76
|
+
export PGUSER=supabase_admin PGHOST=localhost PGPORT=5432 PGDATABASE=postgres
|
|
77
|
+
if [ "$(psql -U supabase_admin -d postgres -tAc "SELECT pg_is_in_recovery()")" != "f" ]; then
|
|
78
|
+
echo "supabase-db is in recovery (standby) — skipping base backup."
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
wal-g backup-push "$PGDATA"
|
|
82
|
+
wal-g delete retain FULL '"${RETAIN}"' --confirm
|
|
83
|
+
'
|
|
84
|
+
resources:
|
|
85
|
+
requests:
|
|
86
|
+
cpu: 50m
|
|
87
|
+
memory: 64Mi
|
|
88
|
+
limits:
|
|
89
|
+
cpu: 250m
|
|
90
|
+
memory: 128Mi
|
|
91
|
+
securityContext:
|
|
92
|
+
allowPrivilegeEscalation: false
|
|
118
93
|
securityContext:
|
|
119
94
|
seccompProfile:
|
|
120
95
|
type: RuntimeDefault
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# The backup CronJob pod no longer talks to postgres:5432 or S3 directly — it
|
|
2
|
+
# runs `kubectl exec` into the supabase-db pod (wal-g runs there). So its only
|
|
3
|
+
# egress need is the Kubernetes apiserver. DNS + intra-namespace egress come
|
|
4
|
+
# from the cluster-baseline policies (network-policies.yaml, podSelector {}).
|
|
5
|
+
# The wal-g → S3 egress now happens from the supabase-db pod, allowed by the
|
|
6
|
+
# supabase-db-s3-egress policy in network-policies.yaml.
|
|
1
7
|
apiVersion: networking.k8s.io/v1
|
|
2
8
|
kind: NetworkPolicy
|
|
3
9
|
metadata:
|
|
@@ -10,22 +16,11 @@ spec:
|
|
|
10
16
|
policyTypes:
|
|
11
17
|
- Egress
|
|
12
18
|
egress:
|
|
13
|
-
#
|
|
14
|
-
-
|
|
15
|
-
- podSelector:
|
|
16
|
-
matchLabels:
|
|
17
|
-
app: vibecarbon-postgres
|
|
18
|
-
ports:
|
|
19
|
-
- protocol: TCP
|
|
20
|
-
port: 5432
|
|
21
|
-
# Upload to S3 (external HTTPS)
|
|
19
|
+
# Kubernetes apiserver (kubernetes.default → ClusterIP 10.43.0.1:443) so
|
|
20
|
+
# `kubectl exec` works. Mirrors the app/traefik apiserver-egress pattern.
|
|
22
21
|
- to:
|
|
23
22
|
- ipBlock:
|
|
24
|
-
cidr:
|
|
25
|
-
except:
|
|
26
|
-
- 10.0.0.0/8
|
|
27
|
-
- 172.16.0.0/12
|
|
28
|
-
- 192.168.0.0/16
|
|
23
|
+
cidr: 10.43.0.1/32
|
|
29
24
|
ports:
|
|
30
25
|
- protocol: TCP
|
|
31
26
|
port: 443
|
|
@@ -5,3 +5,43 @@ metadata:
|
|
|
5
5
|
labels:
|
|
6
6
|
app: vibecarbon-backup
|
|
7
7
|
component: backup
|
|
8
|
+
---
|
|
9
|
+
# The backup CronJob runs `kubectl exec` into the supabase-db pod to invoke
|
|
10
|
+
# wal-g backup-push (wal-g needs local PGDATA access). That requires
|
|
11
|
+
# pods/exec in the vibecarbon namespace. `pods` get/list is needed so
|
|
12
|
+
# `kubectl exec statefulset/...` can resolve the pod.
|
|
13
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
14
|
+
kind: Role
|
|
15
|
+
metadata:
|
|
16
|
+
name: backup-exec
|
|
17
|
+
namespace: vibecarbon
|
|
18
|
+
labels:
|
|
19
|
+
app: vibecarbon-backup
|
|
20
|
+
component: backup
|
|
21
|
+
rules:
|
|
22
|
+
- apiGroups: [""]
|
|
23
|
+
resources: ["pods"]
|
|
24
|
+
verbs: ["get", "list"]
|
|
25
|
+
- apiGroups: [""]
|
|
26
|
+
resources: ["pods/exec"]
|
|
27
|
+
verbs: ["create"]
|
|
28
|
+
- apiGroups: ["apps"]
|
|
29
|
+
resources: ["statefulsets"]
|
|
30
|
+
verbs: ["get"]
|
|
31
|
+
---
|
|
32
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
33
|
+
kind: RoleBinding
|
|
34
|
+
metadata:
|
|
35
|
+
name: backup-exec
|
|
36
|
+
namespace: vibecarbon
|
|
37
|
+
labels:
|
|
38
|
+
app: vibecarbon-backup
|
|
39
|
+
component: backup
|
|
40
|
+
subjects:
|
|
41
|
+
- kind: ServiceAccount
|
|
42
|
+
name: backup-sa
|
|
43
|
+
namespace: vibecarbon
|
|
44
|
+
roleRef:
|
|
45
|
+
kind: Role
|
|
46
|
+
name: backup-exec
|
|
47
|
+
apiGroup: rbac.authorization.k8s.io
|
|
@@ -93,3 +93,36 @@ spec:
|
|
|
93
93
|
port: 53
|
|
94
94
|
- protocol: TCP
|
|
95
95
|
port: 53
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
# Allow the supabase-db pod to egress to S3 (HTTPS) for wal-g: continuous WAL
|
|
99
|
+
# archiving (archive_command → wal-g wal-push) and the nightly base backup
|
|
100
|
+
# (kubectl exec → wal-g backup-push) both run INSIDE the db container and push
|
|
101
|
+
# to Hetzner Object Storage. Without this, the implicit default-deny-egress
|
|
102
|
+
# (allow-dns/allow-intra-namespace-egress select all pods with Egress type)
|
|
103
|
+
# drops the db pod's outbound 443 and wal-g hangs/fails.
|
|
104
|
+
# Selector matches the chart-generated db pod labels; base kustomize `labels:`
|
|
105
|
+
# does not include selectors, so these are not mutated.
|
|
106
|
+
apiVersion: networking.k8s.io/v1
|
|
107
|
+
kind: NetworkPolicy
|
|
108
|
+
metadata:
|
|
109
|
+
name: supabase-db-s3-egress
|
|
110
|
+
namespace: vibecarbon
|
|
111
|
+
spec:
|
|
112
|
+
podSelector:
|
|
113
|
+
matchLabels:
|
|
114
|
+
app.kubernetes.io/name: supabase-db
|
|
115
|
+
app.kubernetes.io/instance: supabase
|
|
116
|
+
policyTypes:
|
|
117
|
+
- Egress
|
|
118
|
+
egress:
|
|
119
|
+
- to:
|
|
120
|
+
- ipBlock:
|
|
121
|
+
cidr: 0.0.0.0/0
|
|
122
|
+
except:
|
|
123
|
+
- 10.0.0.0/8
|
|
124
|
+
- 172.16.0.0/12
|
|
125
|
+
- 192.168.0.0/16
|
|
126
|
+
ports:
|
|
127
|
+
- protocol: TCP
|
|
128
|
+
port: 443
|
|
@@ -54,6 +54,18 @@ secret:
|
|
|
54
54
|
password: DB_PASSWORD
|
|
55
55
|
database: DB_NAME
|
|
56
56
|
|
|
57
|
+
# Override the db image with vibecarbon's wal-g-equipped build (carbon/db/
|
|
58
|
+
# Dockerfile: FROM supabase/postgres + wal-g v3.0.5). The stock chart image
|
|
59
|
+
# has no wal-g, which is why k8s historically had no WAL archiving / PITR.
|
|
60
|
+
# Built + sideloaded to the supabase node by deployK3s (k3s-db-build /
|
|
61
|
+
# k3s-db-sideload); IfNotPresent so the sideloaded image is used, not pulled.
|
|
62
|
+
# {{DB_IMAGE}}/{{DB_IMAGE_TAG}} substituted at deploy time in installSupabase.
|
|
63
|
+
image:
|
|
64
|
+
db:
|
|
65
|
+
repository: "{{DB_IMAGE}}"
|
|
66
|
+
tag: "{{DB_IMAGE_TAG}}"
|
|
67
|
+
pullPolicy: IfNotPresent
|
|
68
|
+
|
|
57
69
|
# Component enable toggles — chart's path is deployment.<comp>.enabled.
|
|
58
70
|
# Top-level `functions:`/`analytics:`/`vector:` are silently ignored
|
|
59
71
|
# (was the bug behind the 2026-04-26 e2e helm-wait timeout).
|
|
@@ -81,6 +93,121 @@ deployment:
|
|
|
81
93
|
operator: Equal
|
|
82
94
|
value: supabase
|
|
83
95
|
effect: NoSchedule
|
|
96
|
+
# Headroom for the daily wal-g base backup, which runs INSIDE this
|
|
97
|
+
# container's cgroup (via `kubectl exec`, see k8s/base/backup/cronjob.yaml).
|
|
98
|
+
# backup-push streams PGDATA → lz4 → S3; size the limit for the backup
|
|
99
|
+
# window, not steady state, or it throttles postgres too. WALG_UPLOAD_
|
|
100
|
+
# CONCURRENCY is kept at 2 (vs compose's 4) to bound the burst.
|
|
101
|
+
resources:
|
|
102
|
+
requests:
|
|
103
|
+
cpu: 250m
|
|
104
|
+
memory: 512Mi
|
|
105
|
+
limits:
|
|
106
|
+
cpu: "2"
|
|
107
|
+
memory: 2Gi
|
|
108
|
+
# Mount the fault-tolerant WAL-archive wrapper that postgres'
|
|
109
|
+
# archive_command calls (enabled at runtime via ALTER SYSTEM in
|
|
110
|
+
# applyK3sManifests). Script body lives in the vibecarbon-wal-archive
|
|
111
|
+
# ConfigMap (k8s/base/backup/configmap-walg.yaml) — verbatim copy of
|
|
112
|
+
# carbon/volumes/db/wal-archive.sh (retry + exit-0 prevents disk-fill).
|
|
113
|
+
volumeMounts:
|
|
114
|
+
- name: wal-archive-script
|
|
115
|
+
mountPath: /etc/postgresql/wal-archive.sh
|
|
116
|
+
subPath: wal-archive.sh
|
|
117
|
+
readOnly: true
|
|
118
|
+
- name: walg-s3-creds
|
|
119
|
+
mountPath: /etc/walg
|
|
120
|
+
readOnly: true
|
|
121
|
+
volumes:
|
|
122
|
+
- name: wal-archive-script
|
|
123
|
+
configMap:
|
|
124
|
+
# Invoked as `bash /etc/postgresql/wal-archive.sh` (archive_command),
|
|
125
|
+
# so no exec bit is required on the mounted file.
|
|
126
|
+
name: vibecarbon-wal-archive
|
|
127
|
+
- name: walg-s3-creds
|
|
128
|
+
secret:
|
|
129
|
+
secretName: vibecarbon-secrets
|
|
130
|
+
# Optional so a no-S3 (dev) deploy still schedules the db pod; wal-g
|
|
131
|
+
# is simply unconfigured then. items maps the INI key → credentials.
|
|
132
|
+
optional: true
|
|
133
|
+
items:
|
|
134
|
+
- key: S3_CREDENTIALS_INI
|
|
135
|
+
path: credentials
|
|
136
|
+
# Restore bootstrap: a marker-gated init container that, ONLY when a
|
|
137
|
+
# restore is requested (RESTORE_TARGET set in vibecarbon-secrets), fetches
|
|
138
|
+
# the latest wal-g base backup into PGDATA before postgres starts. On a
|
|
139
|
+
# normal boot RESTORE_TARGET is empty and the init container is a no-op.
|
|
140
|
+
# Uses the same wal-g-equipped db image. RESTORE_TARGET is an OPTIONAL
|
|
141
|
+
# secretKeyRef so deploys without the key still schedule.
|
|
142
|
+
extraInitContainers:
|
|
143
|
+
- name: walg-restore
|
|
144
|
+
image: "{{DB_IMAGE}}:{{DB_IMAGE_TAG}}"
|
|
145
|
+
imagePullPolicy: IfNotPresent
|
|
146
|
+
command:
|
|
147
|
+
- /bin/bash
|
|
148
|
+
- -c
|
|
149
|
+
- |
|
|
150
|
+
set -euo pipefail
|
|
151
|
+
if [ -z "${RESTORE_TARGET:-}" ]; then
|
|
152
|
+
echo "walg-restore: RESTORE_TARGET empty — normal boot, skipping."
|
|
153
|
+
exit 0
|
|
154
|
+
fi
|
|
155
|
+
echo "walg-restore: RESTORE_TARGET=${RESTORE_TARGET} — restoring from S3."
|
|
156
|
+
if [ -f "${PGDATA}/PG_VERSION" ]; then
|
|
157
|
+
echo "walg-restore: existing PGDATA — clearing for backup-fetch."
|
|
158
|
+
rm -rf "${PGDATA:?}/"* "${PGDATA:?}/".* 2>/dev/null || true
|
|
159
|
+
fi
|
|
160
|
+
# Fetch the latest base backup into PGDATA. (For PITR the base that
|
|
161
|
+
# precedes the target is also the latest base — base backups are
|
|
162
|
+
# frequent, so LATEST is the right starting point either way.)
|
|
163
|
+
wal-g backup-fetch "${PGDATA}" LATEST
|
|
164
|
+
# Configure archive recovery. A fetched base backup is only
|
|
165
|
+
# consistent AFTER replaying the WAL from its redo point to its
|
|
166
|
+
# stop point — and that WAL lives in S3, not in the fetched PGDATA.
|
|
167
|
+
# Without restore_command + recovery.signal postgres aborts startup
|
|
168
|
+
# with "could not locate required checkpoint record" (the bug this
|
|
169
|
+
# fixes — the `latest` branch previously did neither). restore_command
|
|
170
|
+
# pulls each segment back from S3 via `wal-g wal-fetch`; recovery.signal
|
|
171
|
+
# puts postgres into archive recovery; on reaching the end of WAL (or
|
|
172
|
+
# the PITR target) it promotes to read-write. (RCA 2026-05-31: e2e
|
|
173
|
+
# k8s/k8s-ha restore — postgres crash-looped post-fetch.)
|
|
174
|
+
{
|
|
175
|
+
echo "restore_command = 'wal-g wal-fetch \"%f\" \"%p\"'"
|
|
176
|
+
echo "recovery_target_action = 'promote'"
|
|
177
|
+
if [ "${RESTORE_TARGET}" != "latest" ]; then
|
|
178
|
+
# PITR: RESTORE_TARGET is an ISO-8601 timestamp — replay only up
|
|
179
|
+
# to that point instead of to the end of the WAL stream.
|
|
180
|
+
echo "recovery_target_time = '${RESTORE_TARGET}'"
|
|
181
|
+
fi
|
|
182
|
+
} >> "${PGDATA}/postgresql.auto.conf"
|
|
183
|
+
touch "${PGDATA}/recovery.signal"
|
|
184
|
+
echo "walg-restore: backup-fetch complete; archive recovery configured."
|
|
185
|
+
env:
|
|
186
|
+
- name: PGDATA
|
|
187
|
+
value: /var/lib/postgresql/data
|
|
188
|
+
- name: RESTORE_TARGET
|
|
189
|
+
valueFrom:
|
|
190
|
+
secretKeyRef:
|
|
191
|
+
name: vibecarbon-secrets
|
|
192
|
+
key: RESTORE_TARGET
|
|
193
|
+
optional: true
|
|
194
|
+
- name: WALG_S3_PREFIX
|
|
195
|
+
value: "{{WALG_S3_PREFIX}}"
|
|
196
|
+
- name: WALG_COMPRESSION_METHOD
|
|
197
|
+
value: lz4
|
|
198
|
+
- name: AWS_ENDPOINT
|
|
199
|
+
value: "{{S3_ENDPOINT}}"
|
|
200
|
+
- name: AWS_REGION
|
|
201
|
+
value: "{{S3_REGION}}"
|
|
202
|
+
- name: AWS_SHARED_CREDENTIALS_FILE
|
|
203
|
+
value: /etc/walg/credentials
|
|
204
|
+
volumeMounts:
|
|
205
|
+
- name: postgres-volume
|
|
206
|
+
mountPath: /var/lib/postgresql/data
|
|
207
|
+
subPath: postgres-data
|
|
208
|
+
- name: walg-s3-creds
|
|
209
|
+
mountPath: /etc/walg
|
|
210
|
+
readOnly: true
|
|
84
211
|
|
|
85
212
|
# Per-component environment — chart's path is environment.<comp>.
|
|
86
213
|
environment:
|
|
@@ -99,6 +226,23 @@ environment:
|
|
|
99
226
|
GOTRUE_MAILER_AUTOCONFIRM: "true"
|
|
100
227
|
realtime:
|
|
101
228
|
DB_NAME: postgres
|
|
229
|
+
# Non-secret WAL-G env for the db container (continuous WAL archiving +
|
|
230
|
+
# the exec'd base-backup CronJob both read these). The chart renders
|
|
231
|
+
# environment.<comp> as plain `value:` strings (no valueFrom), so the
|
|
232
|
+
# SECRET keys (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) are injected
|
|
233
|
+
# separately via a StatefulSet env patch in applyK3sManifests.
|
|
234
|
+
# {{WALG_S3_PREFIX}} resolves to s3://<backup-bucket|storage-bucket>/
|
|
235
|
+
# backups/<project>/walg (fallback handled in installSupabase).
|
|
236
|
+
db:
|
|
237
|
+
WALG_S3_PREFIX: "{{WALG_S3_PREFIX}}"
|
|
238
|
+
WALG_COMPRESSION_METHOD: lz4
|
|
239
|
+
WALG_UPLOAD_CONCURRENCY: "2"
|
|
240
|
+
AWS_ENDPOINT: "{{S3_ENDPOINT}}"
|
|
241
|
+
AWS_REGION: "{{S3_REGION}}"
|
|
242
|
+
# wal-g (AWS SDK) reads S3 creds from this INI file, mounted from
|
|
243
|
+
# vibecarbon-secrets (deployment.db.volumes below). File-mount, not env,
|
|
244
|
+
# so it's helm-owned and survives every upgrade with no db restart.
|
|
245
|
+
AWS_SHARED_CREDENTIALS_FILE: /etc/walg/credentials
|
|
102
246
|
storage:
|
|
103
247
|
STORAGE_BACKEND: s3
|
|
104
248
|
imgproxy:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecarbon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create and manage production-ready Vibecarbon applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./src/cli.js",
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
"fast-xml-builder@<1.1.7": ">=1.1.7",
|
|
77
77
|
"ip-address@<10.1.1": ">=10.1.1",
|
|
78
78
|
"protobufjs@<7.5.6": "^7.5.6",
|
|
79
|
-
"@protobufjs/utf8@<1.1.1": "^1.1.1"
|
|
79
|
+
"@protobufjs/utf8@<1.1.1": "^1.1.1",
|
|
80
|
+
"tmp@<0.2.6": ">=0.2.6"
|
|
80
81
|
}
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
@@ -88,7 +89,6 @@
|
|
|
88
89
|
"autocannon": "^8.0.0",
|
|
89
90
|
"better-sqlite3": "^12.8.0",
|
|
90
91
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
91
|
-
"msw": "^2.12.14",
|
|
92
92
|
"semantic-release": "^25.0.3",
|
|
93
93
|
"tsx": "^4.21.0",
|
|
94
94
|
"vite": "^8.0.14",
|