#!/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 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 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}" DOCKER_IMPORT_DIR="${DOCKER_IMPORT_DIR:-$PROJECT_ROOT/data/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 } 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" < "$file" </dev/null 2>&1; then warn "Prole DB service not found: ${PROLE_DB_SERVICE} in namespace ${PROLE_DB_NAMESPACE}" fi patch_db_deployment_port write_supabase_postgres_service write_db_alias_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() { log "Resetting 'supabase' namespace..." kubectl delete namespace supabase --ignore-not-found >/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=120s >/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" 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" 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 "$@"