prole/supabase/deploy.sh
chrisfu b03efa8f69 Kong API gateway, docker-import preload, OpenTofu graceful fallback, milestone fix
Kong API Gateway (replacing prole nginx):
- Add etc/init_kong.sh provisioning script (DB-less mode, prole-db namespace)
- Add kong-deployment.yaml and kong-service.yaml manifests
- Rewire ingress rules (svc/git/api.prole.org) to prole-db-kong:8000
- Update kustomization.yaml to reference kong manifests

PostgREST & DB Manager in prole-db namespace:
- Add etc/init_postgrest.sh and etc/init_db_manager.sh scripts
- Add postgrest/db-manager deployment and service manifests
- Add src/db-manager/ Node.js REST endpoint for backup triggers
- Default NAMESPACE changed to prole-db in both scripts

Docker image pre-load from PROLE_DATA/docker-import:
- Add _preload_docker_images() to init_common_services.sh
- Scan for .tar files exported by final_deployment.sh
- Import via k3d image import (k3d) or ctr (k3s) before deployments
- Increase rollout timeouts to 300s (configurable via ROLLOUT_TIMEOUT) in init_openbao.sh, init_opentofu.sh, init_garage_store.sh, init_registry.sh

OpenTofu password resolution fix:
- Add Kubernetes secret fallback in resolve_admin_password()
- Change hard exit 1 to graceful return 1 with warning
- Wrap call in if-guard so set -e doesn't abort the script chain

Milestone fix (init scripts not running):
- Add init_kong.sh, init_postgrest.sh, init_db_manager.sh to InitializationScriptsMilestone.execute() script list and arg branches
- Previously only actions.py had these; milestones.py was missing them

Installer integration:
- Add Kong/PostgREST/DB Manager to silent installer _step_init_scripts
- Add corresponding tabs and execution blocks in UI services.py
2026-02-22 00:57:49 -08:00

945 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [[ -f "$PROJECT_ROOT/env.sh" ]]; then
# shellcheck disable=SC1090
source "$PROJECT_ROOT/env.sh"
fi
MODE="k3d"
USE_DEV_HELPERS="false"
FOREGROUND="false"
FORCE="true"
PROLE_CFG_PATH=""
usage() {
cat <<'USAGE'
Prole::Supabase
Launches the full Supabase open-source stack locally, following the
README/DEVELOPERS guidance (Docker Compose). This is a multi-service deployment
(Postgres, Auth, Storage, Realtime, Kong, Studio, etc.)
Usage:
./deploy.sh [options]
Options:
--mode <local|k3d|k8s> Deployment mode ('local' for Docker Compose, 'k3d' for local k3d, 'k8s' for generic k8s/k3s; default: k3d)
--with-dev-helpers Include docker/dev/docker-compose.dev.yml
--foreground Run docker compose in the foreground (default: detached, local mode only)
-f, --force Reset the Supabase namespace before applying manifests (k3d only; default)
--no-force Skip namespace reset (k3d only)
-c, --config <path> Path to prole.cfg (loads environment defaults)
-h, --help Show help
Notes:
- This script syncs the Supabase repo into $DEV_HOME/supabase.
- The deployment runs as a single "supabase" namespace via the Compose project name.
- For k3d mode, manifests are applied from $SUPABASE_K8S_DIR.
- Docker image artifacts are cached in $DOCKER_IMPORT_DIR.
- This script preserves the README/DEVELOPERS steps:
1) use docker/docker-compose.yml
2) copy docker/.env.example -> docker/.env (if missing)
3) run docker compose up
USAGE
}
die() {
echo "Error: $*" >&2
exit 1
}
warn() {
echo "Warning: $*" >&2
}
log() {
echo "==> $*"
}
apply_defaults() {
DEV_HOME="${DEV_HOME:-$HOME/dev}"
DEV_HOME="${DEV_HOME/#\~/$HOME}"
SUPABASE_DIR="$DEV_HOME/supabase"
DOCKER_DIR="$SUPABASE_DIR/docker"
COMPOSE_FILE="$DOCKER_DIR/docker-compose.yml"
DEV_COMPOSE_FILE="$DOCKER_DIR/dev/docker-compose.dev.yml"
ENV_EXAMPLE="$DOCKER_DIR/.env.example"
ENV_FILE="$DOCKER_DIR/.env"
SUPABASE_K8S_DIR="${SUPABASE_K8S_DIR:-$SCRIPT_DIR/../build/supabase-k8s}"
local default_owner
default_owner="$(id -un 2>/dev/null || echo prole)"
local data_root="${PROLE_DATA:-/opt/prole/data/${default_owner}}"
data_root="${data_root%/}"
DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$data_root/docker-import}"
SUPABASE_IMAGE_PLATFORM="${SUPABASE_IMAGE_PLATFORM:-}"
SUPABASE_IMAGE_PLATFORMS="${SUPABASE_IMAGE_PLATFORMS:-linux/amd64 linux/arm64}"
SUPABASE_POSTGRES_PORT="${SUPABASE_POSTGRES_PORT:-15432}"
PROLE_DB_SERVICE="${PROLE_DB_SERVICE:-prole-db-rw}"
PROLE_DB_NAMESPACE="${PROLE_DB_NAMESPACE:-}"
}
load_prole_cfg() {
local cfg_loader="$PROJECT_ROOT/etc/prole_cfg.sh"
local cfg_path="${PROLE_CFG_PATH:-}"
if [[ -z "$cfg_path" ]]; then
return 0
fi
if [[ -n "$cfg_path" ]]; then
if [[ -d "$cfg_path" ]]; then
cfg_path="$cfg_path/prole.cfg"
fi
[[ -f "$cfg_path" ]] || die "Config file not found: $cfg_path"
PROLE_CONF="$(cd "$(dirname "$cfg_path")" && pwd)"
export PROLE_CONF
fi
if [[ -f "$cfg_loader" ]]; then
# shellcheck disable=SC1090
source "$cfg_loader"
else
warn "prole_cfg.sh not found; config defaults may be incomplete."
fi
}
register_supabase_ports() {
# Register port forwards if helper is available
if command -v prole_register_port_forward >/dev/null 2>&1; then
prole_register_port_forward "supabase-kong" "supabase" "svc/kong" "8000" "8000" "0.0.0.0" "TCP" "Supabase API (Kong)"
prole_register_port_forward "supabase-studio" "supabase" "svc/studio" "8082" "3000" "0.0.0.0" "TCP" "Supabase Studio"
fi
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
detect_platform() {
case "$(uname -m 2>/dev/null || true)" in
arm64|aarch64) echo "linux/arm64" ;;
x86_64|amd64) echo "linux/amd64" ;;
*) echo "linux/amd64" ;;
esac
}
normalize_platform() {
case "${1:-}" in
linux/*) echo "$1" ;;
arm64|aarch64) echo "linux/arm64" ;;
x86_64|amd64) echo "linux/amd64" ;;
"") detect_platform ;;
*) detect_platform ;;
esac
}
compose_cmd() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
return 0
fi
if command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
return 0
fi
die "Docker Compose not found (expected 'docker compose' or 'docker-compose')"
}
ensure_k3d() {
require_cmd k3d
}
ensure_kompose() {
require_cmd kompose
}
list_k3d_clusters() {
local json=""
if json="$(k3d cluster list -o json 2>/dev/null)"; then
if [[ -n "$json" ]]; then
if printf '%s' "$json" | python - <<'PY'
import json
import sys
raw = sys.stdin.read()
if not raw.strip():
sys.exit(1)
try:
data = json.loads(raw)
except Exception:
sys.exit(1)
items = data.get("items") if isinstance(data, dict) else data
if not items:
sys.exit(0)
for item in items:
name = item.get("name") if isinstance(item, dict) else None
if name:
print(name)
PY
then
return 0
fi
fi
fi
k3d cluster list 2>/dev/null | awk 'NR>1 {print $1}'
}
cluster_exists() {
local name="$1"
list_k3d_clusters | awk -v target="$name" '$0 == target {found=1} END {exit found ? 0 : 1}'
}
ensure_k3d_cluster() {
local cluster="${K3D_CLUSTER_NAME:-}"
local -a clusters
local picked=""
mapfile -t clusters < <(list_k3d_clusters || true)
if [[ -n "$cluster" ]]; then
if ! cluster_exists "$cluster"; then
log "Creating k3d cluster '$cluster'..."
k3d cluster create "$cluster" >/dev/null
fi
picked="$cluster"
elif [[ ${#clusters[@]} -gt 0 ]]; then
for name in "${clusters[@]}"; do
if [[ "$name" == "k3s-default" ]]; then
picked="$name"
break
fi
done
if [[ -z "$picked" ]]; then
picked="${clusters[0]}"
fi
else
picked="k3s-default"
log "Creating k3d cluster '$picked'..."
k3d cluster create "$picked" >/dev/null
fi
export K3D_CLUSTER_NAME="$picked"
log "Using k3d cluster: $K3D_CLUSTER_NAME"
k3d cluster start "$K3D_CLUSTER_NAME" >/dev/null 2>&1 || true
k3d kubeconfig merge "$K3D_CLUSTER_NAME" --switch-context >/dev/null 2>&1 || true
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--mode|-m)
MODE="${2:-}"
shift 2
;;
-c|--config)
PROLE_CFG_PATH="${2:-}"
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
shift 2
;;
--config=*)
PROLE_CFG_PATH="${1#*=}"
[[ -n "$PROLE_CFG_PATH" ]] || die "Missing value for $1"
shift
;;
--with-dev-helpers)
USE_DEV_HELPERS="true"
shift
;;
--foreground)
FOREGROUND="true"
shift
;;
-f|--force)
FORCE="true"
shift
;;
--no-force)
FORCE="false"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
}
ensure_env_file() {
if [[ -f "$ENV_FILE" ]]; then
return 0
fi
[[ -f "$ENV_EXAMPLE" ]] || die "Missing env example file: $ENV_EXAMPLE"
log "Creating docker/.env from docker/.env.example"
cp "$ENV_EXAMPLE" "$ENV_FILE"
warn "docker/.env contains default secrets. Update them before any production use."
}
compose_files() {
local -a files=("-f" "docker-compose.yml")
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
[[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE"
files+=("-f" "dev/docker-compose.dev.yml")
fi
echo "${files[@]}"
}
ensure_repo() {
require_cmd git
mkdir -p "$DEV_HOME"
if [[ -d "$SUPABASE_DIR/.git" ]]; then
log "Updating Supabase repo in $SUPABASE_DIR"
(cd "$SUPABASE_DIR" && git pull)
return 0
fi
if [[ -e "$SUPABASE_DIR" ]]; then
die "Path exists but is not a git repo: $SUPABASE_DIR"
fi
log "Cloning Supabase repo into $SUPABASE_DIR"
git clone https://github.com/supabase/supabase.git "$SUPABASE_DIR"
}
run_local() {
ensure_repo
[[ -f "$COMPOSE_FILE" ]] || die "Missing compose file: $COMPOSE_FILE"
require_cmd docker
docker info >/dev/null 2>&1 || die "Docker daemon is not running"
local compose
compose="$(compose_cmd)"
log "Prole::Supabase local deploy (Docker Compose)"
log "Repo: $SUPABASE_DIR"
log "This follows README/DEVELOPERS guidance for running the full open-source stack."
log "Namespace: supabase (Compose project name)"
ensure_env_file
local compose_args=("-f" "docker-compose.yml")
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
[[ -f "$DEV_COMPOSE_FILE" ]] || die "Missing dev helpers compose file: $DEV_COMPOSE_FILE"
compose_args+=("-f" "dev/docker-compose.dev.yml")
fi
local up_args=("up")
if [[ "$FOREGROUND" != "true" ]]; then
up_args+=("-d")
fi
log "Starting Supabase services..."
(cd "$DOCKER_DIR" && COMPOSE_PROJECT_NAME="supabase" $compose "${compose_args[@]}" "${up_args[@]}")
log "Deployment complete."
log "Access Studio at http://localhost:8082 (see docker/.env for ports)."
}
image_safe_name() {
echo "$1" | sed 's/[\/:@]/_/g'
}
image_platform_tag() {
local img="$1"
local platform="$2"
local suffix="${platform//\//-}"
local name tag
if [[ "$img" == *@* ]]; then
name="${img%@*}"
tag="digest-${suffix}"
elif [[ "$img" == *:* ]]; then
name="${img%:*}"
tag="${img##*:}-${suffix}"
else
name="$img"
tag="latest-${suffix}"
fi
echo "${name}:${tag}"
}
ensure_image_artifact() {
local img="$1"
local platform="$2"
local safe_name
local tar_path
local platform_tag
safe_name="$(image_safe_name "$img")"
tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar"
platform_tag="$(image_platform_tag "$img" "$platform")"
if [[ -f "$tar_path" ]]; then
return 0
fi
mkdir -p "$DOCKER_IMPORT_DIR"
# Prefer buildx export to reliably produce single-arch tarballs with containerd-backed Docker
if docker buildx version >/dev/null 2>&1; then
log "Fetching image $img for platform $platform (buildx export)"
if ! printf 'FROM %s\n' "$img" \
| docker buildx build --pull --platform "$platform" -t "$platform_tag" \
--output "type=docker,dest=$tar_path" - >/dev/null 2>&1; then
die "Failed to export image $img ($platform) via buildx"
fi
return 0
fi
# Fallback: traditional pull + save (may fail on some Docker/containerd combos)
log "Fetching image $img for platform $platform"
if ! docker pull --platform "$platform" "$img" >/dev/null 2>&1; then
if docker image inspect "$img" >/dev/null 2>&1; then
warn "Using local image for $img (pull failed for $platform)"
else
die "Failed to pull image for $img ($platform)"
fi
fi
docker tag "$img" "$platform_tag" >/dev/null 2>&1 || true
log "Saving $img ($platform) to $tar_path"
if ! docker save -o "$tar_path" "$platform_tag" >/dev/null 2>&1; then
die "Failed to save image $img ($platform) to $tar_path"
fi
}
load_image_for_platform() {
local img="$1"
local platform="$2"
local safe_name
local tar_path
local platform_tag
safe_name="$(image_safe_name "$img")"
tar_path="${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar"
platform_tag="$(image_platform_tag "$img" "$platform")"
if [[ ! -f "$tar_path" ]]; then
ensure_image_artifact "$img" "$platform"
fi
log "Loading $img ($platform) from $tar_path"
docker load -i "$tar_path" >/dev/null
docker tag "$platform_tag" "$img"
}
write_artifact_manifest() {
local images="$1"
local list_path="${DOCKER_IMPORT_DIR}/supabase-images.txt"
mkdir -p "$DOCKER_IMPORT_DIR"
printf "%s\n" $images > "$list_path"
local platform
for platform in $SUPABASE_IMAGE_PLATFORMS; do
local platform_list="${DOCKER_IMPORT_DIR}/supabase-images-${platform//\//-}.txt"
: > "$platform_list"
for img in $images; do
local safe_name
safe_name="$(image_safe_name "$img")"
echo "${DOCKER_IMPORT_DIR}/${safe_name}.${platform//\//-}.tar" >> "$platform_list"
done
done
}
prefetch_k3d_images() {
require_cmd docker
docker info >/dev/null 2>&1 || die "Docker daemon is not running"
local compose
compose="$(compose_cmd)"
ensure_env_file
local files
files=($(compose_files))
local images
images=$(cd "$DOCKER_DIR" && $compose "${files[@]}" --env-file ".env" config --images | awk 'NF' | sort -u)
if [[ -z "$images" ]]; then
die "No images found in Supabase compose config"
fi
log "Supabase images discovered:"
printf " - %s\n" $images
write_artifact_manifest "$images"
log "Image artifact list written to $DOCKER_IMPORT_DIR/supabase-images.txt"
local platform
for platform in $SUPABASE_IMAGE_PLATFORMS; do
log "Ensuring artifacts for platform '$platform' in $DOCKER_IMPORT_DIR"
for img in $images; do
ensure_image_artifact "$img" "$platform"
done
done
local deploy_platform
deploy_platform="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
log "Loading images for platform '$deploy_platform'"
for img in $images; do
load_image_for_platform "$img" "$deploy_platform"
done
# shellcheck disable=SC2086
if [[ -n "${K3D_CLUSTER_NAME:-}" ]]; then
k3d image import $images -c "$K3D_CLUSTER_NAME"
else
k3d image import $images
fi
}
resolve_prole_db_namespace() {
if [[ -n "${PROLE_DB_NAMESPACE:-}" ]]; then
return 0
fi
if [[ -n "${NAMESPACE:-}" ]]; then
PROLE_DB_NAMESPACE="$NAMESPACE"
return 0
fi
if kubectl get namespace prole >/dev/null 2>&1; then
PROLE_DB_NAMESPACE="prole"
else
PROLE_DB_NAMESPACE="default"
fi
}
patch_db_deployment_port() {
local file="$SUPABASE_K8S_DIR/db-deployment.yaml"
if [[ ! -f "$file" ]]; then
return 0
fi
local port="$SUPABASE_POSTGRES_PORT"
python - "$file" "$port" <<'PY'
import re
import sys
path = sys.argv[1]
port = sys.argv[2]
with open(path, "r", encoding="utf-8") as fh:
data = fh.read()
data = re.sub(r"(name:\s*PGPORT\s*\n\s*value:\s*)\"[^\"]+\"",
rf"\1\"{port}\"", data)
data = re.sub(r"(name:\s*POSTGRES_PORT\s*\n\s*value:\s*)\"[^\"]+\"",
rf"\1\"{port}\"", data)
with open(path, "w", encoding="utf-8") as fh:
fh.write(data)
PY
}
write_supabase_postgres_service() {
local file="$SUPABASE_K8S_DIR/supabase-postgres-service.yaml"
cat > "$file" <<EOF
apiVersion: v1
kind: Service
metadata:
name: supabase-postgres
namespace: supabase
labels:
app: supabase-postgres
spec:
type: ExternalName
externalName: ${PROLE_DB_SERVICE}.${PROLE_DB_NAMESPACE}.svc.cluster.local
ports:
- name: postgres
port: ${SUPABASE_POSTGRES_PORT}
targetPort: 5432
EOF
}
write_db_alias_service() {
local file="$SUPABASE_K8S_DIR/db-service.yaml"
cat > "$file" <<EOF
apiVersion: v1
kind: Service
metadata:
name: db
namespace: supabase
labels:
app: supabase-db
spec:
type: ExternalName
externalName: ${PROLE_DB_SERVICE}.${PROLE_DB_NAMESPACE}.svc.cluster.local
ports:
- name: postgres
port: 5432
targetPort: 5432
EOF
}
write_rest_alias_service() {
local file="$SUPABASE_K8S_DIR/rest-service.yaml"
cat > "$file" <<EOF
apiVersion: v1
kind: Service
metadata:
name: rest
namespace: supabase
labels:
app: supabase-rest
spec:
type: ExternalName
externalName: prole-db-postgrest.${PROLE_DB_NAMESPACE}.svc.cluster.local
ports:
- name: http
port: 3000
targetPort: 3000
EOF
}
fix_k8s_manifests() {
log "Post-processing Kubernetes manifests for k8s compatibility..."
# 1. Fix db data volume: configMap -> emptyDir (postgres needs writable data dir)
local db_deploy="$SUPABASE_K8S_DIR/db-deployment.yaml"
if [[ -f "$db_deploy" ]]; then
python3 - "$db_deploy" <<'PYFIX'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
# Replace the db-cm4 configMap volume definition with emptyDir
# Matches: - configMap:\n name: db-cm4\n name: db-cm4
content = re.sub(
r'(\s+)-\s+configMap:\s*\n\s+name:\s*db-cm4\s*\n(\s+name:\s*db-cm4)',
r'\1- emptyDir: {}\n\2',
content
)
# Also handle the alternate format kompose might produce
content = re.sub(
r'(\s+)-\s+configMap:\s*\n\s+defaultMode:\s*\d+\s*\n\s+name:\s*db-cm4\s*\n(\s+name:\s*db-cm4)',
r'\1- emptyDir: {}\n\2',
content
)
with open(path, 'w') as f:
f.write(content)
print(f" [OK] db data volume -> emptyDir")
PYFIX
fi
# 2. Remove db-cm4 configmap (no longer needed; postgres inits its own data dir)
local db_cm4="$SUPABASE_K8S_DIR/db-cm4-configmap.yaml"
if [[ -f "$db_cm4" ]]; then
rm -f "$db_cm4"
log " Removed db-cm4-configmap.yaml (data dir handled by emptyDir)"
fi
# 3. Scale vector to 0 replicas (docker_logs source needs docker.sock, unavailable in k8s)
local vector_deploy="$SUPABASE_K8S_DIR/vector-deployment.yaml"
if [[ -f "$vector_deploy" ]]; then
python3 - "$vector_deploy" <<'PYFIX'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1)
with open(path, 'w') as f:
f.write(content)
print(" [OK] vector replicas -> 0 (docker_logs incompatible with k8s)")
PYFIX
fi
# 4. Scale functions to 0 replicas (edge-runtime needs function files not present in k8s)
local functions_deploy="$SUPABASE_K8S_DIR/functions-deployment.yaml"
if [[ -f "$functions_deploy" ]]; then
python3 - "$functions_deploy" <<'PYFIX'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1)
with open(path, 'w') as f:
f.write(content)
print(" [OK] functions replicas -> 0 (edge-runtime entrypoint not available in k8s)")
PYFIX
fi
# 5. Scale rest (PostgREST) to 0 replicas (using prole-db postgrest instead)
local rest_deploy="$SUPABASE_K8S_DIR/rest-deployment.yaml"
if [[ -f "$rest_deploy" ]]; then
python3 - "$rest_deploy" <<'PYFIX'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1)
with open(path, 'w') as f:
f.write(content)
print(" [OK] rest replicas -> 0 (using prole-db postgrest)")
PYFIX
fi
# 6. Scale db deployment to 0 replicas (using prole-db instead of supabase standalone postgres)
if [[ -f "$db_deploy" ]]; then
python3 - "$db_deploy" <<'PYFIX'
import sys, re
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
content = re.sub(r'(spec:\s*\n\s+replicas:)\s*\d+', r'\1 0', content, count=1)
with open(path, 'w') as f:
f.write(content)
print(" [OK] db replicas -> 0 (using prole-db as database backend)")
PYFIX
fi
# 7. Fix liveness probes that reference Docker Compose service hostnames
# In Docker Compose, service names resolve via internal DNS, but in k8s
# there is no matching Service for every container. Rewrite probes to
# use localhost (the probe checks the container's own health endpoint).
local storage_deploy="$SUPABASE_K8S_DIR/storage-deployment.yaml"
if [[ -f "$storage_deploy" ]]; then
python3 - "$storage_deploy" <<'PYFIX'
import sys
path = sys.argv[1]
with open(path, 'r') as f:
content = f.read()
content = content.replace('http://storage:5000/status', 'http://localhost:5000/status')
with open(path, 'w') as f:
f.write(content)
print(" [OK] storage liveness probe -> localhost:5000 (no k8s Service needed)")
PYFIX
fi
}
generate_k8s_manifests() {
ensure_kompose
ensure_env_file
local files
files=($(compose_files))
rm -rf "$SUPABASE_K8S_DIR"
mkdir -p "$SUPABASE_K8S_DIR"
log "Generating Kubernetes manifests from docker-compose.yml"
(cd "$DOCKER_DIR" && kompose "${files[@]}" -n supabase -o "$SUPABASE_K8S_DIR" --volumes=configMap --suppress-warnings convert)
fix_k8s_manifests
resolve_prole_db_namespace
write_db_alias_service
write_rest_alias_service
patch_db_deployment_port
write_supabase_postgres_service
sanitize_container_names
}
sanitize_container_names() {
log "Sanitizing container names in manifests..."
local files
files=("$SUPABASE_K8S_DIR"/*-deployment.yaml)
for file in "${files[@]}"; do
[[ -f "$file" ]] || continue
# Replace dots with hyphens in container names (spec.template.spec.containers[].name)
python - "$file" <<'PY'
import sys
import re
path = sys.argv[1]
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
def replace_dots(match):
prefix = match.group(1)
name = match.group(2)
if '.' in name:
return prefix + name.replace('.', '-')
return match.group(0)
# Match 'name: some.container.name' with at least 4 spaces of indentation
# This helps avoid metadata.name which usually has 2 spaces.
new_content = re.sub(r'(\s{4,}name:\s+)([^\n]+)', replace_dots, content)
if new_content != content:
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
PY
done
}
ensure_supabase_namespace() {
if ! kubectl get namespace supabase >/dev/null 2>&1; then
log "Creating 'supabase' namespace..."
kubectl create namespace supabase
fi
}
force_reset_supabase_namespace() {
if ! kubectl get namespace supabase >/dev/null 2>&1; then
ensure_supabase_namespace
return
fi
log "Resetting 'supabase' namespace..."
# Delete all workloads first to avoid finalizer stalls
kubectl delete all --all -n supabase --timeout=60s >/dev/null 2>&1 || true
# Request namespace deletion without blocking
kubectl delete namespace supabase --ignore-not-found --wait=false >/dev/null 2>&1 || true
if kubectl get namespace supabase >/dev/null 2>&1; then
if ! kubectl wait --for=delete namespace/supabase --timeout=120s >/dev/null 2>&1; then
warn "Namespace deletion stalled; clearing finalizers"
python - <<'PY' | kubectl replace --raw "/api/v1/namespaces/supabase/finalize" -f - >/dev/null 2>&1 || true
import json
import subprocess
import sys
try:
raw = subprocess.check_output(["kubectl", "get", "namespace", "supabase", "-o", "json"])
if not raw:
sys.exit(0)
data = json.loads(raw)
if "spec" in data:
data["spec"]["finalizers"] = []
print(json.dumps(data))
except Exception:
sys.exit(0)
PY
kubectl wait --for=delete namespace/supabase --timeout=60s >/dev/null 2>&1 || true
fi
fi
ensure_supabase_namespace
}
run_k3d() {
ensure_repo
log "Prole::Supabase k3d deploy (Kubernetes)"
log "Repo: $SUPABASE_DIR"
require_cmd kubectl
ensure_k3d
ensure_k3d_cluster
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
if [[ "$FORCE" == "true" ]]; then
force_reset_supabase_namespace
else
ensure_supabase_namespace
fi
export SUPABASE_HOME="$SUPABASE_DIR"
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
export SUPABASE_USE_DEV_COMPOSE=1
fi
export SUPABASE_IMAGE_PLATFORM
SUPABASE_IMAGE_PLATFORM="$(normalize_platform "${SUPABASE_IMAGE_PLATFORM:-}")"
prefetch_k3d_images
generate_k8s_manifests
if [[ -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" ]]; then
kubectl apply -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml"
fi
log "Applying Supabase manifests from $SUPABASE_K8S_DIR"
kubectl apply -f "$SUPABASE_K8S_DIR"
register_supabase_ports
log "Deployment complete."
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
}
run_k8s() {
ensure_repo
log "Prole::Supabase k8s deploy (Kubernetes - generic/containerd)"
log "Repo: $SUPABASE_DIR"
require_cmd kubectl
kubectl cluster-info >/dev/null 2>&1 || die "Kubernetes cluster not reachable"
if [[ "$FORCE" == "true" ]]; then
force_reset_supabase_namespace
else
ensure_supabase_namespace
fi
export SUPABASE_HOME="$SUPABASE_DIR"
if [[ "$USE_DEV_HELPERS" == "true" ]]; then
export SUPABASE_USE_DEV_COMPOSE=1
fi
# For generic k8s/k3s clusters (containerd), let nodes pull appropriate arch images directly
# Optionally, a future enhancement could push pre-fetched images to an internal registry.
generate_k8s_manifests
if [[ -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml" ]]; then
kubectl apply -f "$SUPABASE_K8S_DIR/supabase-namespace.yaml"
fi
log "Applying Supabase manifests from $SUPABASE_K8S_DIR"
kubectl apply -f "$SUPABASE_K8S_DIR"
register_supabase_ports
log "Deployment complete."
log "Access Studio via Kong proxy (check ingress/service in 'supabase' namespace)."
}
main() {
parse_args "$@"
load_prole_cfg
apply_defaults
if [[ -z "$MODE" ]]; then
usage
exit 1
fi
case "$MODE" in
local)
run_local
;;
k3d)
run_k3d
;;
k8s)
run_k8s
;;
*)
die "Unsupported mode: $MODE"
;;
esac
}
main "$@"