#!/bin/sh set -eu umask 077 CONFIG_SOURCE="${GROK2API_CONFIG_SOURCE:-/run/grok2api/config.yaml}" APP_CONFIG="/app/config.yaml" SECRETS_FILE_NAME=".grok2api-secrets.env" log() { printf '%s\n' "$*" >&2 } die() { log "error: $*" exit 1 } yaml_quote() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' } is_truthy() { case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in 1|true|yes|on) return 0 ;; *) return 1 ;; esac } has_writable_dir() { dir="$1" [ -d "$dir" ] || return 1 [ -w "$dir" ] || return 1 return 0 } resolve_data_root() { if [ -n "${GROK2API_DATA_DIR:-}" ]; then printf '%s' "$GROK2API_DATA_DIR" return fi if has_writable_dir /data; then printf '%s' /data return fi printf '%s' /app/data } random_hex() { dd if=/dev/urandom bs="$1" count=1 2>/dev/null | od -An -tx1 | tr -d ' \n' } random_base64_32() { dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n' } shell_single_quote() { printf "%s" "$1" | sed "s/'/'\\\\''/g" } persist_secrets() { secrets_file="$1" jwt_secret="$2" credential_key="$3" tmp="${secrets_file}.tmp" { printf "export JWT_SECRET='%s'\n" "$(shell_single_quote "$jwt_secret")" printf "export CREDENTIAL_ENCRYPTION_KEY='%s'\n" "$(shell_single_quote "$credential_key")" } >"$tmp" mv "$tmp" "$secrets_file" chmod 0600 "$secrets_file" 2>/dev/null || true } resolve_listen() { if [ -n "${GROK2API_LISTEN:-}" ]; then printf '%s' "$GROK2API_LISTEN" return fi if [ -n "${PORT:-}" ]; then printf '0.0.0.0:%s' "$PORT" return fi if [ -n "${SPACE_ID:-}" ] || [ -n "${SPACE_HOST:-}" ] || is_truthy "${HF_SPACE:-}" || is_truthy "${GROK2API_HF:-}"; then printf '%s' "0.0.0.0:7860" return fi printf '%s' "0.0.0.0:8000" } resolve_public_api_base_url() { if [ -n "${PUBLIC_API_BASE_URL:-}" ]; then printf '%s' "${PUBLIC_API_BASE_URL%/}" return fi if [ -n "${GROK2API_PUBLIC_API_BASE_URL:-}" ]; then printf '%s' "${GROK2API_PUBLIC_API_BASE_URL%/}" return fi if [ -n "${SPACE_HOST:-}" ]; then printf 'https://%s' "$SPACE_HOST" return fi printf '%s' "" } resolve_secure_cookies() { if [ -n "${SECURE_COOKIES:-}" ]; then if is_truthy "$SECURE_COOKIES"; then printf '%s' "true" else printf '%s' "false" fi return fi if [ -n "${SPACE_ID:-}" ] || [ -n "${SPACE_HOST:-}" ] || is_truthy "${HF_SPACE:-}" || is_truthy "${GROK2API_HF:-}"; then printf '%s' "true" return fi printf '%s' "false" } prepare_data_dirs() { data_root="$1" mkdir -p "$data_root" "$data_root/media" if [ "$(id -u)" -eq 0 ]; then chown -R grok2api:grok2api "$data_root" 2>/dev/null || true fi } resolve_secret_value() { env_primary="$1" env_alt="$2" file_value="$3" if [ -n "$env_primary" ]; then printf '%s' "$env_primary" return fi if [ -n "$env_alt" ]; then printf '%s' "$env_alt" return fi printf '%s' "$file_value" } generate_config_from_env() { data_root="$1" secrets_file="$data_root/$SECRETS_FILE_NAME" env_jwt="${JWT_SECRET:-}" env_jwt_alt="${GROK2API_JWT_SECRET:-}" env_key="${CREDENTIAL_ENCRYPTION_KEY:-}" env_key_alt="${GROK2API_CREDENTIAL_ENCRYPTION_KEY:-}" file_jwt="" file_key="" if [ -f "$secrets_file" ]; then # shellcheck disable=SC1090 . "$secrets_file" file_jwt="${JWT_SECRET:-}" file_key="${CREDENTIAL_ENCRYPTION_KEY:-}" fi jwt_secret="$(resolve_secret_value "$env_jwt" "$env_jwt_alt" "$file_jwt")" credential_key="$(resolve_secret_value "$env_key" "$env_key_alt" "$file_key")" generated_secrets=0 if [ -z "$jwt_secret" ]; then jwt_secret="$(random_hex 32)" generated_secrets=1 fi if [ -z "$credential_key" ]; then credential_key="$(random_base64_32)" generated_secrets=1 fi if [ "${#jwt_secret}" -lt 32 ]; then die "JWT_SECRET must be at least 32 characters" fi admin_user="${BOOTSTRAP_ADMIN_USERNAME:-${GROK2API_BOOTSTRAP_ADMIN_USERNAME:-admin}}" admin_pass="${BOOTSTRAP_ADMIN_PASSWORD:-${GROK2API_BOOTSTRAP_ADMIN_PASSWORD:-}}" if [ -z "$admin_pass" ]; then die "BOOTSTRAP_ADMIN_PASSWORD is required when generating config from environment" fi case "$admin_pass" in replace-with-a-strong-password) die "BOOTSTRAP_ADMIN_PASSWORD must not use the example placeholder" ;; esac db_driver="${DATABASE_DRIVER:-${GROK2API_DATABASE_DRIVER:-sqlite}}" runtime_driver="${RUNTIME_STORE_DRIVER:-${GROK2API_RUNTIME_STORE_DRIVER:-memory}}" sqlite_path="${SQLITE_PATH:-${GROK2API_SQLITE_PATH:-$data_root/backend.db}}" media_path="${MEDIA_PATH:-${GROK2API_MEDIA_PATH:-$data_root/media}}" static_path="${FRONTEND_STATIC_PATH:-/app/frontend/dist}" public_api_base_url="$(resolve_public_api_base_url)" secure_cookies="$(resolve_secure_cookies)" swagger_enabled="false" if is_truthy "${SWAGGER_ENABLED:-}"; then swagger_enabled="true" fi postgres_dsn="${POSTGRES_DSN:-${GROK2API_POSTGRES_DSN:-}}" redis_address="${REDIS_ADDRESS:-${GROK2API_REDIS_ADDRESS:-127.0.0.1:6379}}" redis_username="${REDIS_USERNAME:-${GROK2API_REDIS_USERNAME:-}}" redis_password="${REDIS_PASSWORD:-${GROK2API_REDIS_PASSWORD:-}}" redis_database="${REDIS_DATABASE:-${GROK2API_REDIS_DATABASE:-0}}" redis_prefix="${REDIS_KEY_PREFIX:-${GROK2API_REDIS_KEY_PREFIX:-grok2api:}}" redis_tls="false" if is_truthy "${REDIS_TLS:-${GROK2API_REDIS_TLS:-}}"; then redis_tls="true" fi if [ "$db_driver" = "postgres" ] && [ -z "$postgres_dsn" ]; then die "POSTGRES_DSN is required when DATABASE_DRIVER=postgres" fi jwt_q="$(yaml_quote "$jwt_secret")" key_q="$(yaml_quote "$credential_key")" admin_user_q="$(yaml_quote "$admin_user")" admin_pass_q="$(yaml_quote "$admin_pass")" sqlite_path_q="$(yaml_quote "$sqlite_path")" media_path_q="$(yaml_quote "$media_path")" static_path_q="$(yaml_quote "$static_path")" public_api_q="$(yaml_quote "$public_api_base_url")" postgres_dsn_q="$(yaml_quote "$postgres_dsn")" redis_address_q="$(yaml_quote "$redis_address")" redis_username_q="$(yaml_quote "$redis_username")" redis_password_q="$(yaml_quote "$redis_password")" redis_prefix_q="$(yaml_quote "$redis_prefix")" cat >"$APP_CONFIG" </dev/null || true fi if [ "$generated_secrets" -eq 1 ]; then log "generated JWT/credential secrets and saved them under ${secrets_file}" log "set JWT_SECRET and CREDENTIAL_ENCRYPTION_KEY as Space Secrets for stable personal use" fi if [ -z "$public_api_base_url" ]; then log "PUBLIC_API_BASE_URL is empty; docs may show http://127.0.0.1:8000" log "on Hugging Face, SPACE_HOST is usually injected; or set PUBLIC_API_BASE_URL" fi } install_config_from_source() { src="$1" [ -f "$src" ] || die "missing config: ${src}" cp "$src" "$APP_CONFIG" if [ "$(id -u)" -eq 0 ]; then chown grok2api:grok2api "$APP_CONFIG" fi chmod 0600 "$APP_CONFIG" } run_as_app() { if [ "$(id -u)" -eq 0 ]; then exec su-exec grok2api:grok2api "$@" fi exec "$@" } # ---- main ---- DATA_ROOT="$(resolve_data_root)" prepare_data_dirs "$DATA_ROOT" LISTEN="$(resolve_listen)" if [ -f "$CONFIG_SOURCE" ]; then log "using mounted config: $CONFIG_SOURCE" install_config_from_source "$CONFIG_SOURCE" elif [ -n "${BOOTSTRAP_ADMIN_PASSWORD:-}" ] || [ -n "${GROK2API_BOOTSTRAP_ADMIN_PASSWORD:-}" ] || [ -n "${JWT_SECRET:-}" ] || [ -n "${GROK2API_JWT_SECRET:-}" ] || [ -n "${CREDENTIAL_ENCRYPTION_KEY:-}" ] || [ -n "${GROK2API_CREDENTIAL_ENCRYPTION_KEY:-}" ]; then log "generating config from environment (data root: $DATA_ROOT, listen: $LISTEN)" generate_config_from_env "$DATA_ROOT" else die "missing config: mount ${CONFIG_SOURCE} or set BOOTSTRAP_ADMIN_PASSWORD (and optional JWT_SECRET / CREDENTIAL_ENCRYPTION_KEY) for Hugging Face / env deploy" fi # CLI --listen overrides YAML. Append resolved listen last so HF PORT/7860 wins over image CMD. if [ "$#" -eq 0 ]; then set -- /app/grok2api --config "$APP_CONFIG" --listen "$LISTEN" else set -- "$@" --listen "$LISTEN" fi log "starting grok2api on $LISTEN" run_as_app "$@"