| #!/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 |
| |
| . "$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" <<EOF |
| server: |
| listen: "0.0.0.0:8000" |
| maxBodyBytes: 33554432 |
| readTimeout: 15m |
| requestTimeout: 2h |
| swaggerEnabled: ${swagger_enabled} |
| |
| auth: |
| accessTokenTTL: 15m |
| refreshTokenTTL: 720h |
| secureCookies: ${secure_cookies} |
| |
| secrets: |
| jwtSecret: "${jwt_q}" |
| credentialEncryptionKey: "${key_q}" |
| |
| bootstrapAdmin: |
| username: "${admin_user_q}" |
| password: "${admin_pass_q}" |
| |
| frontend: |
| publicApiBaseURL: "${public_api_q}" |
| staticPath: "${static_path_q}" |
| |
| database: |
| driver: ${db_driver} |
| sqlite: |
| path: "${sqlite_path_q}" |
| postgres: |
| dsn: "${postgres_dsn_q}" |
| maxOpenConns: 50 |
| maxIdleConns: 10 |
| |
| runtimeStore: |
| driver: ${runtime_driver} |
| redis: |
| address: "${redis_address_q}" |
| username: "${redis_username_q}" |
| password: "${redis_password_q}" |
| database: ${redis_database} |
| keyPrefix: "${redis_prefix_q}" |
| tls: ${redis_tls} |
| |
| media: |
| driver: local |
| local: |
| path: "${media_path_q}" |
| |
| routing: |
| reasoningReplayEnabled: true |
| reasoningReplayTTL: 1h |
| reasoningReplayMaxEntries: 10240 |
| EOF |
|
|
| if [ "$(id -u)" -eq 0 ]; then |
| chown grok2api:grok2api "$APP_CONFIG" |
| fi |
| chmod 0600 "$APP_CONFIG" |
|
|
| persist_secrets "$secrets_file" "$jwt_secret" "$credential_key" |
| if [ "$(id -u)" -eq 0 ]; then |
| chown grok2api:grok2api "$secrets_file" 2>/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 "$@" |
| } |
|
|
| |
|
|
| 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 |
|
|
| |
| 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 "$@" |