#!/usr/bin/env bash set -Eeuo pipefail DOWNLOAD_BASE="${RAKAZO_DOWNLOAD_BASE:-https://raw.githubusercontent.com/elie222/rakazo/main/infra/compose}" while [[ "$DOWNLOAD_BASE" == */ ]]; do DOWNLOAD_BASE="${DOWNLOAD_BASE%/}" done case "$DOWNLOAD_BASE" in https://*) ;; *) echo "Rakazo setup failed: RAKAZO_DOWNLOAD_BASE must use https." >&2 exit 1 ;; esac readonly DOWNLOAD_BASE readonly COMPOSE_FILE="docker-compose.images.yml" readonly ENV_EXAMPLE=".env.images.example" readonly ENV_FILE=".env" prepare_only=false skip_existing=false pull_never=false if [[ "${RAKAZO_DOWNLOAD_SKIP_EXISTING:-}" == "1" ]]; then skip_existing=true fi if [[ "${RAKAZO_PULL_NEVER:-}" == "1" ]]; then pull_never=true fi for arg in "$@"; do case "$arg" in --prepare-only) prepare_only=true ;; --local) skip_existing=true ;; --pull-never) pull_never=true ;; --offline) # Air-gap bootstrap: keep local Compose/env files and do not pull images. skip_existing=true pull_never=true ;; *) echo "Usage: bash install-images.sh [--prepare-only] [--local] [--pull-never] [--offline]" >&2 exit 2 ;; esac done temporary_file="" cleanup() { if [[ -n "$temporary_file" ]]; then rm -f -- "$temporary_file" fi } trap cleanup EXIT fail() { echo "Rakazo setup failed: $*" >&2 exit 1 } for command_name in curl docker openssl; do command -v "$command_name" >/dev/null 2>&1 || fail "'$command_name' is required." done docker compose version >/dev/null 2>&1 || fail "the Docker Compose plugin is required." # Optional proxy knobs from an existing .env (operators often set them there for # containers). Do not override values already present in the shell. Treat each # HTTP/HTTPS/NO_PROXY pair as one family so either case in the shell wins. # Within .env, later assignments for a family win (shell presence is snapshotted # before the file is read). Comment stripping is quote-aware so `#` inside # matching quotes is kept; this is not a full dotenv parser. load_proxy_vars_from_env_file() { local file="$1" local line key value local i c quote out local shell_http=0 shell_https=0 shell_no=0 [[ -f "$file" ]] || return 0 [[ -n "${HTTP_PROXY+x}" || -n "${http_proxy+x}" ]] && shell_http=1 [[ -n "${HTTPS_PROXY+x}" || -n "${https_proxy+x}" ]] && shell_https=1 [[ -n "${NO_PROXY+x}" || -n "${no_proxy+x}" ]] && shell_no=1 while IFS= read -r line || [[ -n "$line" ]]; do # Windows/.editorconfig CRLF: drop trailing CR so quoted values still match. line="${line%$'\r'}" out="" quote="" for ((i = 0; i < ${#line}; i++)); do c="${line:i:1}" if [[ -n "$quote" ]]; then # Inside double quotes, treat \" and \\ as escaped so \# stays in-value. if [[ "$quote" == '"' && "$c" == '\' ]] && ((i + 1 < ${#line})); then out+="$c" i=$((i + 1)) out+="${line:i:1}" continue fi out+="$c" [[ "$c" == "$quote" ]] && quote="" elif [[ "$c" == "'" || "$c" == '"' ]]; then quote="$c" out+="$c" elif [[ "$c" == "#" ]]; then break else out+="$c" fi done line="$out" [[ "$line" =~ ^[[:space:]]*(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)=(.*)$ ]] || continue key="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then value="${value:1:${#value}-2}" value="${value//\\\"/\"}" value="${value//\\\\/\\}" elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then value="${value:1:${#value}-2}" fi case "$key" in HTTP_PROXY|http_proxy) if ((shell_http)); then continue fi unset -v HTTP_PROXY http_proxy ;; HTTPS_PROXY|https_proxy) if ((shell_https)); then continue fi unset -v HTTPS_PROXY https_proxy ;; NO_PROXY|no_proxy) if ((shell_no)); then continue fi unset -v NO_PROXY no_proxy ;; esac export "${key}=${value}" done <"$file" } # curl uses lowercase http_proxy for http:// URLs; many Mainland hosts only export HTTP_PROXY. sync_curl_proxy_env() { if [[ -n "${HTTP_PROXY+x}" && -z "${http_proxy+x}" ]]; then export http_proxy="$HTTP_PROXY" fi if [[ -n "${HTTPS_PROXY+x}" && -z "${https_proxy+x}" ]]; then export https_proxy="$HTTPS_PROXY" fi if [[ -n "${NO_PROXY+x}" && -z "${no_proxy+x}" ]]; then export no_proxy="$NO_PROXY" fi } prepare_proxy_env() { load_proxy_vars_from_env_file "$ENV_FILE" sync_curl_proxy_env } prepare_proxy_env curl_download() { local url="$1" local out="$2" local attempt local max_attempts=3 if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then curl -fsSL --proto-redir =https --retry 3 --retry-delay 2 --retry-all-errors "$url" -o "$out" return $? fi attempt=1 while [[ "$attempt" -le "$max_attempts" ]]; do if curl -fsSL --proto-redir =https "$url" -o "$out"; then return 0 fi if [[ "$attempt" -eq "$max_attempts" ]]; then return 1 fi sleep 2 attempt=$((attempt + 1)) done return 1 } download() { local filename="$1" local url="${DOWNLOAD_BASE}/${filename}" if [[ "$skip_existing" == true && -e "$filename" ]]; then echo "Using local ${filename}" return 0 fi temporary_file=$(mktemp "./${filename}.tmp.XXXXXX") if ! curl_download "$url" "$temporary_file"; then fail "could not download ${filename} from ${url}. Set HTTP_PROXY/HTTPS_PROXY in the shell or .env (NO_PROXY for localhost), or pre-place the file and use --local / --offline." fi mv -- "$temporary_file" "$filename" temporary_file="" } create_env() { umask 077 temporary_file=$(mktemp "./${ENV_FILE}.tmp.XXXXXX") while IFS= read -r line || [[ -n "$line" ]]; do case "$line" in "POSTGRES_PASSWORD=") printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 16)" ;; "BETTER_AUTH_SECRET=") printf 'BETTER_AUTH_SECRET=%s\n' "$(openssl rand -hex 32)" ;; "ENCRYPTION_KEY=") printf 'ENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" ;; "SCREEN_PROXY_SECRET=") printf 'SCREEN_PROXY_SECRET=%s\n' "$(openssl rand -hex 32)" ;; "SANDBOX_SUPERVISOR_TOKEN=") printf 'SANDBOX_SUPERVISOR_TOKEN=%s\n' "$(openssl rand -hex 32)" ;; *) printf '%s\n' "$line" ;; esac done < "$ENV_EXAMPLE" > "$temporary_file" chmod 600 "$temporary_file" mv -- "$temporary_file" "$ENV_FILE" temporary_file="" echo "Created .env with random secrets." } validate_required_secrets() { if ! docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f - config --environment <<'YAML' | awk ' BEGIN { required["POSTGRES_PASSWORD"] = 1 required["BETTER_AUTH_SECRET"] = 1 required["ENCRYPTION_KEY"] = 1 required["SCREEN_PROXY_SECRET"] = 1 required["SANDBOX_SUPERVISOR_TOKEN"] = 1 } { name = $0 sub(/=.*/, "", name) if (!(name in required)) next seen[name]++ value = $0 sub(/^[^=]*=/, "", value) gsub(/[[:space:]]/, "", value) if (value != "") nonempty[name]++ } END { for (name in required) { if (seen[name] != 1 || nonempty[name] != 1) exit 1 } } ' services: api: environment: _RAKAZO_VALIDATE_POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} _RAKAZO_VALIDATE_BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in .env} _RAKAZO_VALIDATE_ENCRYPTION_KEY: ${ENCRYPTION_KEY:?Set ENCRYPTION_KEY in .env} _RAKAZO_VALIDATE_SCREEN_PROXY_SECRET: ${SCREEN_PROXY_SECRET:?Set SCREEN_PROXY_SECRET in .env} _RAKAZO_VALIDATE_SANDBOX_SUPERVISOR_TOKEN: ${SANDBOX_SUPERVISOR_TOKEN:?Set SANDBOX_SUPERVISOR_TOKEN in .env} YAML then fail "set every required secret in .env to a non-empty value." fi } download "$COMPOSE_FILE" download "$ENV_EXAMPLE" if [[ -e "$ENV_FILE" ]]; then echo "Keeping existing .env." else create_env fi validate_required_secrets if [[ "$prepare_only" == true ]]; then echo "Rakazo files are ready. Edit .env, then run: bash install-images.sh" exit 0 fi prepare_proxy_env if [[ "$pull_never" == true ]]; then echo "Skipping image pull (--pull-never / --offline); images must already be on this Docker host." else if ! docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" pull; then fail "could not pull images. Shell HTTP_PROXY often does not reach the Docker daemon. Configure daemon proxy/registry-mirrors, set image env vars to a reachable registry, or preload images then compose up with --pull never." fi fi # `--wait` without `--wait-timeout` can hang on one-shot services (Compose < 2.7) # or never return if a healthcheck stays red (Compose < 2.17). Prefer both flags. compose_up_help=$(docker compose up --help 2>/dev/null || true) up_pull_args=() if [[ "$pull_never" == true ]]; then if grep -q -- '--pull' <<<"$compose_up_help"; then up_pull_args=(--pull never) else echo "cannot enforce pull-never on this Compose version; startup fails if an image is missing locally" >&2 fi fi # bash 3.2 + set -u: "${arr[@]}" aborts when arr is empty. if grep -q -- '--wait-timeout' <<<"$compose_up_help"; then echo "Waiting for healthy services." docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d ${up_pull_args[@]+"${up_pull_args[@]}"} --wait --wait-timeout 300 else docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d ${up_pull_args[@]+"${up_pull_args[@]}"} fi echo "Rakazo is starting at http://127.0.0.1:5173"