feat(self-hosted): add setup.sh and run.sh scripts for self-hosted supabase (#45603)

This commit is contained in:
Andrey A. authored and GitHub committed 2026-05-22 17:06:58 +02:00
1 parent 6f88585a7e
commit fcb8ee0412
4 files changed
+647 -2

No files matched your search

+15 -1
View File
@@ -1,3 +1,16 @@
############
# Docker compose override files to layer on top of docker-compose.yml.
# Native docker compose COMPOSE_FILE: colon-separated list, base file first.
# Manage with: ./run.sh config add|remove <name>
#
# Examples:
# COMPOSE_FILE=docker-compose.yml
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
#
############
COMPOSE_FILE=docker-compose.yml
############
# Secrets
#
@@ -8,7 +21,8 @@
# https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase
#
# To generate secrets and API keys:
# sh ./utils/generate-keys.sh
# 1. sh utils/generate-keys.sh
# 2. sh utils/add-new-auth-keys.sh
#
############
+5 -1
View File
@@ -13,7 +13,11 @@ See per-service updates below for details.
## Unreleased
⚠️ **Upcoming default changes:** In a future release, several defaults will change: Postgres 15 → 17, Kong → Envoy, MinIO → RustFS, Analytics/Vector removed from the default stack, and the new API keys and authentication replacing the "legacy" architecture. Most of these are already available as optional configurations.
⚠️ **Upcoming changes:** Check the main Supabase [changelog](https://github.com/orgs/supabase/discussions/categories/changelog?discussions_q=is%3Aopen+category%3AChangelog+label%3Aself-hosted) for updates:
- [Making Analytics and Vector opt-in](https://github.com/orgs/supabase/discussions/46084)
- [Upgrading from PG 15 to 17 (breaking change)](https://github.com/orgs/supabase/discussions/46080)
- [Switching Studio from `supabase_admin` to `postgres` (breaking change)](https://github.com/orgs/supabase/discussions/46081)
---
Executable
+297
View File
@@ -0,0 +1,297 @@
#!/bin/sh
#
# Manage the self-hosted Supabase docker compose stack.
#
# Override files are layered via docker compose's native COMPOSE_FILE env
# var in .env. Format: colon-separated list with docker-compose.yml first.
#
# Examples in .env:
# COMPOSE_FILE=docker-compose.yml
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
#
# Manage with: ./run.sh config add <name> | config remove <name>
# (accepts either a short name like 'pg17' or 'docker-compose.pg17.yml')
#
# Usage:
# ./run.sh start # docker compose up -d --wait
# ./run.sh stop # docker compose down
# ./run.sh restart [service] # restart the stack (or named services)
# ./run.sh restart --except <svc>... # restart all services except the named ones
# ./run.sh recreate [service] # stop then start (or force-recreate one service)
# ./run.sh recreate --except <svc>... # force-recreate all services except the named ones
# ./run.sh status # docker compose ps
# ./run.sh logs [service] # follow logs (all or one service)
# ./run.sh inspect <service> # docker inspect on a service's container
# ./run.sh printenv <service> # print a service's environment variables
# ./run.sh pull # pull images
# ./run.sh config # show the active COMPOSE_FILE list
# ./run.sh config add <name> # add an override to COMPOSE_FILE in .env
# ./run.sh config remove <name> # remove an override from COMPOSE_FILE in .env
# ./run.sh compose-config # dump fully-resolved docker compose config
# ./run.sh secrets # print key passwords and API keys from .env
#
set -e
cd "$(dirname "$0")"
if [ ! -f docker-compose.yml ]; then
echo "ERROR: docker-compose.yml not found in $(pwd)" >&2
exit 1
fi
# Normalize an override argument:
# pg17 -> docker-compose.pg17.yml
# docker-compose.pg17.yml -> docker-compose.pg17.yml
# ./docker-compose.pg17.yml -> docker-compose.pg17.yml
# docker-compose.yml -> error (base file, always implicit)
normalize_override() {
arg="${1#./}"
case "$arg" in
docker-compose.yml)
echo "ERROR: docker-compose.yml is the base file, always included" >&2
return 1
;;
docker-compose.*.yml)
echo "$arg"
;;
*)
echo "docker-compose.${arg}.yml"
;;
esac
}
# Read COMPOSE_FILE from .env (stripping quotes and CR).
read_compose_file() {
[ -f .env ] || return 0
grep '^COMPOSE_FILE=' .env | head -n1 | cut -d= -f2- | tr -d "\r\"'"
}
# Pretty-print the effective compose file list.
print_config() {
val="$1"
[ -z "$val" ] && val="docker-compose.yml"
echo "COMPOSE_FILE=$val"
echo "compose files:"
OLD_IFS=$IFS
IFS=:
for f in $val; do
echo " $f"
done
IFS=$OLD_IFS
echo ""
}
# Update or append COMPOSE_FILE in .env.
write_compose_file() {
new_value="$1"
if [ ! -f .env ]; then
echo "ERROR: .env not found in $(pwd)" >&2
exit 1
fi
new_line="COMPOSE_FILE=$new_value"
if grep -q '^COMPOSE_FILE=' .env; then
sed -i.old -e "s|^COMPOSE_FILE=.*$|$new_line|" .env
rm -f .env.old
else
cat >> .env <<EOF
############
# Docker compose override files to layer on top of docker-compose.yml.
# Colon-separated list. Manage with ./run.sh config add|remove <name>.
#
# Examples:
# COMPOSE_FILE=docker-compose.yml
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
############
$new_line
EOF
fi
}
# Echoes the list of services (one per line) minus those passed as args.
# Warns on unknown names; returns 1 if no services remain.
services_except() {
all_services=$(docker compose config --services)
filtered="$all_services"
for ex in "$@"; do
echo "$all_services" | grep -qFx "$ex" \
|| echo "Warning: '$ex' is not a service in this project" >&2
filtered=$(echo "$filtered" | grep -vFx "$ex" || true)
done
if [ -z "$filtered" ]; then
echo "No services left after applying --except" >&2
return 1
fi
printf '%s\n' "$filtered"
}
CMD="${1:-help}"
[ "$#" -gt 0 ] && shift
case "$CMD" in
start|up)
exec docker compose up -d --wait "$@"
;;
stop|down)
exec docker compose down "$@"
;;
restart)
if [ "${1:-}" = "--except" ]; then
shift
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") restart --except <svc>..." >&2; exit 1; }
services=$(services_except "$@") || exit 1
# shellcheck disable=SC2086
exec docker compose restart $services
fi
exec docker compose restart "$@"
;;
recreate)
if [ "${1:-}" = "--except" ]; then
shift
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") recreate --except <svc>..." >&2; exit 1; }
services=$(services_except "$@") || exit 1
# shellcheck disable=SC2086
exec docker compose up -d --wait --force-recreate --no-deps $services
fi
if [ $# -eq 0 ]; then
docker compose down
exec docker compose up -d --wait
fi
# Single-service recreate: force-recreate the named services only,
# leave their dependencies running.
exec docker compose up -d --wait --force-recreate --no-deps "$@"
;;
status|ps)
exec docker compose ps "$@"
;;
logs)
exec docker compose logs -f "$@"
;;
inspect)
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") inspect <service> [docker-inspect-args]" >&2; exit 1; }
svc="$1"; shift
cid=$(docker compose ps -q "$svc")
[ -z "$cid" ] && { echo "Service '$svc' is not running" >&2; exit 1; }
exec docker inspect "$cid" "$@"
;;
printenv)
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") printenv <service>" >&2; exit 1; }
svc="$1"
cid=$(docker compose ps -q "$svc")
[ -z "$cid" ] && { echo "Service '$svc' is not running" >&2; exit 1; }
exec docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$cid"
;;
pull)
exec docker compose pull "$@"
;;
compose-config)
exec docker compose config "$@"
;;
config)
sub="${1:-show}"
[ "$#" -gt 0 ] && shift
current=$(read_compose_file)
case "$sub" in
show)
print_config "$current"
;;
add)
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") config add <name>..." >&2; exit 1; }
new_value="${current:-docker-compose.yml}"
changed=false
for arg in "$@"; do
file=$(normalize_override "$arg") || exit 1
if [ ! -f "$file" ]; then
echo "ERROR: $file not found" >&2
exit 1
fi
case ":$new_value:" in
*":$file:"*) echo "Already present: $file" ;;
*) new_value="$new_value:$file"; changed=true ;;
esac
done
[ "$changed" = true ] && write_compose_file "$new_value"
print_config "$new_value"
;;
remove|rm)
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") config remove <name>..." >&2; exit 1; }
new_value="${current:-docker-compose.yml}"
changed=false
for arg in "$@"; do
file=$(normalize_override "$arg") || exit 1
case ":$new_value:" in
*":$file:"*)
# Drop $file by rebuilding the colon list
tmp=""
OLD_IFS=$IFS
IFS=:
for tok in $new_value; do
[ "$tok" = "$file" ] || tmp="${tmp:+$tmp:}$tok"
done
IFS=$OLD_IFS
new_value="$tmp"
changed=true
;;
*) echo "Not present: $file" ;;
esac
done
[ "$changed" = true ] && write_compose_file "$new_value"
print_config "$new_value"
;;
*)
echo "Unknown config subcommand: $sub" >&2
echo "Use: config | config add <name>... | config remove <name>..." >&2
exit 1
;;
esac
;;
secrets)
if [ ! -f .env ]; then
echo "ERROR: .env not found in $(pwd)" >&2
exit 1
fi
for var in POSTGRES_PASSWORD DASHBOARD_PASSWORD \
SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY \
S3_PROTOCOL_ACCESS_KEY_ID S3_PROTOCOL_ACCESS_KEY_SECRET; do
line=$(grep "^${var}=" .env | head -n1)
if [ -n "$line" ]; then
echo "$line"
else
echo "${var}="
fi
done
echo ""
;;
help|-h|--help)
cat <<EOF
Usage: $(basename "$0") <command>
Commands:
start Start the stack (docker compose up -d --wait)
stop Stop the stack (docker compose down)
restart [service] Restart the stack (or named services)
restart --except <svc>...
Restart all services except the named ones
recreate [service] Stop then start, or force-recreate one service (--no-deps)
recreate --except <svc>...
Force-recreate all services except the named ones (--no-deps)
status Show service status
logs [service] Follow logs (optionally for a single service)
inspect <service> Inspect a service's container (forwards extra args to docker inspect)
printenv <service> Print a service's environment variables (one per line)
pull Pull all images
config Show the active COMPOSE_FILE list
config add <name> Add an override to COMPOSE_FILE in .env (short name or full filename)
config remove <name> Remove an override from COMPOSE_FILE in .env
compose-config Dump the fully-resolved docker compose config
secrets Show key passwords and API keys from .env
EOF
;;
*)
echo "Unknown command: $CMD" >&2
echo "Run '$0 help' for usage." >&2
exit 1
;;
esac
+330
View File
@@ -0,0 +1,330 @@
#!/bin/sh
#
# Bootstrap a self-hosted Supabase project on Linux (Debian/Ubuntu or RHEL/CentOS/Fedora).
#
# What it does:
# 1. Installs prerequisites: git, curl, openssl, jq, ca-certificates
# 2. Installs Docker Engine + Compose plugin (if missing)
# 3. Optionally installs the AWS CLI v2 (--with-aws)
# 4. Sparse-clones the repo to extract the contents of ./docker
# 5. Creates a project directory in CWD and copies docker/* into it
# 6. Prompts for the main URLs and writes them to .env
# 7. Generates secrets and asymmetric API keys via utils/*.sh
#
# Usage:
# sh setup.sh # interactive
# sh setup.sh -y # accept defaults, no prompts
# sh setup.sh --project-dir my-supabase # name the project directory
# sh setup.sh --skip-deps # skip system-package installation
# sh setup.sh --with-aws # also install the AWS CLI v2
#
# curl -fsSL <url-to-this-script> | sh # bootstrap from scratch in CWD
#
set -e
PROJECT_DIR="supabase-project"
SKIP_DEPS=0
WITH_AWS=0
ASSUME_YES=0
print_help() {
cat <<EOF
Usage: setup.sh [options]
Options:
-p, --project-dir <name> Name of the project directory (default: supabase-project)
--skip-deps Skip installation of system packages
--with-aws Install the AWS CLI v2
-y, --yes Non-interactive: accept defaults, no prompts
-h, --help Show this help and exit
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
-p|--project-dir) PROJECT_DIR="$2"; shift 2 ;;
--skip-deps) SKIP_DEPS=1; shift ;;
--with-aws) WITH_AWS=1; shift ;;
-y|--yes) ASSUME_YES=1; shift ;;
-h|--help) print_help; exit 0 ;;
*) echo "Unknown option: $1" >&2; print_help; exit 1 ;;
esac
done
if [ "$(id -u)" = "0" ]; then
SUDO=""
else
SUDO="sudo"
fi
log() { printf "===> %s\n" "$*"; }
warn() { printf "WARNING: %s\n" "$*" >&2; }
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
# Prompt with a default; echoes the chosen value on stdout.
# Reads from /dev/tty so prompts work even when stdin is a pipe (curl | sh).
# Falls back to the default with -y or when no controlling terminal exists.
ask() {
# ask <prompt> <default> -> echoes chosen value
if [ "$ASSUME_YES" = "1" ] || ! { : > /dev/tty; } 2>/dev/null; then
printf '%s' "$2"
return
fi
printf "%s [%s]: " "$1" "$2" > /dev/tty
read -r reply < /dev/tty
[ -z "$reply" ] && reply="$2"
printf '%s' "$reply"
}
OS_FAMILY=""
OS_ID=""
OS_CODENAME=""
detect_os() {
[ -f /etc/os-release ] || die "Cannot detect OS: /etc/os-release missing. Linux only."
# shellcheck disable=SC1091
. /etc/os-release
OS_ID="$ID"
OS_CODENAME="${VERSION_CODENAME:-}"
case "$ID" in
ubuntu|debian) OS_FAMILY="debian" ;;
centos|rhel|fedora|rocky|almalinux|ol|amzn) OS_FAMILY="rhel" ;;
*)
case "${ID_LIKE:-}" in
*debian*|*ubuntu*) OS_FAMILY="debian" ;;
*rhel*|*fedora*|*centos*) OS_FAMILY="rhel" ;;
*) die "Unsupported distribution: $ID" ;;
esac
;;
esac
log "Detected OS: $ID ($OS_FAMILY)"
}
pkg_update() {
if [ "$OS_FAMILY" = "debian" ]; then
$SUDO apt-get update -qq -y
else
$SUDO dnf makecache -q -y || true
fi
}
pkg_install() {
if [ "$OS_FAMILY" = "debian" ]; then
export DEBIAN_FRONTEND=noninteractive
$SUDO apt-get install -qq -y "$@"
else
$SUDO dnf install -q -y "$@"
fi
}
install_base_packages() {
log "Installing base packages: git, curl, openssl, jq, ca-certificates"
pkg_update
if [ "$OS_FAMILY" = "debian" ]; then
pkg_install git curl openssl jq ca-certificates \
apt-transport-https gnupg lsb-release
else
pkg_install git curl openssl jq ca-certificates dnf-plugins-core
fi
}
docker_present() {
command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1
}
install_docker() {
if docker_present; then
log "Docker already installed: $(docker --version)"
return 0
fi
log "Installing Docker Engine and Compose plugin"
if [ "$OS_FAMILY" = "debian" ]; then
$SUDO install -m 0755 -d /etc/apt/keyrings
curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" \
| $SUDO gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
$SUDO chmod a+r /etc/apt/keyrings/docker.gpg
codename="${OS_CODENAME:-$(lsb_release -cs 2>/dev/null || echo stable)}"
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${OS_ID} ${codename} stable" \
| $SUDO tee /etc/apt/sources.list.d/docker.list >/dev/null
$SUDO apt-get update -y
pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
else
repo_distro="centos"
case "$OS_ID" in
fedora) repo_distro="fedora" ;;
rhel) repo_distro="rhel" ;;
esac
$SUDO dnf config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" 2>/dev/null \
|| $SUDO dnf-3 config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo"
pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
fi
log "Enabling and starting docker service"
$SUDO systemctl enable --now docker || warn "Could not enable docker via systemctl; start it manually."
docker_present || die "Docker installation finished but 'docker compose' is still unavailable."
}
install_aws() {
if command -v aws >/dev/null 2>&1; then
log "AWS CLI already installed: $(aws --version 2>&1)"
return 0
fi
arch=$(uname -m)
case "$arch" in
x86_64|amd64) aws_arch="x86_64" ;;
aarch64|arm64) aws_arch="aarch64" ;;
*) die "Unsupported architecture for AWS CLI: $arch" ;;
esac
log "Installing AWS CLI v2 (${aws_arch})"
command -v unzip >/dev/null 2>&1 || pkg_install unzip
tmp=$(mktemp -d)
(
cd "$tmp"
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${aws_arch}.zip" -o awscliv2.zip
unzip -q -o awscliv2.zip
$SUDO ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
)
rm -rf "$tmp"
}
SRC_DIR=""
SRC_TMP=""
prepare_source() {
log "Sparse-cloning supabase repo"
SRC_TMP=$(mktemp -d) || return 1
git clone --filter=blob:none --no-checkout --depth=1 --quiet \
https://github.com/supabase/supabase "$SRC_TMP/supabase" 2>/dev/null || \
{ rm -rf "$SRC_TMP"; return 1; }
cd "$SRC_TMP/supabase" || { rm -rf "$SRC_TMP"; return 1; }
git sparse-checkout init --cone && \
git sparse-checkout set docker && \
git checkout --quiet 2>/dev/null
SRC_DIR="$PWD/docker"
cd - > /dev/null
}
cleanup_src_tmp() {
if [ -n "$SRC_TMP" ] && [ -d "$SRC_TMP" ]; then
rm -rf "$SRC_TMP"
fi
}
trap cleanup_src_tmp EXIT
read_env() {
grep "^$1=" .env 2>/dev/null | head -n1 | cut -d= -f2-
}
# --- Main ---
log "Setup starting in $(pwd)"
log "This may take several minutes..."
if [ "$SKIP_DEPS" = "1" ]; then
log "Skipping system-package installation (--skip-deps)"
else
detect_os
install_base_packages
install_docker
fi
if [ "$WITH_AWS" = "1" ]; then
install_aws
fi
# Idempotent re-run: if CWD is already a set-up project, skip bootstrap.
# A clone has docker-compose.yml + utils/ but only .env.example;
# a set-up project also has a real .env.
if [ -f .env ] && [ -f docker-compose.yml ] && [ -d utils ]; then
log "Already in a Supabase project directory; skipping bootstrap."
exit 0
fi
prepare_source
target="$(pwd)/$PROJECT_DIR"
if [ -e "$target" ]; then
die "Target $target already exists. Pick a different name with --project-dir"
fi
log "Creating project at $target"
mkdir -p "$target"
cp -rf "$SRC_DIR/." "$target/"
if [ -f "$target/.env.example" ] && [ ! -f "$target/.env" ]; then
cp "$target/.env.example" "$target/.env"
fi
cd "$target"
current_public_url=$(read_env SUPABASE_PUBLIC_URL)
current_api_url=$(read_env API_EXTERNAL_URL)
current_site_url=$(read_env SITE_URL)
[ -z "$current_public_url" ] && current_public_url="http://localhost:8000"
[ -z "$current_api_url" ] && current_api_url="$current_public_url"
[ -z "$current_site_url" ] && current_site_url="http://localhost:3000"
if [ "$ASSUME_YES" = "1" ] || ! { : > /dev/tty; } 2>/dev/null; then
log "Non-interactive: keeping default URLs (edit .env to change)"
else
echo ""
echo "Configure the main URLs (press Enter to accept the default)."
echo ""
fi
public_url=$(ask "SUPABASE_PUBLIC_URL (Studio + APIs)" "$current_public_url")
api_url=$(ask "API_EXTERNAL_URL (Auth callbacks)" "$public_url")
site_url=$(ask "SITE_URL (default Auth redirect)" "$current_site_url")
# Suggest PROXY_DOMAIN from the public_url host (unless it's localhost-ish)
public_host=$(printf '%s' "$public_url" | sed -e 's|^https*://||' -e 's|/.*$||' -e 's|:.*$||')
case "$public_host" in
localhost|127.*|"") current_proxy_domain=$(read_env PROXY_DOMAIN) ;;
*) current_proxy_domain="$public_host" ;;
esac
[ -z "$current_proxy_domain" ] && current_proxy_domain="your-domain.example.com"
proxy_domain=$(ask "PROXY_DOMAIN (for nginx/caddy HTTPS proxy)" "$current_proxy_domain")
# Derive CERTBOT_EMAIL = admin@<last-two-labels-of-proxy-domain>.
# Naive: doesn't handle ccTLDs like .co.uk; user can edit .env after.
domain_root=$(printf '%s' "$proxy_domain" | awk -F. 'NF>=2 { print $(NF-1)"."$NF; next } { print }')
certbot_email="admin@${domain_root}"
log "Setting CERTBOT_EMAIL=${certbot_email}"
sed -i.old \
-e "s|^SUPABASE_PUBLIC_URL=.*$|SUPABASE_PUBLIC_URL=${public_url}|" \
-e "s|^API_EXTERNAL_URL=.*$|API_EXTERNAL_URL=${api_url}|" \
-e "s|^SITE_URL=.*$|SITE_URL=${site_url}|" \
-e "s|^PROXY_DOMAIN=.*$|PROXY_DOMAIN=${proxy_domain}|" \
-e "s|^CERTBOT_EMAIL=.*$|CERTBOT_EMAIL=${certbot_email}|" \
.env
rm -f .env.old
log "Generating secrets and legacy API keys"
sh utils/generate-keys.sh --update-env
log "Generating asymmetric key pair and opaque API keys"
sh utils/add-new-auth-keys.sh --update-env
log "Pulling Docker images"
docker compose pull || warn "docker compose pull failed; you can retry later."
echo ""
echo "Setup complete. Project ready at: $(pwd)"
echo ""
echo "Next steps:"
echo " cd $(pwd)"
echo " sh ./run.sh config"
echo " sh ./run.sh secrets"
echo " sh ./run.sh start"
echo ""
echo "To enable docker-compose overrides (pg17, envoy, caddy, nginx, rustfs, s3, logs):"
echo " sh ./run.sh config add pg17"
echo ""