#!/bin/bash set -euo pipefail # https://stackoverflow.com/a/28085062/18954618 : "${CI:=false}" : "${WITH_REDIS:=false}" : "${SUDO_USER:=""}" NO_COLOR='' RED='' CYAN='' GREEN='' # Check if terminal supports colors https://unix.stackexchange.com/a/10065/642181 if [ -t 1 ]; then total_colors=$(tput colors) if [[ -n "$total_colors" && $total_colors -ge 8 ]]; then # https://stackoverflow.com/a/28938235/18954618 NO_COLOR='\033[0m' RED='\033[0;31m' CYAN='\033[0;36m' GREEN='\033[0;32m' fi fi error_log() { echo -e "${RED}ERROR: $1${NO_COLOR}"; } info_log() { echo -e "${CYAN}INFO: $1${NO_COLOR}"; } error_exit() { error_log "$*" exit 1 } # https://stackoverflow.com/a/18216122/18954618 if [ "$EUID" -ne 0 ]; then error_exit "Please run this script as root user"; fi usage() { echo "Usage: $0 [OPTIONS]" echo "" echo "Self-host Supabase with nginx/caddy and authelia 2FA with just ONE bash script." echo "" echo "Options:" echo " -h, --help Show this help message and exit" echo " --proxy PROXY Set the reverse proxy to use (nginx or caddy). Default: caddy" echo " --with-authelia Enable or disable Authelia 2FA support" echo "" echo "Examples:" echo " $0 --proxy nginx --with-authelia # Set up Supabase with nginx and Authelia 2FA" echo " $0 --proxy caddy # Set up Supabase with caddy and no 2FA" echo "" echo "For more information, visit the project repository:" echo "https://github.com/singh-inder/supabase-automated-self-host" } has_argument() { [[ ("$1" == *=* && -n ${1#*=}) || (-n "$2" && "$2" != -*) ]] } extract_argument() { echo "${2:-${1#*=}}"; } with_authelia=false proxy="caddy" # https://medium.com/@wujido20/handling-flags-in-bash-scripts-4b06b4d0ed04 while [ $# -gt 0 ]; do case "$1" in -h | --help) usage exit 0 ;; --with-authelia) with_authelia=true ;; --proxy) if has_argument "$@"; then proxy="$(extract_argument "$@")" shift fi ;; *) echo -e "ERROR: ${RED}Invalid option:${NO_COLOR} $1" >&2 usage exit 1 ;; esac shift done if [[ "$proxy" != "caddy" && "$proxy" != "nginx" ]]; then error_exit "proxy can only be caddy or nginx" fi info_log "Configuration Summary" echo -e " ${GREEN}Proxy:${NO_COLOR} ${proxy}" echo -e " ${GREEN}Authelia 2FA:${NO_COLOR} ${with_authelia}" detect_arch() { case $(uname -m) in x86_64) echo "amd64" ;; aarch64 | arm64) echo "arm64" ;; armv7l) echo "arm" ;; i686 | i386) echo "386" ;; *) echo "err" ;; esac } #https://stackoverflow.com/a/18434831/18954618 detect_os() { case $(uname | tr '[:upper:]' '[:lower:]') in linux*) echo "linux" ;; # darwin*) echo "darwin" ;; *) echo "err" ;; esac } os="$(detect_os)" arch="$(detect_arch)" if [[ "$os" == "err" ]]; then error_exit "This script only supports linux os"; fi if [[ "$arch" == "err" ]]; then error_exit "Unsupported cpu architecture"; fi packages=(curl wget jq openssl git) # set -e doesn't work if any command is part of an if statement. package installation errors have to be checked https://stackoverflow.com/a/821419/18954618 # https://unix.stackexchange.com/a/571192/642181 if [ -x "$(command -v apt-get)" ]; then apt-get update && apt-get install -y "${packages[@]}" apache2-utils elif [ -x "$(command -v apk)" ]; then apk update && apk add --no-cache "${packages[@]}" apache2-utils elif [ -x "$(command -v dnf)" ]; then dnf makecache && dnf install -y "${packages[@]}" httpd-tools elif [ -x "$(command -v zypper)" ]; then zypper refresh && zypper install "${packages[@]}" apache2-utils elif [ -x "$(command -v pacman)" ]; then pacman -Syu --noconfirm "${packages[@]}" apache elif [ -x "$(command -v pkg)" ]; then pkg update && pkg install -y "${packages[@]}" apache24 elif [[ -x "$(command -v brew)" && -n "$SUDO_USER" ]]; then # brew doesn't allow installation with sudo privileges, thats why have to run script as user who initiated this script with sudo privileges sudo -u "$SUDO_USER" brew install "${packages[@]}" httpd else # diff between array expansion with "@" and "*" https://linuxsimply.com/bash-scripting-tutorial/expansion/array-expansion/ error_exit "Failed to install packages. Package manager not found.\nSupported package managers: apt, apk, dnf, zypper, pacman, pkg, brew" fi if [ $? -ne 0 ]; then error_exit "Failed to install packages."; fi githubAc="https://github.com/singh-inder" repoUrl="$githubAc/supabase-automated-self-host" directory="$(basename "$repoUrl")" if [ -d "$directory" ]; then info_log "$directory directory present, skipping git clone" else git clone --depth=1 "$repoUrl" "$directory" fi if ! cd "$directory"/docker; then error_exit "Unable to access $directory/docker directory"; fi if [ ! -f ".env.example" ]; then error_exit ".env.example file not found. Exiting!"; fi download_binary() { wget "$1" -O "$2" &>/dev/null && chmod +x "$2" &>/dev/null; } urlParserBin="./url-parser" yqBin="./yq" if [ ! -x "$urlParserBin" ]; then info_log "Downloading url-parser from $githubAc/url-parser" download_binary "$githubAc"/url-parser/releases/download/v1.1.0/url-parser-"$os"-"$arch" "$urlParserBin" fi if [ ! -x "$yqBin" ]; then info_log "Downloading yq from https://github.com/mikefarah/yq" download_binary https://github.com/mikefarah/yq/releases/download/v4.45.4/yq_"$os"_"$arch" "$yqBin" fi echo -e "---------------------------------------------------------------------------\n" format_prompt() { echo -e "${GREEN}$1${NO_COLOR}"; } confirmation_prompt() { local variable_to_update_name="$1" local answer="" read -rp "$(format_prompt "$2")" answer # converts input to lowercase case "${answer,,}" in y | yes) answer=true ;; n | no) answer=false ;; *) error_log "Please answer yes or no\n" answer="" ;; esac # Use eval to dynamically assign the new value to the variable name. This indirectly updates the variable in the caller's scope. if [ -n "$answer" ]; then eval "$variable_to_update_name=$answer"; fi } domain="" while [ -z "$domain" ]; do if [ "$CI" == true ]; then domain="https://supabase.example.com" else read -rp "$(format_prompt "Enter your domain:") " domain fi if ! protocol="$("$urlParserBin" --url "$domain" --get scheme 2>/dev/null)"; then error_log "Couldn't extract protocol. Please check the url you entered.\n" domain="" continue fi if ! host="$("$urlParserBin" --url "$domain" --get host 2>/dev/null)"; then error_log "Couldn't extract url host. Please check the url you entered.\n" domain="" continue fi if [[ "$with_authelia" == true ]]; then # cookies.authelia_url needs to be https https://www.authelia.com/configuration/session/introduction/#authelia_url if [[ "$protocol" != "https" ]]; then error_log "As you've enabled --with-authelia flag, url protocol needs to https" domain="" else if ! registered_domain="$("$urlParserBin" --url "$domain" --get registeredDomain 2>/dev/null)" || [ -z "$registered_domain" ] || [ "$registered_domain" = "." ] then error_log "Couldn't extract root domain. Please check the url you entered.\n" domain="" fi fi elif [[ "$protocol" != "http" && "$protocol" != "https" ]]; then error_log "Url protocol must be http or https\n" domain="" fi done username="" if [[ "$CI" == true ]]; then username="inder"; fi while [ -z "$username" ]; do read -rp "$(format_prompt "Enter username:") " username # https://stackoverflow.com/questions/18041761/bash-need-to-test-for-alphanumeric-string if [[ ! "$username" =~ ^[a-zA-Z0-9]+$ ]]; then error_log "Only alphabets and numbers are allowed" username="" fi # read command automatically trims leading & trailing whitespace. No need to handle it separately done password="" confirmPassword="" if [[ "$CI" == true ]]; then password="password" confirmPassword="password" fi while [[ -z "$password" || "$password" != "$confirmPassword" ]]; do read -s -rp "$(format_prompt "Enter password(password is hidden):") " password echo read -s -rp "$(format_prompt "Confirm password:") " confirmPassword echo if [[ "$password" != "$confirmPassword" ]]; then error_log "Password mismatch. Please try again!\n" fi done autoConfirm="" if [[ "$CI" == true ]]; then autoConfirm="false"; fi while [ -z "$autoConfirm" ]; do confirmation_prompt autoConfirm "Do you want to send confirmation emails to register users? If yes, you'll have to setup your own SMTP server [y/n]: " if [[ "$autoConfirm" == true ]]; then autoConfirm="false" elif [[ "$autoConfirm" == false ]]; then autoConfirm="true" fi done # If with_authelia, then additionally ask for email and display name if [[ "$with_authelia" == true ]]; then email="" display_name="" setup_redis="" if [[ "$CI" == true ]]; then email="indersingh.dev@outlook.com" display_name="Inder Singh" if [[ "$WITH_REDIS" == true ]]; then setup_redis=true; fi fi while [ -z "$email" ]; do read -rp "$(format_prompt "Enter your email for Authelia:") " email # split email string on @ symbol IFS="@" read -r before_at after_at <<<"$email" if [[ -z "$before_at" || -z "$after_at" ]]; then error_log "Invalid email" email="" fi done while [ -z "$display_name" ]; do read -rp "$(format_prompt "Enter Display Name:") " display_name if [[ ! "$display_name" =~ ^[a-zA-Z0-9[:space:]]+$ ]]; then error_log "Only alphabets, numbers and spaces are allowed" display_name="" fi done while [[ "$CI" == false && -z "$setup_redis" ]]; do confirmation_prompt setup_redis "Do you want to setup redis with authelia? [y/n]: " done fi info_log "Finishing..." # in caddy basic_auth, hashed password is loaded in memory # in nginx basic_auth, websites slows down a lot if bcrypt rounds number is high as the hashed password file is checked again and again on every request. # This is only applicable when using basic_auth, not with authelia bcryptRounds=12 if [[ "$proxy" == "nginx" && "$with_authelia" == false ]]; then bcryptRounds=6; fi # https://www.baeldung.com/linux/bcrypt-hash#using-htpasswd password=$(htpasswd -bnBC "$bcryptRounds" "" "$password" | cut -d : -f 2) gen_hex() { openssl rand -hex "$1"; } compose_file="docker-compose.yml" caddy_compose_file="docker-compose.caddy.yml" nginx_compose_file="docker-compose.nginx.yml" authelia_compose_file="docker-compose.authelia.yml" compose_file_env="$compose_file" if [ "$proxy" = "caddy" ]; then compose_file_env="${compose_file_env}:$caddy_compose_file" else compose_file_env="${compose_file_env}:$nginx_compose_file" fi if [ "$with_authelia" = true ]; then compose_file_env="${compose_file_env}:$authelia_compose_file" fi sed -e "s|^COMPOSE_FILE=.*$|COMPOSE_FILE=$compose_file_env|" \ -e "s|^API_EXTERNAL_URL=.*$|API_EXTERNAL_URL=$domain/auth/v1|" \ -e "s|^SUPABASE_PUBLIC_URL=.*$|SUPABASE_PUBLIC_URL=$domain|" \ -e "s|^ENABLE_EMAIL_AUTOCONFIRM=.*$|ENABLE_EMAIL_AUTOCONFIRM=$autoConfirm|" .env.example >.env info_log "Generating secrets and legacy API keys" if ! sh utils/generate-keys.sh --update-env; then error_exit "Failed to generate Supabase keys." fi info_log "Generating asymmetric key pair and opaque API keys" if ! sh utils/add-new-auth-keys.sh --update-env; then error_exit "Failed to generate asymmetric Supabase keys." fi update_yaml_file() { # https://github.com/mikefarah/yq/issues/465#issuecomment-2265381565 sed -i '/^\r\{0,1\}$/s// #BLANK_LINE/' "$2" "$yqBin" -i "$1" "$2" sed -i "s/ *#BLANK_LINE//g" "$2" } env_vars="PROXY_DOMAIN=$host" # add env vars in .env file update_env_vars() { for env_key_value in "$@"; do env_vars="${env_vars}\n$env_key_value" done } # START DEFINING proxy_service_yaml proxy_service_yaml="" if [[ "$with_authelia" == true ]]; then proxy_service_yaml=".services.$proxy.depends_on.authelia.condition=\"service_healthy\"" else proxy_service_yaml=".services.$proxy.environment.PROXY_AUTH_USERNAME=\"\${PROXY_AUTH_USERNAME:?error}\" | .services.$proxy.environment.PROXY_AUTH_PASSWORD=\"\${PROXY_AUTH_PASSWORD:?error}\"" update_env_vars "PROXY_AUTH_USERNAME=$username" "PROXY_AUTH_PASSWORD='$password'" fi if [[ "$proxy" == "nginx" && "$CI" == true ]]; then # https://github.com/JonasAlfredsson/docker-nginx-certbot/blob/master/docs/advanced_usage.md#local-ca proxy_service_yaml="${proxy_service_yaml} | .services.nginx.environment.USE_LOCAL_CA=1" fi if [ "$proxy" = "nginx" ]; then update_yaml_file "$proxy_service_yaml" "$nginx_compose_file" else update_yaml_file "$proxy_service_yaml" "$caddy_compose_file" fi if [[ "$with_authelia" == true ]]; then # Dynamically update yaml path from env https://github.com/mikefarah/yq/discussions/1253 # https://mikefarah.gitbook.io/yq/operators/style # WRITE AUTHELIA users_database.yml file # adding disabled=false after updating style to double so that every value except disabled is double quoted yaml_path=".users.$username" displayName="$display_name" password="$password" email="$email" \ "$yqBin" -n 'eval(strenv(yaml_path)).displayname = strenv(displayName) | eval(strenv(yaml_path)).password = strenv(password) | eval(strenv(yaml_path)).email = strenv(email) | eval(strenv(yaml_path)).groups = ["admins","dev"] | .. style="double" | eval(strenv(yaml_path)).disabled = false' >./volumes/authelia/users_database.yml authelia_config_file_yaml='.access_control.rules[0].domain=strenv(host) | .session.cookies[0].domain=strenv(registered_domain) | .session.cookies[0].authelia_url=strenv(authelia_url) | .session.cookies[0].default_redirection_url=strenv(redirect_url)' server_endpoints="forward-auth" implementation="ForwardAuth" if [[ "$proxy" == "nginx" ]]; then server_endpoints="auth-request" implementation="AuthRequest" fi # auth implementation authelia_config_file_yaml="${authelia_config_file_yaml} | .server.endpoints.authz.$server_endpoints.implementation=\"$implementation\"" update_env_vars "AUTHELIA_SCHEMA=authelia" "AUTHELIA_SESSION_SECRET=$(gen_hex 32)" "AUTHELIA_STORAGE_ENCRYPTION_KEY=$(gen_hex 32)" "AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET=$(gen_hex 32)" if [[ "$setup_redis" == true ]]; then authelia_config_file_yaml="${authelia_config_file_yaml}|.session.redis.host=\"redis\" | .session.redis.port=6379" authelia_docker_service_yaml='.services.redis.image="redis:8.2.1" | .services.redis.expose=[6379] | .services.redis.volumes=["./volumes/redis:/data"] | .services.redis.healthcheck={ "test" : ["CMD-SHELL","redis-cli ping | grep PONG"], "timeout" : "5s", "interval" : "1s", "retries" : 5 } | .services.authelia.depends_on.redis.condition="service_healthy"' update_yaml_file "$authelia_docker_service_yaml" "$authelia_compose_file" fi host="$host" registered_domain="$registered_domain" authelia_url="$domain"/authenticate redirect_url="$domain" \ update_yaml_file "$authelia_config_file_yaml" "./volumes/authelia/configuration.yml" # shellcheck disable=SC2016 update_yaml_file '.services.db.environment.AUTHELIA_SCHEMA = "${AUTHELIA_SCHEMA:?error}" | .services.db.volumes += "./volumes/db/schema-authelia.sh:/docker-entrypoint-initdb.d/schema-authelia.sh"' \ "$compose_file" fi echo -e "$env_vars" >>.env proxy_file="" proxy_template_file="" if [ "$proxy" = "caddy" ]; then proxy_file="./volumes/caddy/Caddyfile" proxy_template_file="./volumes/caddy/caddyfile.template" else proxy_file="./volumes/nginx/nginx.conf" proxy_template_file="./volumes/nginx/nginx.template" fi write_proxy_file() { local block_name="$1" local template_file="$2" local new_file="$3" sed "\\|## BLOCK_${block_name}_START ##|,\\|## BLOCK_${block_name}_END ##| s|^\\([[:space:]]*\\)#\\([^#]\\)|\\1\\2|" "$template_file" >"$new_file" } if [ "$with_authelia" = true ]; then write_proxy_file "AUTHELIA" "$proxy_template_file" "$proxy_file" else write_proxy_file "BASIC_AUTH" "$proxy_template_file" "$proxy_file" fi if [[ "$CI" == true && "$proxy" == "caddy" ]]; then sed -i '2i tls internal' "$proxy_file" fi unset password confirmPassword if [ -n "$SUDO_USER" ]; then chown -R "$SUDO_USER": .; fi info_log "Cleaning up!" for bin in "$yqBin" "$urlParserBin"; do rm "$bin"; done echo -e "\nšŸŽ‰ Success!" echo "šŸ‘‰ Next steps:" echo "1. Change into the docker directory:" echo " cd $directory/docker" echo "2. Start the services with Docker Compose:" echo " docker compose up -d" echo "šŸš€ Everything should now be running!" echo -e "\n🌐 To access the dashboard over the internet, ensure your firewall allows traffic on ports 80 and 443\n"