#!/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: sh run.sh config add | config remove # (accepts either a short name like 'pg17' or 'docker-compose.pg17.yml') # # Usage: # sh run.sh start # docker compose up -d --wait # sh run.sh stop # docker compose down # sh run.sh restart [service] # restart the stack (or named services) # sh run.sh restart --except ... # restart all services except the named ones # sh run.sh recreate [service] # stop then start (or force-recreate one service) # sh run.sh recreate --except ... # force-recreate all services except the named ones # sh run.sh status # docker compose ps # sh run.sh logs [service] # follow logs (all or one service) # sh run.sh inspect # docker inspect on a service's container # sh run.sh printenv # print a service's environment variables # sh run.sh pull # pull images # sh run.sh config # show the active COMPOSE_FILE list # sh run.sh config add # add an override to COMPOSE_FILE in .env # sh run.sh config remove # remove an override from COMPOSE_FILE in .env # sh run.sh compose-config # dump fully-resolved docker compose config # sh 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 <. # # 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 ..." >&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 ..." >&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 [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 " >&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 ..." >&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 ..." >&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 ... | config remove ..." >&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 < 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 ... Restart all services except the named ones recreate [service] Stop then start, or force-recreate one service (--no-deps) recreate --except ... Force-recreate all services except the named ones (--no-deps) status Show service status logs [service] Follow logs (optionally for a single service) inspect Inspect a service's container (forwards extra args to docker inspect) printenv Print a service's environment variables (one per line) pull Pull all images config Show the active COMPOSE_FILE list config add Add an override to COMPOSE_FILE in .env (short name or full filename) config remove 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