Files
2026-07-13 12:08:54 +08:00

3358 lines
109 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
if [[ -z "${BASH_VERSINFO+x}" || "${BASH_VERSINFO[0]}" -lt 4 ]]; then
echo "Error: scripts/setup/setup.sh requires Bash 4 or newer." >&2
echo "Hint: install a newer bash and run it via 'bash scripts/setup/setup.sh ...'." >&2
exit 1
fi
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="$SCRIPT_DIR/lib"
# shellcheck disable=SC2034
TEMPLATES_DIR="$SCRIPT_DIR/templates"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
declare -A ENV_VALUES
declare -A ORIGINAL_ENV_VALUES
declare -A COMPOSE_ENV_OVERRIDES
declare -A COMPOSE_REWRITE_SERVICE_SET
declare -A COMPOSE_SERVICE_IMAGE_OVERRIDES
declare -A REQUIRED_DB_TYPES
declare -A DOCKER_SERVICE_SET
declare -A EXISTING_MANAGED_ROOT_SERVICE_SET
declare -a DOCKER_SERVICES
SSL_CERT_SOURCE_PATH=""
SSL_KEY_SOURCE_PATH=""
LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING=""
NORMALIZED_SERVER_HOST_FOR_COMPOSE=""
FORCE_REWRITE_COMPOSE="no"
DEBUG="${DEBUG:-false}"
PRESET_VLLM_EMBEDDING=(
"EMBEDDING_BINDING=openai"
"EMBEDDING_BINDING_HOST=http://localhost:8001/v1"
"EMBEDDING_MODEL=BAAI/bge-m3"
"EMBEDDING_DIM=1024"
"VLLM_EMBED_MODEL=BAAI/bge-m3"
"VLLM_EMBED_PORT=8001"
"VLLM_EMBED_DEVICE=cpu"
)
PRESET_VLLM_RERANKER=(
"RERANK_BINDING=cohere"
"LIGHTRAG_SETUP_RERANK_PROVIDER=vllm"
"RERANK_MODEL=BAAI/bge-reranker-v2-m3"
"RERANK_BINDING_HOST=http://localhost:8000/rerank"
"VLLM_RERANK_MODEL=BAAI/bge-reranker-v2-m3"
"VLLM_RERANK_PORT=8000"
"VLLM_RERANK_DEVICE=cpu"
)
VLLM_SERVICES=(
"vllm-embed"
"vllm-rerank"
)
STORAGE_SERVICES=(
"postgres"
"neo4j"
"mongodb"
"redis"
"milvus"
"qdrant"
"memgraph"
"opensearch"
)
DEFAULT_RUNTIME_TARGET="host"
COMPOSE_LIGHTRAG_WORKING_DIR="/app/data/rag_storage"
COMPOSE_LIGHTRAG_INPUT_DIR="/app/data/inputs"
COMPOSE_LIGHTRAG_PROMPT_DIR="/app/data/prompts"
# shellcheck disable=SC2034
COLOR_RESET=""
COLOR_BOLD=""
COLOR_BLUE=""
COLOR_GREEN=""
COLOR_YELLOW=""
# shellcheck disable=SC2034
COLOR_RED=""
# shellcheck disable=SC1091
source "$LIB_DIR/storage_requirements.sh"
# shellcheck disable=SC1091
source "$LIB_DIR/validation.sh"
# shellcheck disable=SC1091
source "$LIB_DIR/prompts.sh"
# shellcheck disable=SC1091
source "$LIB_DIR/file_ops.sh"
# shellcheck disable=SC1091
source "$LIB_DIR/presets.sh"
init_colors() {
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
COLOR_RESET=$'\033[0m'
COLOR_BOLD=$'\033[1m'
COLOR_BLUE=$'\033[34m'
COLOR_GREEN=$'\033[32m'
COLOR_YELLOW=$'\033[33m'
# shellcheck disable=SC2034
COLOR_RED=$'\033[31m'
fi
}
reset_state() {
ENV_VALUES=()
ORIGINAL_ENV_VALUES=()
COMPOSE_ENV_OVERRIDES=()
COMPOSE_REWRITE_SERVICE_SET=()
COMPOSE_SERVICE_IMAGE_OVERRIDES=()
REQUIRED_DB_TYPES=()
DOCKER_SERVICE_SET=()
EXISTING_MANAGED_ROOT_SERVICE_SET=()
DOCKER_SERVICES=()
SSL_CERT_SOURCE_PATH=""
SSL_KEY_SOURCE_PATH=""
LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING=""
NORMALIZED_SERVER_HOST_FOR_COMPOSE=""
}
validate_runtime_target() {
local runtime_target="${1:-$DEFAULT_RUNTIME_TARGET}"
case "$runtime_target" in
host|compose)
return 0
;;
*)
format_error \
"Invalid LIGHTRAG_RUNTIME_TARGET: ${runtime_target}" \
"Use 'host' or 'compose', or rerun the setup wizard to regenerate .env."
return 1
;;
esac
}
set_runtime_target() {
local runtime_target="${1:-$DEFAULT_RUNTIME_TARGET}"
if ! validate_runtime_target "$runtime_target"; then
return 1
fi
ENV_VALUES["LIGHTRAG_RUNTIME_TARGET"]="$runtime_target"
}
clear_deprecated_vllm_dtype_state() {
unset 'ENV_VALUES[VLLM_EMBED_DTYPE]'
unset 'ENV_VALUES[VLLM_RERANK_DTYPE]'
}
normalize_vllm_rerank_binding_state() {
if [[ "${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}" == "vllm" ]]; then
ENV_VALUES["RERANK_BINDING"]="cohere"
fi
}
# Backfill sentinel hosts for bedrock/gemini bindings when LLM_BINDING_HOST or
# EMBEDDING_BINDING_HOST is missing or empty. Flows that skip collect_llm_config /
# collect_embedding_config (--server, --storage) would otherwise let the openai URL
# hardcoded in env.example leak into the regenerated .env.
normalize_provider_binding_hosts() {
local llm_sentinel="" embedding_sentinel=""
case "${ENV_VALUES[LLM_BINDING]:-}" in
bedrock) llm_sentinel="DEFAULT_BEDROCK_ENDPOINT" ;;
gemini) llm_sentinel="DEFAULT_GEMINI_ENDPOINT" ;;
esac
case "${ENV_VALUES[EMBEDDING_BINDING]:-}" in
bedrock) embedding_sentinel="DEFAULT_BEDROCK_ENDPOINT" ;;
gemini) embedding_sentinel="DEFAULT_GEMINI_ENDPOINT" ;;
esac
if [[ -n "$llm_sentinel" && -z "${ENV_VALUES[LLM_BINDING_HOST]:-}" ]]; then
ENV_VALUES["LLM_BINDING_HOST"]="$llm_sentinel"
fi
if [[ -n "$embedding_sentinel" && -z "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" ]]; then
ENV_VALUES["EMBEDDING_BINDING_HOST"]="$embedding_sentinel"
fi
}
load_existing_env_if_present() {
local env_file="${REPO_ROOT}/.env"
if [[ -f "$env_file" ]]; then
log_debug "Loading existing .env defaults from $env_file"
load_env_file "$env_file"
clear_deprecated_vllm_dtype_state
normalize_vllm_rerank_binding_state
normalize_provider_binding_hosts
if [[ "${ENV_VALUES[SSL]:-false}" == "true" ]]; then
SSL_CERT_SOURCE_PATH="${ENV_VALUES[SSL_CERTFILE]:-}"
SSL_KEY_SOURCE_PATH="${ENV_VALUES[SSL_KEYFILE]:-}"
fi
snapshot_original_env_values
fi
}
snapshot_original_env_values() {
local key
ORIGINAL_ENV_VALUES=()
for key in "${!ENV_VALUES[@]}"; do
ORIGINAL_ENV_VALUES["$key"]="${ENV_VALUES[$key]}"
done
}
prepare_compose_output_from_existing() {
local output_file="$1"
local existing_file="$2"
if [[ -z "$existing_file" || "$existing_file" == "$output_file" || -f "$output_file" ]]; then
return 0
fi
if ! cp "$existing_file" "$output_file"; then
format_error "Failed to prepare compose output at ${output_file}" \
"Check file permissions and available disk space, then rerun setup."
return 1
fi
log_success "Using ${existing_file} as merge input for ${output_file}"
}
log_debug() {
if [[ "$DEBUG" == "true" ]]; then
echo "${COLOR_YELLOW}[debug]${COLOR_RESET} $*"
fi
}
log_info() {
echo "${COLOR_BLUE}${COLOR_BOLD}$*${COLOR_RESET}"
}
log_warn() {
echo "${COLOR_YELLOW}$*${COLOR_RESET}"
}
log_success() {
echo "${COLOR_GREEN}$*${COLOR_RESET}"
}
log_step() {
echo "${COLOR_BLUE}${COLOR_BOLD}$*${COLOR_RESET}"
}
# Return success when the given host binds to a loopback interface only.
# An empty host means "use the server default" (0.0.0.0), i.e. not loopback.
host_is_loopback() {
local host="${1:-}"
case "$host" in
localhost|127.0.0.1|::1|127.*) return 0 ;;
esac
return 1
}
# Print a suggestion (informational only — no logic change) when the server is
# configured to bind to a non-loopback address without any authentication.
warn_if_network_exposed_without_auth() {
local host="${ENV_VALUES[HOST]:-0.0.0.0}"
if host_is_loopback "$host"; then
return 0
fi
if [[ -n "${ENV_VALUES[AUTH_ACCOUNTS]:-}" || -n "${ENV_VALUES[LIGHTRAG_API_KEY]:-}" ]]; then
return 0
fi
log_warn "HOST=$host is reachable from the network but no authentication is configured."
echo " Suggestion: set AUTH_ACCOUNTS (with TOKEN_SECRET) or LIGHTRAG_API_KEY before"
echo " exposing the server, or bind to a loopback address (HOST=127.0.0.1)."
}
normalize_loopback_uri_for_compose() {
local uri="$1"
if [[ "$uri" =~ ^([a-zA-Z][a-zA-Z0-9+.-]*://)([^/?#]+@)?(localhost|127\.0\.0\.1|0\.0\.0\.0)([/:?].*)?$ ]]; then
printf '%s%shost.docker.internal%s' \
"${BASH_REMATCH[1]}" \
"${BASH_REMATCH[2]}" \
"${BASH_REMATCH[4]}"
return 0
fi
printf '%s' "$uri"
}
ensure_mongodb_direct_connection_suffix() {
local suffix="${1:-}"
local path query fragment filtered_query=""
local part
local -a query_parts=()
if [[ "$suffix" == *"#"* ]]; then
fragment="#${suffix#*#}"
suffix="${suffix%%#*}"
else
fragment=""
fi
if [[ "$suffix" == *"?"* ]]; then
path="${suffix%%\?*}"
query="${suffix#*\?}"
else
path="$suffix"
query=""
fi
if [[ -z "$path" ]]; then
path="/"
fi
if [[ -n "$query" ]]; then
IFS='&' read -r -a query_parts <<< "$query"
for part in "${query_parts[@]}"; do
if [[ -z "$part" || "$part" == directConnection=* ]]; then
continue
fi
if [[ -n "$filtered_query" ]]; then
filtered_query="${filtered_query}&${part}"
else
filtered_query="$part"
fi
done
fi
if [[ -n "$filtered_query" ]]; then
printf '%s?%s&directConnection=true%s' "$path" "$filtered_query" "$fragment"
else
printf '%s?directConnection=true%s' "$path" "$fragment"
fi
}
mongodb_uri_has_direct_connection_true() {
local uri="$1"
local direct_connection_pattern='[?&]directConnection=true([&#]|$)'
[[ "$uri" =~ ^mongodb:// ]] && [[ "$uri" =~ $direct_connection_pattern ]]
}
is_wizard_managed_local_mongodb_uri() {
local uri="$1"
[[ "$uri" =~ ^mongodb://([^/?#]+@)?(mongodb|localhost|127\.0\.0\.1|0\.0\.0\.0):27017([/?#].*)?$ ]] && \
mongodb_uri_has_direct_connection_true "$uri"
}
normalize_mongodb_uri_for_local_service() {
local uri="$1"
local suffix
if [[ "$uri" =~ ^mongodb://([^/?#]+@)?(mongodb|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then
suffix="$(ensure_mongodb_direct_connection_suffix "${BASH_REMATCH[4]:-/}")"
printf 'mongodb://localhost:27017%s' "$suffix"
return 0
fi
printf '%s' "$uri"
}
normalize_neo4j_uri_for_local_service() {
local uri="$1"
if [[ "$uri" =~ ^([a-zA-Z][a-zA-Z0-9+.-]*://)([^/?#]+@)?(neo4j|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then
printf '%s%slocalhost:7687%s' \
"${BASH_REMATCH[1]}" \
"${BASH_REMATCH[2]}" \
"${BASH_REMATCH[5]}"
return 0
fi
printf '%s' "$uri"
}
normalize_redis_uri_for_local_service() {
local uri="$1"
if [[ "$uri" =~ ^rediss?://([^/?#]+@)?(redis|localhost|127\.0\.0\.1|0\.0\.0\.0)(:([0-9]+))?(/.*)?$ ]]; then
printf 'redis://localhost:6379%s' "${BASH_REMATCH[5]}"
return 0
fi
printf '%s' "$uri"
}
normalize_milvus_uri_for_local_service() {
local uri="$1"
if [[ "$uri" =~ ^(https?://)([^/?#]+@)?(milvus|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then
printf '%slocalhost:19530%s' \
"${BASH_REMATCH[1]}" \
"${BASH_REMATCH[5]}"
return 0
fi
printf '%s' "$uri"
}
normalize_qdrant_uri_for_local_service() {
local uri="$1"
if [[ "$uri" =~ ^(https?://)([^/?#]+@)?(qdrant|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then
printf '%slocalhost:6333%s' \
"${BASH_REMATCH[1]}" \
"${BASH_REMATCH[5]}"
return 0
fi
printf '%s' "$uri"
}
normalize_memgraph_uri_for_local_service() {
local uri="$1"
if [[ "$uri" =~ ^(bolt://)([^/?#]+@)?(memgraph|localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?([/?#].*)?$ ]]; then
printf 'bolt://localhost:7687%s' "${BASH_REMATCH[5]}"
return 0
fi
printf '%s' "$uri"
}
normalize_loopback_host_for_compose() {
local host="$1"
if [[ "$host" == "localhost" || "$host" == "127.0.0.1" || "$host" == "0.0.0.0" ]]; then
printf 'host.docker.internal'
return 0
fi
printf '%s' "$host"
}
normalize_opensearch_hosts_for_compose() {
local hosts="$1"
local entry=""
local trimmed=""
local normalized_entry=""
local -a raw_entries=()
local -a normalized_entries=()
IFS=',' read -r -a raw_entries <<< "$hosts"
for entry in "${raw_entries[@]}"; do
trimmed="${entry#"${entry%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
# OPENSEARCH_HOSTS is intentionally limited to bare host[:port] entries.
# TLS is configured separately via OPENSEARCH_USE_SSL, so scheme-bearing
# URLs are rejected during validation rather than normalized here.
normalized_entry="$trimmed"
if [[ "$trimmed" =~ ^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:[0-9]+)?$ ]]; then
normalized_entry="host.docker.internal${BASH_REMATCH[2]}"
fi
normalized_entries+=("$normalized_entry")
done
(
IFS=','
printf '%s' "${normalized_entries[*]}"
)
}
env_value_is_true() {
local value="${1:-}"
case "${value,,}" in
true|1|yes)
return 0
;;
*)
return 1
;;
esac
}
normalize_server_host_for_compose() {
# Keep the published bind address/port configurable through compose-time
# variable expansion, while forcing the container itself to listen on the
# internal service defaults.
LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING='${HOST:-0.0.0.0}:${PORT:-9621}:9621'
NORMALIZED_SERVER_HOST_FOR_COMPOSE="0.0.0.0"
}
host_cuda_available() {
command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1
}
resolve_local_device_default() {
local configured_device="${1:-}"
if [[ "$configured_device" == "cpu" || "$configured_device" == "cuda" ]]; then
printf '%s' "$configured_device"
return 0
fi
if host_cuda_available; then
printf 'cuda'
else
printf 'cpu'
fi
}
default_loopback_url() {
local port="$1"
local path="${2:-}"
printf 'http://localhost:%s%s' "$port" "$path"
}
uri_points_to_host() {
local uri="$1"
shift
local host=""
local allowed_host
if [[ "$uri" =~ ^[a-zA-Z][a-zA-Z0-9+.-]*://([^/?#]+@)?(\[[^]]+\]|[^/:?#]+) ]]; then
host="${BASH_REMATCH[2]}"
for allowed_host in "$@"; do
if [[ "$host" == "$allowed_host" ]]; then
return 0
fi
done
fi
return 1
}
prefer_local_service_uri() {
local current_uri="$1"
local default_uri="$2"
shift 2
if [[ -z "$current_uri" ]]; then
printf '%s' "$default_uri"
return 0
fi
if uri_points_to_host "$current_uri" "$@"; then
printf '%s' "$current_uri"
return 0
fi
printf '%s' "$default_uri"
}
set_compose_override() {
local key="$1"
local value="${2:-}"
if [[ -n "$value" ]]; then
COMPOSE_ENV_OVERRIDES["$key"]="$value"
else
unset "COMPOSE_ENV_OVERRIDES[$key]"
fi
}
set_managed_service_compose_overrides() {
local root_service="$1"
case "$root_service" in
postgres)
if [[ -z "${COMPOSE_ENV_OVERRIDES[POSTGRES_HOST]+set}" ]]; then
set_compose_override "POSTGRES_HOST" "postgres"
fi
# The bundled postgres compose service always listens on 5432 internally.
if [[ -z "${COMPOSE_ENV_OVERRIDES[POSTGRES_PORT]+set}" ]]; then
set_compose_override "POSTGRES_PORT" "5432"
fi
;;
neo4j)
if [[ -z "${COMPOSE_ENV_OVERRIDES[NEO4J_URI]+set}" ]]; then
set_compose_override "NEO4J_URI" "neo4j://neo4j:7687"
fi
;;
mongodb)
if [[ -z "${COMPOSE_ENV_OVERRIDES[MONGO_URI]+set}" ]]; then
set_compose_override "MONGO_URI" "mongodb://mongodb:27017/?directConnection=true"
fi
;;
redis)
if [[ -z "${COMPOSE_ENV_OVERRIDES[REDIS_URI]+set}" ]]; then
set_compose_override "REDIS_URI" "redis://redis:6379"
fi
;;
milvus)
if [[ -z "${COMPOSE_ENV_OVERRIDES[MILVUS_URI]+set}" ]]; then
set_compose_override "MILVUS_URI" "http://milvus:19530"
fi
;;
qdrant)
if [[ -z "${COMPOSE_ENV_OVERRIDES[QDRANT_URL]+set}" ]]; then
set_compose_override "QDRANT_URL" "http://qdrant:6333"
fi
;;
memgraph)
if [[ -z "${COMPOSE_ENV_OVERRIDES[MEMGRAPH_URI]+set}" ]]; then
set_compose_override "MEMGRAPH_URI" "bolt://memgraph:7687"
fi
;;
opensearch)
if [[ -z "${COMPOSE_ENV_OVERRIDES[OPENSEARCH_HOSTS]+set}" ]]; then
set_compose_override "OPENSEARCH_HOSTS" "opensearch:9200"
fi
;;
esac
}
prepare_compose_runtime_overrides() {
local normalized_value
local key
local root_service
# EMBEDDING_BINDING_HOST: when vllm-embed is part of this compose, the LightRAG
# container must reach it by Docker service name, not by a loopback address.
# This applies even when the wizard did not visit the embedding step (e.g.
# env_server_flow), because vllm-embed is detected and added to DOCKER_SERVICE_SET
# before prepare_compose_env_overrides is called.
if [[ -z "${COMPOSE_ENV_OVERRIDES[EMBEDDING_BINDING_HOST]+set}" ]]; then
if [[ -n "${DOCKER_SERVICE_SET[vllm-embed]+set}" ]]; then
set_compose_override "EMBEDDING_BINDING_HOST" \
"http://vllm-embed:${ENV_VALUES[VLLM_EMBED_PORT]:-8001}/v1"
elif [[ -n "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" ]]; then
normalized_value="$(normalize_loopback_uri_for_compose "${ENV_VALUES[EMBEDDING_BINDING_HOST]}")"
if [[ "$normalized_value" != "${ENV_VALUES[EMBEDDING_BINDING_HOST]}" ]]; then
set_compose_override "EMBEDDING_BINDING_HOST" "$normalized_value"
fi
fi
fi
# RERANK_BINDING_HOST: same pattern for vllm-rerank.
if [[ -z "${COMPOSE_ENV_OVERRIDES[RERANK_BINDING_HOST]+set}" ]]; then
if [[ -n "${DOCKER_SERVICE_SET[vllm-rerank]+set}" ]]; then
set_compose_override "RERANK_BINDING_HOST" \
"http://vllm-rerank:${ENV_VALUES[VLLM_RERANK_PORT]:-8000}/rerank"
elif [[ -n "${ENV_VALUES[RERANK_BINDING_HOST]:-}" ]]; then
normalized_value="$(normalize_loopback_uri_for_compose "${ENV_VALUES[RERANK_BINDING_HOST]}")"
if [[ "$normalized_value" != "${ENV_VALUES[RERANK_BINDING_HOST]}" ]]; then
set_compose_override "RERANK_BINDING_HOST" "$normalized_value"
fi
fi
fi
for root_service in postgres neo4j mongodb redis milvus qdrant memgraph opensearch; do
if [[ -n "${DOCKER_SERVICE_SET[$root_service]+set}" ]]; then
set_managed_service_compose_overrides "$root_service"
fi
done
for key in \
"LLM_BINDING_HOST" \
"REDIS_URI" \
"MONGO_URI" \
"NEO4J_URI" \
"MILVUS_URI" \
"QDRANT_URL" \
"MEMGRAPH_URI"; do
if [[ -n "${COMPOSE_ENV_OVERRIDES[$key]+set}" ]]; then
continue
fi
if [[ -n "${ENV_VALUES[$key]:-}" ]]; then
normalized_value="$(normalize_loopback_uri_for_compose "${ENV_VALUES[$key]}")"
if [[ "$normalized_value" != "${ENV_VALUES[$key]}" ]]; then
set_compose_override "$key" "$normalized_value"
fi
fi
done
for key in "OPENSEARCH_HOSTS"; do
if [[ -n "${COMPOSE_ENV_OVERRIDES[$key]+set}" ]]; then
continue
fi
if [[ -n "${ENV_VALUES[$key]:-}" ]]; then
normalized_value="$(normalize_opensearch_hosts_for_compose "${ENV_VALUES[$key]}")"
if [[ "$normalized_value" != "${ENV_VALUES[$key]}" ]]; then
set_compose_override "$key" "$normalized_value"
fi
fi
done
for key in "POSTGRES_HOST"; do
if [[ -n "${COMPOSE_ENV_OVERRIDES[$key]+set}" ]]; then
continue
fi
if [[ -n "${ENV_VALUES[$key]:-}" ]]; then
normalized_value="$(normalize_loopback_host_for_compose "${ENV_VALUES[$key]}")"
if [[ "$normalized_value" != "${ENV_VALUES[$key]}" ]]; then
set_compose_override "$key" "$normalized_value"
fi
fi
done
normalize_server_host_for_compose "${ENV_VALUES[HOST]:-0.0.0.0}"
normalized_value="$NORMALIZED_SERVER_HOST_FOR_COMPOSE"
set_compose_override "HOST" "$normalized_value"
set_compose_override "PORT" "9621"
}
prepare_compose_ssl_overrides() {
local cert_name=""
local key_name=""
if [[ -n "$SSL_CERT_SOURCE_PATH" ]]; then
cert_name="$(resolve_staged_ssl_basename "cert" "$SSL_CERT_SOURCE_PATH" "$SSL_KEY_SOURCE_PATH")"
set_compose_override "SSL_CERTFILE" "/app/data/certs/${cert_name}"
fi
if [[ -n "$SSL_KEY_SOURCE_PATH" ]]; then
key_name="$(resolve_staged_ssl_basename "key" "$SSL_KEY_SOURCE_PATH" "$SSL_CERT_SOURCE_PATH")"
set_compose_override "SSL_KEYFILE" "/app/data/certs/${key_name}"
fi
}
prepare_compose_data_path_overrides() {
# Compose mounts always bind the data directories into these container paths.
# Force lightrag to use them so values from the mounted .env cannot redirect
# storage into a different location.
set_compose_override "WORKING_DIR" "$COMPOSE_LIGHTRAG_WORKING_DIR"
set_compose_override "INPUT_DIR" "$COMPOSE_LIGHTRAG_INPUT_DIR"
set_compose_override "PROMPT_DIR" "$COMPOSE_LIGHTRAG_PROMPT_DIR"
}
prepare_compose_env_overrides() {
prepare_compose_data_path_overrides
prepare_compose_runtime_overrides
prepare_compose_ssl_overrides
}
add_docker_service() {
local service="$1"
if [[ -z "${DOCKER_SERVICE_SET[$service]+set}" ]]; then
DOCKER_SERVICE_SET["$service"]=1
DOCKER_SERVICES+=("$service")
fi
}
restore_storage_docker_services_from_env() {
local db_type
local marker_key=""
local service_name=""
local db_types=("postgresql" "neo4j" "mongodb" "redis" "milvus" "qdrant" "memgraph" "opensearch")
for db_type in "${db_types[@]}"; do
marker_key="$(storage_deployment_marker_key "$db_type")"
if [[ -n "$marker_key" && "${ENV_VALUES[$marker_key]:-}" == "docker" ]]; then
service_name="$(storage_service_name_for_db_type "$db_type")"
if [[ -n "$service_name" ]]; then
add_docker_service "$service_name"
fi
fi
done
}
restore_vllm_docker_services_from_env() {
if [[ "${ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]:-}" == "vllm" ]]; then
add_docker_service "vllm-embed"
fi
if [[ "${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}" == "vllm" ]]; then
add_docker_service "vllm-rerank"
fi
}
compose_has_non_wizard_services() {
local compose_file="$1"
local service_name=""
if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then
return 1
fi
while IFS= read -r service_name; do
if [[ -z "$(_managed_service_root_name "$service_name")" ]]; then
return 0
fi
done < <(detect_compose_services "$compose_file")
return 1
}
resolve_compose_output_action() {
local existing_compose="$1"
local -n action_ref="$2"
local -n runtime_target_ref="$3"
local -n host_hint_ref="$4"
local current_target="${ENV_VALUES[LIGHTRAG_RUNTIME_TARGET]:-$DEFAULT_RUNTIME_TARGET}"
action_ref="write_env_only"
runtime_target_ref="$DEFAULT_RUNTIME_TARGET"
host_hint_ref="no"
if ((${#DOCKER_SERVICES[@]} > 0)); then
action_ref="rewrite_compose"
runtime_target_ref="compose"
return 0
fi
if compose_has_non_wizard_services "$existing_compose"; then
action_ref="rewrite_compose"
runtime_target_ref="compose"
return 0
fi
if [[ -n "$existing_compose" ]]; then
if confirm_default_no "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?"; then
action_ref="delete_compose_and_switch_host"
runtime_target_ref="host"
host_hint_ref="yes"
else
action_ref="rewrite_compose"
runtime_target_ref="compose"
fi
return 0
fi
if [[ "$current_target" == "compose" ]]; then
if confirm_default_yes "Run LightRAG Server via Docker?"; then
action_ref="rewrite_compose"
runtime_target_ref="compose"
else
host_hint_ref="yes"
fi
else
if confirm_default_no "Run LightRAG Server via Docker?"; then
action_ref="rewrite_compose"
runtime_target_ref="compose"
else
host_hint_ref="yes"
fi
fi
}
mark_compose_service_for_rewrite() {
local service="$1"
local root_service=""
root_service="$(_managed_service_root_name "$service")"
if [[ -n "$root_service" ]]; then
COMPOSE_REWRITE_SERVICE_SET["$root_service"]=1
fi
}
record_existing_managed_root_services() {
local compose_file="$1"
local service_name
local root_service
EXISTING_MANAGED_ROOT_SERVICE_SET=()
if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then
return 0
fi
while IFS= read -r service_name; do
root_service="$(_managed_service_root_name "$service_name")"
if [[ -n "$root_service" ]]; then
EXISTING_MANAGED_ROOT_SERVICE_SET["$root_service"]=1
fi
done < <(detect_managed_root_services "$compose_file")
}
collect_preserved_storage_service_images() {
local compose_file="${1:-}"
local service_name=""
local image_value=""
COMPOSE_SERVICE_IMAGE_OVERRIDES=()
if [[ "$FORCE_REWRITE_COMPOSE" == "yes" || -z "$compose_file" || ! -f "$compose_file" ]]; then
return 0
fi
# Only postgres and neo4j are wizard-managed Docker storage services that users
# commonly pin to custom registry images. If new storage backends are added as
# wizard-managed Docker services, extend this list accordingly.
for service_name in postgres neo4j; do
if [[ -z "${COMPOSE_REWRITE_SERVICE_SET[$service_name]+set}" ]] || \
[[ -z "${DOCKER_SERVICE_SET[$service_name]+set}" ]] || \
! existing_managed_root_service_present "$service_name"; then
continue
fi
image_value="$(read_service_image_value "$compose_file" "$service_name" || true)"
if [[ -n "$image_value" ]]; then
COMPOSE_SERVICE_IMAGE_OVERRIDES["$service_name"]="$image_value"
fi
done
}
backup_existing_compose_if_generating() {
local generate_compose="${1:-no}"
local existing_compose="${2:-}"
local compose_backup_path=""
if [[ "$generate_compose" != "yes" ]]; then
return 0
fi
compose_backup_path="$(backup_compose_file "$existing_compose")" || return 1
if [[ -n "$compose_backup_path" ]]; then
log_success "Backed up existing compose file to $compose_backup_path"
fi
}
backup_existing_compose_for_action() {
local compose_action="${1:-write_env_only}"
local existing_compose="${2:-}"
local compose_backup_path=""
case "$compose_action" in
rewrite_compose|delete_compose_and_switch_host)
;;
*)
return 0
;;
esac
compose_backup_path="$(backup_compose_file "$existing_compose")" || return 1
if [[ -n "$compose_backup_path" ]]; then
log_success "Backed up existing compose file to $compose_backup_path"
fi
}
remove_existing_compose_file() {
local compose_file="${1:-}"
if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then
return 0
fi
if ! rm "$compose_file"; then
format_error "Failed to remove ${compose_file}" \
"Check file permissions, then remove the compose file manually or rerun setup."
return 1
fi
log_success "Removed ${compose_file}"
}
existing_managed_root_service_present() {
local root_service="$1"
[[ -n "${EXISTING_MANAGED_ROOT_SERVICE_SET[$root_service]+set}" ]]
}
compose_service_block_contains_literal() {
local compose_file="$1"
local service_name="$2"
local literal="$3"
if [[ -z "$compose_file" || ! -f "$compose_file" ]]; then
return 1
fi
awk -v header=" ${service_name}:" -v literal="$literal" '
$0 == header { in_service = 1; next }
in_service && $0 ~ /^ [^[:space:]]/ { exit found ? 0 : 1 }
in_service && index($0, literal) { found = 1; exit 0 }
END { exit found ? 0 : 1 }
' "$compose_file"
}
mongodb_service_requires_atlas_local_rewrite() {
local compose_file="${1:-}"
if [[ -z "$compose_file" ]]; then
return 1
fi
if [[ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" != "MongoVectorDBStorage" ]]; then
return 1
fi
if [[ "${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}" != "docker" ]]; then
return 1
fi
if [[ -z "${DOCKER_SERVICE_SET[mongodb]+set}" ]] || \
! existing_managed_root_service_present "mongodb"; then
return 1
fi
if ! compose_service_block_contains_literal "$compose_file" "mongodb" "image: mongodb/mongodb-atlas-local:"; then
return 0
fi
if ! compose_service_block_contains_literal "$compose_file" "mongodb" "mongo_config_data:/data/configdb"; then
return 0
fi
if ! compose_service_block_contains_literal "$compose_file" "mongodb" "mongo_mongot_data:/data/mongot"; then
return 0
fi
return 1
}
configure_mongodb_compose_migration_rewrite() {
local existing_compose="${1:-}"
if [[ "$FORCE_REWRITE_COMPOSE" == "yes" ]]; then
return 0
fi
if mongodb_service_requires_atlas_local_rewrite "$existing_compose"; then
mark_compose_service_for_rewrite "mongodb"
fi
}
env_value_changed_from_original() {
local key="$1"
local missing_marker="__LIGHTRAG_MISSING__"
local current_value="${ENV_VALUES[$key]-$missing_marker}"
local original_value="${ORIGINAL_ENV_VALUES[$key]-$missing_marker}"
[[ "$current_value" != "$original_value" ]]
}
any_env_value_changed_from_original() {
local key
for key in "$@"; do
if env_value_changed_from_original "$key"; then
return 0
fi
done
return 1
}
compose_template_variant_for_service() {
local service="$1"
local snapshot="${2:-current}"
local device=""
case "$service" in
milvus)
if [[ "$snapshot" == "original" ]]; then
device="${ORIGINAL_ENV_VALUES[MILVUS_DEVICE]:-cpu}"
else
device="${ENV_VALUES[MILVUS_DEVICE]:-cpu}"
fi
;;
qdrant)
if [[ "$snapshot" == "original" ]]; then
device="${ORIGINAL_ENV_VALUES[QDRANT_DEVICE]:-cpu}"
else
device="${ENV_VALUES[QDRANT_DEVICE]:-cpu}"
fi
;;
vllm-embed)
if [[ "$snapshot" == "original" ]]; then
device="${ORIGINAL_ENV_VALUES[VLLM_EMBED_DEVICE]:-cpu}"
else
device="${ENV_VALUES[VLLM_EMBED_DEVICE]:-cpu}"
fi
;;
vllm-rerank)
if [[ "$snapshot" == "original" ]]; then
device="${ORIGINAL_ENV_VALUES[VLLM_RERANK_DEVICE]:-cpu}"
else
device="${ENV_VALUES[VLLM_RERANK_DEVICE]:-cpu}"
fi
;;
*)
printf 'default'
return 0
;;
esac
if [[ "$device" == "cuda" ]]; then
printf 'gpu'
else
printf 'cpu'
fi
}
configure_base_compose_rewrites() {
if [[ "$FORCE_REWRITE_COMPOSE" == "yes" ]]; then
return 0
fi
if existing_managed_root_service_present "vllm-embed" && \
[[ -n "${DOCKER_SERVICE_SET[vllm-embed]+set}" ]] && \
[[ "$(compose_template_variant_for_service "vllm-embed" "current")" != \
"$(compose_template_variant_for_service "vllm-embed" "original")" ]]; then
mark_compose_service_for_rewrite "vllm-embed"
fi
if existing_managed_root_service_present "vllm-rerank" && \
[[ -n "${DOCKER_SERVICE_SET[vllm-rerank]+set}" ]] && \
[[ "$(compose_template_variant_for_service "vllm-rerank" "current")" != \
"$(compose_template_variant_for_service "vllm-rerank" "original")" ]]; then
mark_compose_service_for_rewrite "vllm-rerank"
fi
}
configure_storage_compose_rewrites() {
if [[ "$FORCE_REWRITE_COMPOSE" == "yes" ]]; then
return 0
fi
if existing_managed_root_service_present "postgres" && \
[[ -n "${DOCKER_SERVICE_SET[postgres]+set}" ]] && \
any_env_value_changed_from_original "POSTGRES_USER" "POSTGRES_PASSWORD" "POSTGRES_DATABASE"; then
mark_compose_service_for_rewrite "postgres"
fi
if existing_managed_root_service_present "neo4j" && \
[[ -n "${DOCKER_SERVICE_SET[neo4j]+set}" ]] && \
any_env_value_changed_from_original "NEO4J_DATABASE"; then
mark_compose_service_for_rewrite "neo4j"
fi
if existing_managed_root_service_present "milvus" && \
[[ -n "${DOCKER_SERVICE_SET[milvus]+set}" ]] && \
[[ "$(compose_template_variant_for_service "milvus" "current")" != \
"$(compose_template_variant_for_service "milvus" "original")" ]]; then
mark_compose_service_for_rewrite "milvus"
fi
if existing_managed_root_service_present "qdrant" && \
[[ -n "${DOCKER_SERVICE_SET[qdrant]+set}" ]] && \
[[ "$(compose_template_variant_for_service "qdrant" "current")" != \
"$(compose_template_variant_for_service "qdrant" "original")" ]]; then
mark_compose_service_for_rewrite "qdrant"
fi
}
select_storage_backends() {
local deployment_type="$1"
local kv_default="JsonKVStorage"
local vector_default="NanoVectorDBStorage"
local graph_default="NetworkXStorage"
local doc_default="JsonDocStatusStorage"
local kv_storage vector_storage graph_storage doc_storage
if [[ "$deployment_type" == "production" ]]; then
kv_default="PGKVStorage"
vector_default="MilvusVectorDBStorage"
graph_default="Neo4JStorage"
doc_default="PGDocStatusStorage"
fi
kv_default="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-$kv_default}"
vector_default="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-$vector_default}"
graph_default="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-$graph_default}"
doc_default="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-$doc_default}"
while true; do
kv_storage="$(prompt_choice "KV storage" "$kv_default" "${KV_STORAGE_OPTIONS[@]}")"
doc_storage="$(prompt_choice "Doc status storage" "$doc_default" "${DOC_STATUS_STORAGE_OPTIONS[@]}")"
vector_storage="$(prompt_choice "Vector storage" "$vector_default" "${VECTOR_STORAGE_OPTIONS[@]}")"
graph_storage="$(prompt_choice "Graph storage" "$graph_default" "${GRAPH_STORAGE_OPTIONS[@]}")"
if check_storage_compatibility "$kv_storage" "$vector_storage" "$graph_storage" "$doc_storage"; then
break
fi
if confirm_default_no "Proceed with these storage selections anyway?"; then
break
fi
done
ENV_VALUES["LIGHTRAG_KV_STORAGE"]="$kv_storage"
ENV_VALUES["LIGHTRAG_VECTOR_STORAGE"]="$vector_storage"
ENV_VALUES["LIGHTRAG_GRAPH_STORAGE"]="$graph_storage"
ENV_VALUES["LIGHTRAG_DOC_STATUS_STORAGE"]="$doc_storage"
for storage in "$kv_storage" "$vector_storage" "$graph_storage" "$doc_storage"; do
if [[ -n "${STORAGE_DB_TYPES[$storage]:-}" ]]; then
REQUIRED_DB_TYPES["${STORAGE_DB_TYPES[$storage]}"]=1
fi
done
}
initialize_default_storage_backends() {
# env-base does not prompt for storage, but its generated .env must remain
# self-consistent for first-run users who have not run env-storage yet.
ENV_VALUES["LIGHTRAG_KV_STORAGE"]="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-JsonKVStorage}"
ENV_VALUES["LIGHTRAG_VECTOR_STORAGE"]="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-NanoVectorDBStorage}"
ENV_VALUES["LIGHTRAG_GRAPH_STORAGE"]="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-NetworkXStorage}"
ENV_VALUES["LIGHTRAG_DOC_STATUS_STORAGE"]="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-JsonDocStatusStorage}"
}
storage_service_name_for_db_type() {
local db_type="$1"
case "$db_type" in
postgresql)
printf 'postgres'
;;
neo4j|mongodb|redis|milvus|qdrant|memgraph|opensearch)
printf '%s' "$db_type"
;;
*)
printf ''
;;
esac
}
storage_deployment_marker_key() {
local db_type="$1"
case "$db_type" in
postgresql)
printf 'LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT'
;;
neo4j)
printf 'LIGHTRAG_SETUP_NEO4J_DEPLOYMENT'
;;
mongodb)
printf 'LIGHTRAG_SETUP_MONGODB_DEPLOYMENT'
;;
redis)
printf 'LIGHTRAG_SETUP_REDIS_DEPLOYMENT'
;;
milvus)
printf 'LIGHTRAG_SETUP_MILVUS_DEPLOYMENT'
;;
qdrant)
printf 'LIGHTRAG_SETUP_QDRANT_DEPLOYMENT'
;;
memgraph)
printf 'LIGHTRAG_SETUP_MEMGRAPH_DEPLOYMENT'
;;
opensearch)
printf 'LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT'
;;
*)
printf ''
;;
esac
}
storage_default_docker_for_db_type() {
local db_type="$1"
local marker_key
marker_key="$(storage_deployment_marker_key "$db_type")"
if [[ -n "$marker_key" && "${ENV_VALUES[$marker_key]:-}" == "docker" ]]; then
printf 'yes'
else
printf 'no'
fi
}
persist_storage_deployment_choice() {
local db_type="$1"
local deployment_mode="${2:-no}"
local marker_key
marker_key="$(storage_deployment_marker_key "$db_type")"
if [[ -z "$marker_key" ]]; then
return 0
fi
case "$deployment_mode" in
yes|docker)
ENV_VALUES["$marker_key"]="docker"
;;
no|'')
unset "ENV_VALUES[$marker_key]"
;;
*)
ENV_VALUES["$marker_key"]="$deployment_mode"
;;
esac
}
clear_unused_storage_deployment_markers() {
local db_type
for db_type in postgresql neo4j mongodb redis milvus qdrant memgraph opensearch; do
if [[ -z "${REQUIRED_DB_TYPES[$db_type]+set}" ]]; then
persist_storage_deployment_choice "$db_type" "no"
fi
done
}
collect_database_config() {
local db_type="$1"
local default_docker="${2:-no}"
local service_name=""
local deployment_mode="no"
# Storage collector rule for this wizard:
# - Existing ENV_VALUES loaded from .env are user-owned configuration.
# - Collectors should use those values as defaults and preserve them when they
# are already set, even for Docker-managed services.
# - A collector may normalize the stored form, or write a hard default only
# when the key is absent.
# Keep future storage collectors aligned with this behavior so rerunning the
# wizard does not silently erase explicit .env overrides.
case "$db_type" in
postgresql)
collect_postgres_config "$default_docker"
;;
neo4j)
collect_neo4j_config "$default_docker"
;;
mongodb)
collect_mongodb_config "$default_docker"
;;
redis)
collect_redis_config "$default_docker"
;;
milvus)
collect_milvus_config "$default_docker"
;;
qdrant)
collect_qdrant_config "$default_docker"
;;
memgraph)
collect_memgraph_config "$default_docker"
;;
opensearch)
collect_opensearch_config "$default_docker"
;;
*)
echo "Unknown database type: $db_type" >&2
return 1
;;
esac
service_name="$(storage_service_name_for_db_type "$db_type")"
if [[ -n "$service_name" && -n "${DOCKER_SERVICE_SET[$service_name]+set}" ]]; then
deployment_mode="docker"
fi
persist_storage_deployment_choice "$db_type" "$deployment_mode"
}
collect_postgres_config() {
local default_docker="${1:-no}"
local use_docker="no"
local host port user password database
local existing_user="" existing_password="" existing_database=""
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run PostgreSQL locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run PostgreSQL locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "postgres"
host="${ENV_VALUES[POSTGRES_HOST]:-localhost}"
if [[ "$host" != "localhost" && "$host" != "127.0.0.1" && "$host" != "0.0.0.0" && "$host" != "postgres" ]]; then
host="localhost"
elif [[ "$host" == "postgres" ]]; then
host="localhost"
fi
else
host="${ENV_VALUES[POSTGRES_HOST]:-localhost}"
fi
host="$(prompt_with_default "PostgreSQL host" "$host")"
if [[ "$use_docker" == "yes" ]]; then
port="5432"
set_compose_override "POSTGRES_HOST" "postgres"
set_compose_override "POSTGRES_PORT" "5432"
else
port="$(prompt_until_valid "PostgreSQL port" "${ENV_VALUES[POSTGRES_PORT]:-5432}" validate_port)"
set_compose_override "POSTGRES_HOST" ""
set_compose_override "POSTGRES_PORT" ""
fi
# The bundled postgres image creates its user/password/database from the
# POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB env vars on first start, so docker
# and host deployments share the same prompts and defaults (rag/rag/lightrag).
existing_user="${ORIGINAL_ENV_VALUES[POSTGRES_USER]-${ENV_VALUES[POSTGRES_USER]:-}}"
existing_password="${ORIGINAL_ENV_VALUES[POSTGRES_PASSWORD]-${ENV_VALUES[POSTGRES_PASSWORD]:-}}"
existing_database="${ORIGINAL_ENV_VALUES[POSTGRES_DATABASE]-${ENV_VALUES[POSTGRES_DATABASE]:-}}"
user="$(prompt_with_default "PostgreSQL user" "${existing_user:-rag}")"
password="$(prompt_secret_with_default "PostgreSQL password: " "${existing_password:-rag}")"
database="$(prompt_with_default "PostgreSQL database" "${existing_database:-lightrag}")"
ENV_VALUES["POSTGRES_HOST"]="$host"
ENV_VALUES["POSTGRES_PORT"]="$port"
ENV_VALUES["POSTGRES_USER"]="$user"
ENV_VALUES["POSTGRES_PASSWORD"]="$password"
ENV_VALUES["POSTGRES_DATABASE"]="$database"
}
collect_neo4j_config() {
local default_docker="${1:-no}"
local use_docker="no"
local uri username password database
local existing_username="" existing_password="" existing_database=""
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run Neo4j locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run Neo4j locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "neo4j"
uri="$(prefer_local_service_uri "${ENV_VALUES[NEO4J_URI]:-}" "neo4j://localhost:7687" "neo4j" "localhost" "127.0.0.1" "0.0.0.0")"
else
uri="${ENV_VALUES[NEO4J_URI]:-neo4j://localhost:7687}"
fi
uri="$(prompt_until_valid "Neo4j URI" "$uri" validate_uri neo4j)"
if [[ "$use_docker" == "yes" ]]; then
uri="$(normalize_neo4j_uri_for_local_service "$uri")"
fi
existing_username="${ORIGINAL_ENV_VALUES[NEO4J_USERNAME]-${ENV_VALUES[NEO4J_USERNAME]:-}}"
existing_password="${ORIGINAL_ENV_VALUES[NEO4J_PASSWORD]-${ENV_VALUES[NEO4J_PASSWORD]:-}}"
existing_database="${ORIGINAL_ENV_VALUES[NEO4J_DATABASE]-${ENV_VALUES[NEO4J_DATABASE]:-}}"
if [[ "$use_docker" == "yes" ]]; then
username="$(prompt_until_valid "Neo4j username" "${existing_username:-neo4j}" validate_non_empty)"
password="$(prompt_secret_until_valid_with_default "Neo4j password: " "${existing_password:-neo4j_password}" validate_non_empty)"
if [[ -n "$existing_database" ]]; then
database="$(prompt_with_default "Neo4j database" "$existing_database")"
else
database="neo4j"
fi
else
username="$(prompt_with_default "Neo4j username" "${existing_username:-neo4j}")"
password="$(prompt_secret_with_default "Neo4j password: " "${existing_password:-neo4j_password}")"
database="$(prompt_with_default "Neo4j database" "${existing_database:-neo4j}")"
fi
ENV_VALUES["NEO4J_URI"]="$uri"
ENV_VALUES["NEO4J_USERNAME"]="$username"
ENV_VALUES["NEO4J_PASSWORD"]="$password"
ENV_VALUES["NEO4J_DATABASE"]="$database"
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "NEO4J_URI" "neo4j://neo4j:7687"
else
set_compose_override "NEO4J_URI" ""
fi
}
collect_mongodb_config() {
local default_docker="${1:-no}"
local use_docker="no"
local uri database
local existing_database=""
local vector_search_required="no"
if [[ "${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" == "MongoVectorDBStorage" ]]; then
vector_search_required="yes"
fi
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run MongoDB locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run MongoDB locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
if [[ "$vector_search_required" == "yes" ]]; then
log_info "Docker MongoDB uses Atlas Local, so MongoVectorDBStorage can use Atlas Search / Vector Search locally."
fi
add_docker_service "mongodb"
uri="$(prefer_local_service_uri "${ENV_VALUES[MONGO_URI]:-}" "mongodb://localhost:27017/?directConnection=true" "mongodb" "localhost" "127.0.0.1" "0.0.0.0")"
elif [[ "$vector_search_required" == "yes" ]]; then
uri="${ENV_VALUES[MONGO_URI]:-}"
if [[ -z "$uri" ]] || is_wizard_managed_local_mongodb_uri "$uri"; then
uri="mongodb+srv://cluster.example.mongodb.net/"
fi
else
uri="${ENV_VALUES[MONGO_URI]:-mongodb://localhost:27017/}"
fi
if [[ "$vector_search_required" == "yes" ]]; then
uri="$(prompt_until_valid "MongoDB URI (must support Atlas Search / Vector Search)" "$uri" validate_uri mongodb)"
else
uri="$(prompt_until_valid "MongoDB URI" "$uri" validate_uri mongodb)"
fi
if [[ "$use_docker" == "yes" ]]; then
uri="$(normalize_mongodb_uri_for_local_service "$uri")"
fi
existing_database="${ORIGINAL_ENV_VALUES[MONGO_DATABASE]-${ENV_VALUES[MONGO_DATABASE]:-}}"
database="$(prompt_with_default "MongoDB database" "${existing_database:-LightRAG}")"
ENV_VALUES["MONGO_URI"]="$uri"
ENV_VALUES["MONGO_DATABASE"]="$database"
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "MONGO_URI" "mongodb://mongodb:27017/?directConnection=true"
else
set_compose_override "MONGO_URI" ""
fi
}
collect_redis_config() {
local default_docker="${1:-no}"
local use_docker="no"
local uri
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run Redis locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run Redis locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "redis"
uri="$(prefer_local_service_uri "${ENV_VALUES[REDIS_URI]:-}" "redis://localhost:6379" "redis" "localhost" "127.0.0.1" "0.0.0.0")"
else
uri="${ENV_VALUES[REDIS_URI]:-redis://localhost:6379}"
fi
uri="$(prompt_until_valid "Redis URI" "$uri" validate_uri redis)"
if [[ "$use_docker" == "yes" ]]; then
uri="$(normalize_redis_uri_for_local_service "$uri")"
fi
ENV_VALUES["REDIS_URI"]="$uri"
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "REDIS_URI" "redis://redis:6379"
else
set_compose_override "REDIS_URI" ""
fi
}
collect_milvus_config() {
local default_docker="${1:-no}"
local use_docker="no"
local uri db_name milvus_device=""
local existing_db_name="" existing_device=""
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run Milvus locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run Milvus locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "milvus"
uri="$(prefer_local_service_uri "${ENV_VALUES[MILVUS_URI]:-}" "http://localhost:19530" "milvus" "localhost" "127.0.0.1" "0.0.0.0")"
else
uri="${ENV_VALUES[MILVUS_URI]:-http://localhost:19530}"
fi
existing_db_name="${ORIGINAL_ENV_VALUES[MILVUS_DB_NAME]-${ENV_VALUES[MILVUS_DB_NAME]:-}}"
existing_device="${ORIGINAL_ENV_VALUES[MILVUS_DEVICE]-${ENV_VALUES[MILVUS_DEVICE]:-}}"
if [[ "$use_docker" == "yes" ]]; then
milvus_device="$(resolve_local_device_default "$existing_device")"
milvus_device="$(prompt_choice "Milvus device" "$milvus_device" "cpu" "cuda")"
if [[ "$milvus_device" == "cuda" ]] && ! host_cuda_available; then
log_warn "CUDA device selected for Milvus but no NVIDIA driver detected on host."
fi
uri="$(prompt_until_valid "Milvus URI" "$uri" validate_uri milvus)"
uri="$(normalize_milvus_uri_for_local_service "$uri")"
if [[ -z "${ENV_VALUES[MINIO_ACCESS_KEY_ID]:-}" ]]; then
ENV_VALUES["MINIO_ACCESS_KEY_ID"]="minioadmin"
fi
if [[ -z "${ENV_VALUES[MINIO_SECRET_ACCESS_KEY]:-}" ]]; then
ENV_VALUES["MINIO_SECRET_ACCESS_KEY"]="minioadmin"
fi
else
uri="$(prompt_until_valid "Milvus URI" "$uri" validate_uri milvus)"
fi
db_name="$(prompt_with_default "Milvus database name" "${existing_db_name:-lightrag}")"
ENV_VALUES["MILVUS_URI"]="$uri"
ENV_VALUES["MILVUS_DB_NAME"]="$db_name"
if [[ -n "$milvus_device" ]]; then
ENV_VALUES["MILVUS_DEVICE"]="$milvus_device"
fi
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "MILVUS_URI" "http://milvus:19530"
else
set_compose_override "MILVUS_URI" ""
fi
}
collect_qdrant_config() {
local default_docker="${1:-no}"
local use_docker="no"
local url qdrant_device=""
local existing_device=""
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run Qdrant locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run Qdrant locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "qdrant"
url="$(prefer_local_service_uri "${ENV_VALUES[QDRANT_URL]:-}" "http://localhost:6333" "qdrant" "localhost" "127.0.0.1" "0.0.0.0")"
else
url="${ENV_VALUES[QDRANT_URL]:-http://localhost:6333}"
fi
existing_device="${ORIGINAL_ENV_VALUES[QDRANT_DEVICE]-${ENV_VALUES[QDRANT_DEVICE]:-}}"
if [[ "$use_docker" == "yes" ]]; then
qdrant_device="$(resolve_local_device_default "$existing_device")"
qdrant_device="$(prompt_choice "Qdrant device" "$qdrant_device" "cpu" "cuda")"
if [[ "$qdrant_device" == "cuda" ]] && ! host_cuda_available; then
log_warn "CUDA device selected for Qdrant but no NVIDIA driver detected on host."
fi
url="$(prompt_until_valid "Qdrant URL" "$url" validate_uri qdrant)"
url="$(normalize_qdrant_uri_for_local_service "$url")"
else
url="$(prompt_until_valid "Qdrant URL" "$url" validate_uri qdrant)"
fi
ENV_VALUES["QDRANT_URL"]="$url"
if [[ -n "$qdrant_device" ]]; then
ENV_VALUES["QDRANT_DEVICE"]="$qdrant_device"
fi
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "QDRANT_URL" "http://qdrant:6333"
else
set_compose_override "QDRANT_URL" ""
fi
}
collect_memgraph_config() {
local default_docker="${1:-no}"
local use_docker="no"
local uri
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run Memgraph locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run Memgraph locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "memgraph"
uri="$(prefer_local_service_uri "${ENV_VALUES[MEMGRAPH_URI]:-}" "bolt://localhost:7687" "memgraph" "localhost" "127.0.0.1" "0.0.0.0")"
else
uri="${ENV_VALUES[MEMGRAPH_URI]:-bolt://localhost:7687}"
fi
uri="$(prompt_until_valid "Memgraph URI" "$uri" validate_uri memgraph)"
if [[ "$use_docker" == "yes" ]]; then
uri="$(normalize_memgraph_uri_for_local_service "$uri")"
fi
ENV_VALUES["MEMGRAPH_URI"]="$uri"
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "MEMGRAPH_URI" "bolt://memgraph:7687"
else
set_compose_override "MEMGRAPH_URI" ""
fi
}
collect_opensearch_config() {
local default_docker="${1:-no}"
local use_docker="no"
local hosts user password
local existing_user="" existing_password=""
local existing_use_ssl="" existing_verify_certs=""
local existing_num_shards="" existing_num_replicas=""
local use_ssl="true"
local verify_certs="false"
local use_ssl_default="yes"
local verify_certs_default="no"
if [[ "$default_docker" == "yes" ]]; then
if confirm_default_yes "Run OpenSearch locally via Docker?"; then
use_docker="yes"
fi
else
if confirm_default_no "Run OpenSearch locally via Docker?"; then
use_docker="yes"
fi
fi
if [[ "$use_docker" == "yes" ]]; then
add_docker_service "opensearch"
hosts="$(prefer_local_service_uri "${ENV_VALUES[OPENSEARCH_HOSTS]:-}" "localhost:9200" "opensearch" "localhost" "127.0.0.1" "0.0.0.0")"
else
hosts="${ENV_VALUES[OPENSEARCH_HOSTS]:-localhost:9200}"
fi
existing_user="${ORIGINAL_ENV_VALUES[OPENSEARCH_USER]-${ENV_VALUES[OPENSEARCH_USER]:-}}"
existing_password="${ORIGINAL_ENV_VALUES[OPENSEARCH_PASSWORD]-${ENV_VALUES[OPENSEARCH_PASSWORD]:-}}"
existing_use_ssl="${ORIGINAL_ENV_VALUES[OPENSEARCH_USE_SSL]-${ENV_VALUES[OPENSEARCH_USE_SSL]:-}}"
existing_verify_certs="${ORIGINAL_ENV_VALUES[OPENSEARCH_VERIFY_CERTS]-${ENV_VALUES[OPENSEARCH_VERIFY_CERTS]:-}}"
existing_num_shards="${ORIGINAL_ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]-${ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]:-}}"
existing_num_replicas="${ORIGINAL_ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]-${ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]:-}}"
hosts="$(prompt_until_valid "OpenSearch hosts (host:port, comma-separated)" "$hosts" validate_opensearch_hosts_format)"
user="$(prompt_with_default "OpenSearch user" "${existing_user:-admin}")"
password="$(prompt_secret_until_valid_with_default "OpenSearch password: " "${existing_password:-LightRAG2026_!@}" validate_opensearch_password_strength)"
if [[ "$use_docker" == "yes" ]]; then
if [[ -n "$existing_use_ssl" ]]; then
env_value_is_true "$existing_use_ssl" && use_ssl="true" || use_ssl="false"
else
use_ssl="true"
fi
verify_certs="false"
else
if [[ -n "$existing_use_ssl" ]] && ! env_value_is_true "$existing_use_ssl"; then
use_ssl_default="no"
fi
if [[ "$use_ssl_default" == "yes" ]]; then
confirm_default_yes "Use SSL for OpenSearch?" && use_ssl="true" || use_ssl="false"
else
confirm_default_no "Use SSL for OpenSearch?" && use_ssl="true" || use_ssl="false"
fi
if [[ "$use_ssl" == "true" ]]; then
if [[ -n "$existing_verify_certs" ]] && env_value_is_true "$existing_verify_certs"; then
verify_certs_default="yes"
fi
if [[ "$verify_certs_default" == "yes" ]]; then
confirm_default_yes "Verify OpenSearch TLS certificates?" && verify_certs="true" || verify_certs="false"
else
confirm_default_no "Verify OpenSearch TLS certificates?" && verify_certs="true" || verify_certs="false"
fi
fi
fi
local num_shards num_replicas
if [[ "$use_docker" == "yes" ]]; then
num_shards="1"
num_replicas="0"
else
num_shards="$(prompt_until_valid "Number of index shards" "${existing_num_shards:-1}" validate_positive_integer)"
num_replicas="$(prompt_until_valid "Number of index replicas (use 2 for 3-AZ clusters)" "${existing_num_replicas:-0}" validate_non_negative_integer)"
fi
ENV_VALUES["OPENSEARCH_HOSTS"]="$hosts"
ENV_VALUES["OPENSEARCH_USER"]="$user"
ENV_VALUES["OPENSEARCH_PASSWORD"]="$password"
ENV_VALUES["OPENSEARCH_USE_SSL"]="$use_ssl"
ENV_VALUES["OPENSEARCH_VERIFY_CERTS"]="$verify_certs"
ENV_VALUES["OPENSEARCH_NUMBER_OF_SHARDS"]="$num_shards"
ENV_VALUES["OPENSEARCH_NUMBER_OF_REPLICAS"]="$num_replicas"
if [[ "$use_docker" == "yes" ]]; then
set_compose_override "OPENSEARCH_HOSTS" "opensearch:9200"
else
set_compose_override "OPENSEARCH_HOSTS" ""
fi
}
clear_bedrock_credentials() {
unset 'ENV_VALUES[AWS_ACCESS_KEY_ID]'
unset 'ENV_VALUES[AWS_SECRET_ACCESS_KEY]'
unset 'ENV_VALUES[AWS_SESSION_TOKEN]'
unset 'ENV_VALUES[AWS_REGION]'
}
collect_bedrock_credentials() {
local access_key secret_key session_token region
log_info "Bedrock ignores LLM_BINDING_API_KEY/EMBEDDING_BINDING_API_KEY; use SigV4 credentials or AWS_BEARER_TOKEN_BEDROCK."
if [[ -n "${ENV_VALUES[AWS_ACCESS_KEY_ID]:-}" && -n "${ENV_VALUES[AWS_SECRET_ACCESS_KEY]:-}" ]]; then
if confirm_default_yes "Reuse existing AWS Bedrock credentials?"; then
region="$(prompt_with_default "AWS region" "${ENV_VALUES[AWS_REGION]:-us-east-1}")"
ENV_VALUES["AWS_REGION"]="$region"
return 0
fi
fi
if confirm_default_no "Store explicit AWS Bedrock credentials in .env?"; then
access_key="$(prompt_required_secret "AWS access key ID: ")"
secret_key="$(prompt_required_secret "AWS secret access key: ")"
session_token="$(mask_sensitive_input "AWS session token (optional): ")"
region="$(prompt_with_default "AWS region" "${ENV_VALUES[AWS_REGION]:-us-east-1}")"
ENV_VALUES["AWS_ACCESS_KEY_ID"]="$access_key"
ENV_VALUES["AWS_SECRET_ACCESS_KEY"]="$secret_key"
ENV_VALUES["AWS_REGION"]="$region"
if [[ -n "$session_token" ]]; then
ENV_VALUES["AWS_SESSION_TOKEN"]="$session_token"
else
unset 'ENV_VALUES[AWS_SESSION_TOKEN]'
fi
return 0
fi
log_info "Using the ambient AWS credential chain (for example IAM roles, AWS profiles, or aws sso login)."
clear_bedrock_credentials
region="$(prompt_clearable_with_default "AWS region (optional)" "${ENV_VALUES[AWS_REGION]:-}")"
apply_clearable_env_value "AWS_REGION" "$region"
}
store_optional_env_value() {
local key="$1"
local value="${2:-}"
if [[ -n "$value" ]]; then
ENV_VALUES["$key"]="$value"
else
unset "ENV_VALUES[$key]"
fi
}
provider_default_or_existing() {
local selected_binding="$1"
local existing_binding="${2:-}"
local existing_value="${3:-}"
local default_value="${4:-}"
if [[ "$selected_binding" == "$existing_binding" && -n "$existing_value" ]]; then
printf '%s' "$existing_value"
return 0
fi
printf '%s' "$default_value"
}
default_llm_model_for_binding() {
local binding="$1"
case "$binding" in
openai|azure_openai)
printf 'gpt-5-mini'
;;
ollama|lollms|openai-ollama)
printf 'mistral-nemo:latest'
;;
gemini)
printf 'gemini-flash-latest'
;;
bedrock)
printf 'anthropic.claude-3-5-sonnet-20241022-v2:0'
;;
*)
printf 'gpt-5-mini'
;;
esac
}
default_embedding_model_for_binding() {
local binding="$1"
case "$binding" in
openai|azure_openai)
printf 'text-embedding-3-large'
;;
ollama)
printf 'bge-m3:latest'
;;
jina)
printf 'jina-embeddings-v4'
;;
gemini)
printf 'gemini-embedding-001'
;;
bedrock)
printf 'amazon.titan-embed-text-v2:0'
;;
lollms)
printf 'lollms_embedding_model'
;;
*)
printf 'text-embedding-3-large'
;;
esac
}
default_embedding_dim_for_binding() {
local binding="$1"
case "$binding" in
openai|azure_openai)
printf '3072'
;;
ollama|bedrock|lollms)
printf '1024'
;;
jina)
printf '2048'
;;
gemini)
printf '1536'
;;
*)
printf '3072'
;;
esac
}
collect_llm_config() {
local options=("openai" "azure_openai" "ollama" "openai-ollama" "lollms" "gemini" "bedrock")
local current_binding="${ENV_VALUES[LLM_BINDING]:-openai}"
local binding model model_default host host_default api_key
binding="$(prompt_choice "LLM provider" "$current_binding" "${options[@]}")"
model_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_MODEL]:-}" "$(default_llm_model_for_binding "$binding")")"
model="$(prompt_with_default "LLM model" "$model_default")"
case "$binding" in
ollama)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "$(default_loopback_url 11434)")"
host="$(prompt_with_default "Ollama host" "$host_default")"
api_key=""
;;
openai-ollama)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "$(default_loopback_url 11434 "/v1")")"
host="$(prompt_with_default "OpenAI-compatible Ollama endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "LLM API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key openai)"
;;
lollms)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "http://localhost:9600")"
host="$(prompt_with_default "LoLLMs host" "$host_default")"
api_key=""
;;
azure_openai)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "https://example.openai.azure.com/")"
host="$(prompt_with_default "Azure OpenAI endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Azure OpenAI API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key azure_openai)"
;;
gemini)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "DEFAULT_GEMINI_ENDPOINT")"
host="$(prompt_with_default "Gemini endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Gemini API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key gemini)"
;;
bedrock)
host="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "DEFAULT_BEDROCK_ENDPOINT")"
api_key=""
collect_bedrock_credentials
;;
*)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[LLM_BINDING_HOST]:-}" "https://api.openai.com/v1")"
host="$(prompt_with_default "LLM endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "LLM API key: " "${ENV_VALUES[LLM_BINDING_API_KEY]:-}" validate_api_key "$binding")"
;;
esac
ENV_VALUES["LLM_BINDING"]="$binding"
ENV_VALUES["LLM_MODEL"]="$model"
ENV_VALUES["LLM_BINDING_HOST"]="$host"
store_optional_env_value "LLM_BINDING_API_KEY" "$api_key"
# Role-specific LLM models — default to the base LLM_MODEL when unset in .env.
local keyword_default query_default keyword_model query_model
keyword_default="${ENV_VALUES[KEYWORD_LLM_MODEL]:-$model}"
query_default="${ENV_VALUES[QUERY_LLM_MODEL]:-$model}"
keyword_model="$(prompt_with_default "Keyword LLM model" "$keyword_default")"
query_model="$(prompt_with_default "Query LLM model" "$query_default")"
ENV_VALUES["KEYWORD_LLM_MODEL"]="$keyword_model"
ENV_VALUES["QUERY_LLM_MODEL"]="$query_model"
}
collect_embedding_config() {
local options=("openai" "azure_openai" "ollama" "jina" "lollms" "gemini" "bedrock")
local current_binding="${ENV_VALUES[EMBEDDING_BINDING]:-openai}"
local binding model model_default host host_default api_key dim dim_default
if [[ "${ENV_VALUES[LLM_BINDING]:-}" == "openai-ollama" ]]; then
binding="ollama"
if [[ "$current_binding" != "ollama" ]]; then
log_info "openai-ollama uses Ollama embeddings. Forcing embedding provider to ollama."
fi
else
binding="$(prompt_choice "Embedding provider" "$current_binding" "${options[@]}")"
fi
model_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_MODEL]:-}" "$(default_embedding_model_for_binding "$binding")")"
dim_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_DIM]:-}" "$(default_embedding_dim_for_binding "$binding")")"
model="$(prompt_with_default "Embedding model" "$model_default")"
dim="$(prompt_with_default "Embedding dimension" "$dim_default")"
local llm_host_fallback="" llm_api_key_default=""
if [[ "$binding" == "${ENV_VALUES[LLM_BINDING]:-}" ]]; then
llm_host_fallback="${ENV_VALUES[LLM_BINDING_HOST]:-}"
llm_api_key_default="${ENV_VALUES[LLM_BINDING_API_KEY]:-}"
fi
case "$binding" in
ollama)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-$(default_loopback_url 11434)}")"
host="$(prompt_with_default "Ollama embedding host" "$host_default")"
api_key=""
;;
lollms)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-http://localhost:9600}")"
host="$(prompt_with_default "LoLLMs embedding host" "$host_default")"
api_key=""
;;
azure_openai)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-https://example.openai.azure.com/}")"
host="$(prompt_with_default "Azure OpenAI endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Azure OpenAI API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key azure_openai)"
;;
gemini)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-DEFAULT_GEMINI_ENDPOINT}")"
host="$(prompt_with_default "Gemini endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Gemini API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key gemini)"
;;
bedrock)
host="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-DEFAULT_BEDROCK_ENDPOINT}")"
api_key=""
collect_bedrock_credentials
;;
jina)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-https://api.jina.ai/v1/embeddings}")"
host="$(prompt_with_default "Jina endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Jina API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key jina)"
;;
*)
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}" "${llm_host_fallback:-https://api.openai.com/v1}")"
host="$(prompt_with_default "Embedding endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Embedding API key: " "${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-$llm_api_key_default}" validate_api_key "$binding")"
;;
esac
ENV_VALUES["EMBEDDING_BINDING"]="$binding"
ENV_VALUES["EMBEDDING_MODEL"]="$model"
ENV_VALUES["EMBEDDING_DIM"]="$dim"
ENV_VALUES["EMBEDDING_BINDING_HOST"]="$host"
store_optional_env_value "EMBEDDING_BINDING_API_KEY" "$api_key"
# User chose a remote provider — clear the Docker deployment marker.
unset 'ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]'
}
collect_rerank_config() {
# Pass "yes" to skip the "Enable reranking?" prompt (caller already asked it).
# The optional second argument is retained for caller compatibility.
local skip_enable_check="${1:-no}"
local _docker_choice_override="${2:-prompt}"
local options=("cohere" "jina" "aliyun")
local current_binding="${ENV_VALUES[RERANK_BINDING]:-cohere}"
local binding model host api_key
local default_model="" default_host="" model_default="" host_default=""
local previous_provider="${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}"
unset 'ENV_VALUES[VLLM_RERANK_DTYPE]'
if [[ "$skip_enable_check" != "yes" ]]; then
local rerank_was_enabled="no"
if [[ -n "${ENV_VALUES[RERANK_BINDING]:-}" && "${ENV_VALUES[RERANK_BINDING]}" != "null" ]]; then
rerank_was_enabled="yes"
fi
local rerank_enabled="no"
if [[ "$rerank_was_enabled" == "yes" ]]; then
confirm_default_yes "Enable reranking?" && rerank_enabled="yes"
else
confirm_default_no "Enable reranking?" && rerank_enabled="yes"
fi
if [[ "$rerank_enabled" != "yes" ]]; then
ENV_VALUES["RERANK_BINDING"]="null"
unset 'ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]'
return
fi
fi
if [[ "$current_binding" == "null" ]]; then
current_binding="cohere"
fi
binding="$(prompt_choice "Rerank provider" "$current_binding" "${options[@]}")"
case "$binding" in
cohere)
default_model="rerank-v3.5"
default_host="https://api.cohere.com/v2/rerank"
;;
jina)
default_model="jina-reranker-v2-base-multilingual"
default_host="https://api.jina.ai/v1/rerank"
;;
aliyun)
default_model="gte-rerank-v2"
default_host="https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank"
;;
*)
default_model=""
default_host=""
;;
esac
if [[ "$previous_provider" == "vllm" ]]; then
# Switching away from local vLLM should replace stale localhost/model values.
model_default="$default_model"
host_default="$default_host"
else
model_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[RERANK_MODEL]:-}" "$default_model")"
host_default="$(provider_default_or_existing "$binding" "$current_binding" "${ENV_VALUES[RERANK_BINDING_HOST]:-}" "$default_host")"
fi
model="$(prompt_with_default "Rerank model" "$model_default")"
host="$(prompt_with_default "Rerank endpoint" "$host_default")"
api_key="$(prompt_secret_until_valid_with_default "Rerank API key: " "${ENV_VALUES[RERANK_BINDING_API_KEY]:-}" validate_api_key "$binding")"
ENV_VALUES["RERANK_BINDING"]="$binding"
# Only env_base_flow's Docker branch should keep the local vLLM setup marker.
unset 'ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]'
if [[ -n "$model" ]]; then
ENV_VALUES["RERANK_MODEL"]="$model"
fi
if [[ -n "$host" ]]; then
ENV_VALUES["RERANK_BINDING_HOST"]="$host"
fi
store_optional_env_value "RERANK_BINDING_API_KEY" "$api_key"
}
collect_server_config() {
local host port title description summary_language
host="$(prompt_with_default "Server host" "${ENV_VALUES[HOST]:-0.0.0.0}")"
port="$(prompt_until_valid "Server port" "${ENV_VALUES[PORT]:-9621}" validate_port)"
title="$(prompt_with_default "WebUI title" "${ENV_VALUES[WEBUI_TITLE]:-My Graph KB}")"
description="$(prompt_with_default "WebUI description" "${ENV_VALUES[WEBUI_DESCRIPTION]:-Simple and Fast Graph Based RAG System}")"
summary_language="$(prompt_with_default "Summary language" "${ENV_VALUES[SUMMARY_LANGUAGE]:-English}")"
ENV_VALUES["HOST"]="$host"
ENV_VALUES["PORT"]="$port"
ENV_VALUES["WEBUI_TITLE"]="$title"
ENV_VALUES["WEBUI_DESCRIPTION"]="$description"
ENV_VALUES["SUMMARY_LANGUAGE"]="$summary_language"
}
collect_ssl_config() {
local cert key
local ssl_enabled_default="no"
case "${ENV_VALUES[SSL]:-}" in
true|TRUE|True|1|yes|YES|Yes|y|Y|on|ON|On|t|T)
ssl_enabled_default="yes"
;;
esac
if [[ "$ssl_enabled_default" == "yes" ]]; then
if ! confirm_default_yes "Enable SSL/TLS for the API server?"; then
unset 'ENV_VALUES[SSL]'
unset 'ENV_VALUES[SSL_CERTFILE]'
unset 'ENV_VALUES[SSL_KEYFILE]'
SSL_CERT_SOURCE_PATH=""
SSL_KEY_SOURCE_PATH=""
return
fi
else
if ! confirm_default_no "Enable SSL/TLS for the API server?"; then
unset 'ENV_VALUES[SSL]'
unset 'ENV_VALUES[SSL_CERTFILE]'
unset 'ENV_VALUES[SSL_KEYFILE]'
SSL_CERT_SOURCE_PATH=""
SSL_KEY_SOURCE_PATH=""
return
fi
fi
cert="$(prompt_until_valid "SSL certificate file" "${ENV_VALUES[SSL_CERTFILE]:-}" validate_existing_file)"
key="$(prompt_until_valid "SSL key file" "${ENV_VALUES[SSL_KEYFILE]:-}" validate_existing_file)"
ENV_VALUES["SSL"]="true"
ENV_VALUES["SSL_CERTFILE"]="$cert"
ENV_VALUES["SSL_KEYFILE"]="$key"
SSL_CERT_SOURCE_PATH="$cert"
SSL_KEY_SOURCE_PATH="$key"
}
collect_security_config() {
local required="${1:-no}"
local default_yes="${2:-no}"
local auth_accounts token_secret token_expire api_key whitelist
local confirm_result=1
local whitelist_default=""
local whitelist_is_set="no"
if [[ -n "${ENV_VALUES[WHITELIST_PATHS]+set}" ]]; then
whitelist_default="${ENV_VALUES[WHITELIST_PATHS]}"
whitelist_is_set="yes"
fi
if [[ "$default_yes" == "yes" ]]; then
if confirm_default_yes "Configure authentication and API key settings?"; then
confirm_result=0
fi
else
if confirm_default_no "Configure authentication and API key settings?"; then
confirm_result=0
fi
fi
if ((confirm_result != 0)); then
if [[ "$required" == "yes" ]]; then
echo "Warning: production deployments should configure AUTH_ACCOUNTS; API keys are optional on top." >&2
fi
return
fi
echo "Press Enter to keep an existing value. Type 'clear' to remove it." >&2
if [[ "$whitelist_is_set" == "no" ]]; then
whitelist_default="/health"
elif [[ "$required" == "yes" && "$whitelist_default" == "/health,/api/*" ]]; then
whitelist_default="/health"
fi
auth_accounts="$(prompt_clearable_with_default "Auth accounts (user:pass,comma-separated)" "${ENV_VALUES[AUTH_ACCOUNTS]:-}")"
token_secret="$(prompt_clearable_secret_with_default "JWT token secret: " "${ENV_VALUES[TOKEN_SECRET]:-}")"
token_expire="$(prompt_clearable_with_default "Token expire hours" "${ENV_VALUES[TOKEN_EXPIRE_HOURS]:-48}")"
api_key="$(prompt_clearable_secret_with_default "LightRAG API key: " "${ENV_VALUES[LIGHTRAG_API_KEY]:-}")"
whitelist="$(prompt_clearable_with_default "Whitelist paths (comma-separated)" "$whitelist_default")"
if [[ "$whitelist_is_set" == "yes" && -z "$whitelist_default" && -z "$whitelist" ]]; then
whitelist="$CLEAR_INPUT_SENTINEL"
fi
if [[ -z "$token_secret" ]]; then
token_secret="$(openssl rand -hex 32 2>/dev/null || LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 64)"
log_info "Generated TOKEN_SECRET and saved to .env."
fi
apply_clearable_env_value "AUTH_ACCOUNTS" "$auth_accounts"
apply_clearable_env_value "TOKEN_SECRET" "$token_secret"
apply_clearable_env_value "TOKEN_EXPIRE_HOURS" "$token_expire"
apply_clearable_env_value "LIGHTRAG_API_KEY" "$api_key"
apply_clearable_env_value "WHITELIST_PATHS" "$whitelist" "empty"
}
apply_clearable_env_value() {
local key="$1"
local value="${2:-}"
local clear_mode="${3:-unset}"
if [[ "$clear_mode" == "empty" && "$value" == "$CLEAR_INPUT_SENTINEL" ]]; then
ENV_VALUES["$key"]=""
return 0
fi
if [[ "$value" == "$CLEAR_INPUT_SENTINEL" || -z "$value" ]]; then
unset "ENV_VALUES[$key]"
return 0
fi
ENV_VALUES["$key"]="$value"
}
collect_observability_config() {
local secret_key public_key host
if ! confirm_default_no "Enable Langfuse observability?"; then
unset 'ENV_VALUES[LANGFUSE_ENABLE_TRACE]'
unset 'ENV_VALUES[LANGFUSE_SECRET_KEY]'
unset 'ENV_VALUES[LANGFUSE_PUBLIC_KEY]'
unset 'ENV_VALUES[LANGFUSE_HOST]'
return
fi
secret_key="$(prompt_secret_until_valid_with_default "Langfuse secret key: " "${ENV_VALUES[LANGFUSE_SECRET_KEY]:-}" validate_api_key langfuse)"
public_key="$(prompt_secret_until_valid_with_default "Langfuse public key: " "${ENV_VALUES[LANGFUSE_PUBLIC_KEY]:-}" validate_api_key langfuse)"
host="$(prompt_with_default "Langfuse host" "${ENV_VALUES[LANGFUSE_HOST]:-https://cloud.langfuse.com}")"
if [[ -n "$secret_key" ]]; then
ENV_VALUES["LANGFUSE_SECRET_KEY"]="$secret_key"
fi
if [[ -n "$public_key" ]]; then
ENV_VALUES["LANGFUSE_PUBLIC_KEY"]="$public_key"
fi
if [[ -n "$host" ]]; then
ENV_VALUES["LANGFUSE_HOST"]="$host"
fi
ENV_VALUES["LANGFUSE_ENABLE_TRACE"]="true"
}
show_summary() {
local key
local value
echo
log_info "Configuration summary:"
if ((${#ENV_VALUES[@]} > 0)); then
local -a sorted_keys
mapfile -t sorted_keys < <(printf '%s\n' "${!ENV_VALUES[@]}" | sort)
for key in "${sorted_keys[@]}"; do
value="${ENV_VALUES[$key]}"
if is_sensitive_env_key "$key"; then
value="***"
fi
printf ' %s=%s\n' "$key" "$value"
done
fi
if ((${#DOCKER_SERVICES[@]} > 0)); then
echo
log_info "Docker services to include:"
for service in "${DOCKER_SERVICES[@]}"; do
echo " - $service"
done
echo " Compose file: docker-compose.final.yml"
fi
}
# Preserve already-staged SSL mounts when regenerating compose output. The
# setup wizards treat .env as the configuration for the current target runtime,
# not as a single file guaranteed to work for both host and Docker Compose at
# the same time. A later wizard run may rewrite .env again when the operator
# switches between host and compose workflows.
prepare_inherited_ssl_assets_for_compose() {
local existing_compose="${1:-}"
local staged_cert_source="$SSL_CERT_SOURCE_PATH"
local staged_key_source="$SSL_KEY_SOURCE_PATH"
local preserved_cert_path=""
local preserved_key_path=""
if [[ -n "$SSL_CERT_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_CERT_SOURCE_PATH"; then
if [[ -n "$existing_compose" ]]; then
preserved_cert_path="$(read_service_environment_value "$existing_compose" "lightrag" "SSL_CERTFILE" || true)"
fi
if [[ "$preserved_cert_path" == /app/data/certs/* ]]; then
log_warn "SSL_CERTFILE source is missing; preserving the existing compose SSL certificate mount."
staged_cert_source=""
ENV_VALUES["SSL_CERTFILE"]="$preserved_cert_path"
set_compose_override "SSL_CERTFILE" "$preserved_cert_path"
else
format_error "Invalid SSL_CERTFILE" \
"Set it to an existing certificate file, disable SSL, or rerun the wizard to choose a new certificate."
return 1
fi
fi
if [[ -n "$SSL_KEY_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_KEY_SOURCE_PATH"; then
if [[ -n "$existing_compose" ]]; then
preserved_key_path="$(read_service_environment_value "$existing_compose" "lightrag" "SSL_KEYFILE" || true)"
fi
if [[ "$preserved_key_path" == /app/data/certs/* ]]; then
log_warn "SSL_KEYFILE source is missing; preserving the existing compose SSL key mount."
staged_key_source=""
ENV_VALUES["SSL_KEYFILE"]="$preserved_key_path"
set_compose_override "SSL_KEYFILE" "$preserved_key_path"
else
format_error "Invalid SSL_KEYFILE" \
"Set it to an existing private key file, disable SSL, or rerun the wizard to choose a new key."
return 1
fi
fi
SSL_CERT_SOURCE_PATH="$staged_cert_source"
SSL_KEY_SOURCE_PATH="$staged_key_source"
if [[ -n "$SSL_CERT_SOURCE_PATH" || -n "$SSL_KEY_SOURCE_PATH" ]]; then
stage_ssl_assets "$SSL_CERT_SOURCE_PATH" "$SSL_KEY_SOURCE_PATH"
fi
}
prepare_managed_service_assets_for_compose() {
local existing_compose="${1:-}"
if ! prepare_inherited_ssl_assets_for_compose "$existing_compose"; then
return 1
fi
if [[ -n "${DOCKER_SERVICE_SET[redis]:-}" ]]; then
stage_redis_config_asset || return 1
fi
}
env_base_flow() {
local vllm_embed_api_key=""
local vllm_rerank_api_key=""
local existing_vllm_embed_model=""
local existing_embedding_dim=""
local existing_vllm_embed_port=""
local existing_vllm_embed_host=""
local existing_vllm_embed_device=""
local previous_embedding_provider=""
local existing_vllm_rerank_model=""
local existing_vllm_rerank_port=""
local existing_vllm_rerank_host=""
local existing_vllm_rerank_device=""
local previous_rerank_provider=""
if host_cuda_available; then
log_info "GPU detected: NVIDIA GPU found. New local vLLM services default to CUDA (GPU image + float16)."
else
log_info "GPU detection: no NVIDIA GPU found. New local vLLM services default to CPU image + float32."
fi
reset_state
load_existing_env_if_present
initialize_default_storage_backends
log_info "Base configuration wizard (LLM / Embedding / Reranker)"
echo "This wizard only modifies LLM, embedding, and reranker settings."
echo "Storage, server, and security settings are preserved."
echo ""
log_step "LLM configuration"
collect_llm_config
echo ""
# ── Embedding ────────────────────────────────────────────────────────────────
log_step "Embedding configuration"
local docker_embed_default="no"
previous_embedding_provider="${ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]:-}"
if [[ "$previous_embedding_provider" == "vllm" ]]; then
docker_embed_default="yes"
fi
local use_docker_embed="no"
if [[ "$docker_embed_default" == "yes" ]]; then
confirm_default_yes "Run embedding model locally via Docker (vLLM)?" && use_docker_embed="yes" || use_docker_embed="no"
else
confirm_default_no "Run embedding model locally via Docker (vLLM)?" && use_docker_embed="yes" || use_docker_embed="no"
fi
if [[ "$use_docker_embed" == "yes" ]]; then
existing_vllm_embed_model="${ORIGINAL_ENV_VALUES[VLLM_EMBED_MODEL]-${ENV_VALUES[VLLM_EMBED_MODEL]:-}}"
existing_embedding_dim="${ORIGINAL_ENV_VALUES[EMBEDDING_DIM]-${ENV_VALUES[EMBEDDING_DIM]:-}}"
existing_vllm_embed_port="${ORIGINAL_ENV_VALUES[VLLM_EMBED_PORT]-${ENV_VALUES[VLLM_EMBED_PORT]:-}}"
existing_vllm_embed_host="${ORIGINAL_ENV_VALUES[EMBEDDING_BINDING_HOST]-${ENV_VALUES[EMBEDDING_BINDING_HOST]:-}}"
existing_vllm_embed_device="${ORIGINAL_ENV_VALUES[VLLM_EMBED_DEVICE]-${ENV_VALUES[VLLM_EMBED_DEVICE]:-}}"
apply_preset_overwrite "${PRESET_VLLM_EMBEDDING[@]}"
local vllm_embed_device
vllm_embed_device="$(resolve_local_device_default "$existing_vllm_embed_device")"
vllm_embed_device="$(prompt_choice "Embedding device" "$vllm_embed_device" "cpu" "cuda")"
if [[ "$vllm_embed_device" == "cuda" ]] && ! host_cuda_available; then
log_warn "CUDA device selected for vLLM embedding but no NVIDIA driver detected on host."
fi
if [[ -n "$existing_vllm_embed_port" ]]; then
ENV_VALUES["VLLM_EMBED_PORT"]="$existing_vllm_embed_port"
fi
if [[ -n "$existing_embedding_dim" ]]; then
ENV_VALUES["EMBEDDING_DIM"]="$existing_embedding_dim"
fi
if [[ "$previous_embedding_provider" == "vllm" && -n "$existing_vllm_embed_host" ]]; then
ENV_VALUES["EMBEDDING_BINDING_HOST"]="$existing_vllm_embed_host"
else
ENV_VALUES["EMBEDDING_BINDING_HOST"]="http://localhost:${ENV_VALUES[VLLM_EMBED_PORT]:-8001}/v1"
fi
local embed_model
embed_model="$(prompt_with_default "Embedding model" "${existing_vllm_embed_model:-${ENV_VALUES[VLLM_EMBED_MODEL]:-BAAI/bge-m3}}")"
ENV_VALUES["VLLM_EMBED_MODEL"]="$embed_model"
ENV_VALUES["EMBEDDING_MODEL"]="$embed_model"
ENV_VALUES["VLLM_EMBED_DEVICE"]="$vllm_embed_device"
ENV_VALUES["LIGHTRAG_SETUP_EMBEDDING_PROVIDER"]="vllm"
vllm_embed_api_key="${ENV_VALUES[VLLM_EMBED_API_KEY]:-${ENV_VALUES[EMBEDDING_BINDING_API_KEY]:-}}"
if [[ -z "$vllm_embed_api_key" ]]; then
vllm_embed_api_key="$(openssl rand -hex 16 2>/dev/null || LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)"
fi
ENV_VALUES["VLLM_EMBED_API_KEY"]="$vllm_embed_api_key"
ENV_VALUES["EMBEDDING_BINDING_API_KEY"]="$vllm_embed_api_key"
add_docker_service "vllm-embed"
set_compose_override "EMBEDDING_BINDING_HOST" \
"http://vllm-embed:${ENV_VALUES[VLLM_EMBED_PORT]:-8001}/v1"
else
collect_embedding_config
fi
echo ""
# ── Reranker ─────────────────────────────────────────────────────────────────
log_step "Reranker configuration"
local rerank_enabled_default="no"
if [[ -n "${ENV_VALUES[RERANK_BINDING]:-}" && "${ENV_VALUES[RERANK_BINDING]}" != "null" ]]; then
rerank_enabled_default="yes"
fi
previous_rerank_provider="${ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}"
local enable_reranking="no"
if [[ "$rerank_enabled_default" == "yes" ]]; then
confirm_default_yes "Enable reranking?" && enable_reranking="yes" || enable_reranking="no"
else
confirm_default_no "Enable reranking?" && enable_reranking="yes" || enable_reranking="no"
fi
if [[ "$enable_reranking" == "yes" ]]; then
local docker_rerank_default="no"
if [[ "$previous_rerank_provider" == "vllm" ]]; then
docker_rerank_default="yes"
fi
local use_docker_rerank="no"
if [[ "$docker_rerank_default" == "yes" ]]; then
confirm_default_yes "Run rerank service locally via Docker?" && use_docker_rerank="yes" || use_docker_rerank="no"
else
confirm_default_no "Run rerank service locally via Docker?" && use_docker_rerank="yes" || use_docker_rerank="no"
fi
if [[ "$use_docker_rerank" == "yes" ]]; then
existing_vllm_rerank_model="${ORIGINAL_ENV_VALUES[VLLM_RERANK_MODEL]-${ENV_VALUES[VLLM_RERANK_MODEL]:-}}"
existing_vllm_rerank_port="${ORIGINAL_ENV_VALUES[VLLM_RERANK_PORT]-${ENV_VALUES[VLLM_RERANK_PORT]:-}}"
existing_vllm_rerank_host="${ORIGINAL_ENV_VALUES[RERANK_BINDING_HOST]-${ENV_VALUES[RERANK_BINDING_HOST]:-}}"
existing_vllm_rerank_device="${ORIGINAL_ENV_VALUES[VLLM_RERANK_DEVICE]-${ENV_VALUES[VLLM_RERANK_DEVICE]:-}}"
apply_preset_overwrite "${PRESET_VLLM_RERANKER[@]}"
local vllm_rerank_device
vllm_rerank_device="$(resolve_local_device_default "$existing_vllm_rerank_device")"
vllm_rerank_device="$(prompt_choice "Rerank device" "$vllm_rerank_device" "cpu" "cuda")"
if [[ "$vllm_rerank_device" == "cuda" ]] && ! host_cuda_available; then
log_warn "CUDA device selected for vLLM rerank but no NVIDIA driver detected on host."
fi
local rerank_model rerank_port
if [[ -n "$existing_vllm_rerank_port" ]]; then
ENV_VALUES["VLLM_RERANK_PORT"]="$existing_vllm_rerank_port"
fi
if [[ "$previous_rerank_provider" == "vllm" && -n "$existing_vllm_rerank_host" ]]; then
ENV_VALUES["RERANK_BINDING_HOST"]="$existing_vllm_rerank_host"
else
ENV_VALUES["RERANK_BINDING_HOST"]="http://localhost:${ENV_VALUES[VLLM_RERANK_PORT]:-8000}/rerank"
fi
rerank_model="$(prompt_with_default "Rerank model" "${existing_vllm_rerank_model:-${ENV_VALUES[VLLM_RERANK_MODEL]:-BAAI/bge-reranker-v2-m3}}")"
rerank_port="${ENV_VALUES[VLLM_RERANK_PORT]:-8000}"
ENV_VALUES["VLLM_RERANK_MODEL"]="$rerank_model"
ENV_VALUES["RERANK_MODEL"]="$rerank_model"
ENV_VALUES["VLLM_RERANK_PORT"]="$rerank_port"
ENV_VALUES["VLLM_RERANK_DEVICE"]="$vllm_rerank_device"
ENV_VALUES["LIGHTRAG_SETUP_RERANK_PROVIDER"]="vllm"
vllm_rerank_api_key="${ENV_VALUES[VLLM_RERANK_API_KEY]:-${ENV_VALUES[RERANK_BINDING_API_KEY]:-}}"
if [[ -z "$vllm_rerank_api_key" ]]; then
vllm_rerank_api_key="$(openssl rand -hex 16 2>/dev/null || LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32)"
fi
ENV_VALUES["VLLM_RERANK_API_KEY"]="$vllm_rerank_api_key"
ENV_VALUES["RERANK_BINDING_API_KEY"]="$vllm_rerank_api_key"
add_docker_service "vllm-rerank"
set_compose_override "RERANK_BINDING_HOST" \
"http://vllm-rerank:${rerank_port}/rerank"
else
# Reranking enabled but not via Docker — ask provider/host/model/api_key
collect_rerank_config "yes" "no"
fi
else
ENV_VALUES["RERANK_BINDING"]="null"
unset 'ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]'
fi
echo ""
finalize_base_setup
}
finalize_base_setup() {
local backup_path
local compose_file
local existing_compose
local compose_action="write_env_only"
local runtime_target="$DEFAULT_RUNTIME_TARGET"
local show_host_start_hint="no"
local svc_names=""
if [[ ! -f "${REPO_ROOT}/env.example" ]]; then
format_error "env.example is missing in $REPO_ROOT" "Restore env.example before running setup."
return 1
fi
if [[ ! -w "$REPO_ROOT" ]]; then
format_error "No write permission in $REPO_ROOT" "Run the setup from a writable directory."
return 1
fi
if ! validate_sensitive_env_literals; then
return 1
fi
if ! validate_mongo_vector_storage_config \
"${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" \
"${ENV_VALUES[MONGO_URI]:-}" \
"${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then
return 1
fi
show_summary
if ! confirm_required_yes_no "${COLOR_YELLOW}Ready to proceed and write .env${COLOR_RESET}"; then
log_warn "Setup cancelled."
return 1
fi
existing_compose="$(find_generated_compose_file)"
compose_file="${REPO_ROOT}/docker-compose.final.yml"
record_existing_managed_root_services "$existing_compose"
restore_storage_docker_services_from_env
configure_mongodb_compose_migration_rewrite "$existing_compose"
configure_base_compose_rewrites
if ((${#DOCKER_SERVICES[@]} > 0)); then
# LightRAG depends on managed Docker services; it must run via Docker.
svc_names="$(printf '%s ' "${DOCKER_SERVICES[@]}")"
svc_names="${svc_names% }"
echo "LightRAG requires Docker services: ${svc_names}"
if ! confirm_default_yes "${COLOR_YELLOW}The compose file will be created/updated. Continue?${COLOR_RESET}"; then
log_warn "Setup cancelled."
return 1
fi
compose_action="rewrite_compose"
runtime_target="compose"
else
resolve_compose_output_action \
"$existing_compose" \
compose_action \
runtime_target \
show_host_start_hint
fi
if [[ "$compose_action" == "rewrite_compose" ]]; then
backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1
if ! prepare_managed_service_assets_for_compose "$existing_compose"; then
return 1
fi
collect_preserved_storage_service_images "$existing_compose"
prepare_compose_env_overrides
elif [[ "$compose_action" == "delete_compose_and_switch_host" ]]; then
backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1
fi
backup_path="$(backup_env_file)"
if [[ -n "$backup_path" ]]; then
log_success "Backed up existing .env to $backup_path"
fi
clear_deprecated_vllm_dtype_state
set_runtime_target "$runtime_target" || return 1
generate_env_file "${REPO_ROOT}/env.example" "${REPO_ROOT}/.env"
log_success "Wrote .env"
case "$compose_action" in
rewrite_compose)
prepare_compose_output_from_existing "$compose_file" "$existing_compose" || return 1
generate_docker_compose "$compose_file"
log_success "Wrote ${compose_file}"
echo " To start: docker compose -f ${compose_file} up -d"
;;
delete_compose_and_switch_host)
remove_existing_compose_file "$existing_compose" || return 1
echo " To start: lightrag-server"
;;
*)
if [[ "$show_host_start_hint" == "yes" ]]; then
echo " To start: lightrag-server"
fi
;;
esac
}
env_storage_flow() {
local env_file="${REPO_ROOT}/.env"
local db_type
local db_order=("postgresql" "neo4j" "mongodb" "redis" "milvus" "qdrant" "memgraph" "opensearch")
if [[ ! -f "$env_file" ]]; then
format_error "No .env file found." "Run 'make env-base' first to configure LLM and embedding."
return 1
fi
reset_state
load_existing_env_if_present
log_info "Storage configuration wizard"
echo "This wizard only modifies storage backend settings."
echo "LLM, embedding, reranker, server, and security settings are preserved."
echo ""
log_step "Storage backend selection"
select_storage_backends "custom"
log_debug "Storage selections: kv=${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-} vector=${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-} graph=${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-} doc=${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-}"
clear_unused_storage_deployment_markers
log_step "Database configuration"
for db_type in "${db_order[@]}"; do
if [[ -n "${REQUIRED_DB_TYPES[$db_type]+set}" ]]; then
collect_database_config "$db_type" "$(storage_default_docker_for_db_type "$db_type")"
echo ""
fi
done
finalize_storage_setup
}
finalize_storage_setup() {
local backup_path
local compose_file
local existing_compose
local compose_action="write_env_only"
local runtime_target="$DEFAULT_RUNTIME_TARGET"
local show_host_start_hint="no"
if [[ ! -f "${REPO_ROOT}/env.example" ]]; then
format_error "env.example is missing in $REPO_ROOT" "Restore env.example before running setup."
return 1
fi
if [[ ! -w "$REPO_ROOT" ]]; then
format_error "No write permission in $REPO_ROOT" "Run the setup from a writable directory."
return 1
fi
if [[ -n "${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-}" ]]; then
if ! validate_required_variables \
"${ENV_VALUES[LIGHTRAG_KV_STORAGE]}" \
"${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]}" \
"${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]}" \
"${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]}"; then
return 1
fi
fi
if ! validate_mongo_vector_storage_config \
"${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" \
"${ENV_VALUES[MONGO_URI]:-}" \
"${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then
return 1
fi
if ! validate_sensitive_env_literals; then
return 1
fi
show_summary
if ! confirm_required_yes_no "${COLOR_YELLOW}Ready to proceed and write .env${COLOR_RESET}"; then
log_warn "Setup cancelled."
return 1
fi
existing_compose="$(find_generated_compose_file)"
compose_file="${REPO_ROOT}/docker-compose.final.yml"
record_existing_managed_root_services "$existing_compose"
restore_vllm_docker_services_from_env
configure_storage_compose_rewrites
configure_mongodb_compose_migration_rewrite "$existing_compose"
resolve_compose_output_action \
"$existing_compose" \
compose_action \
runtime_target \
show_host_start_hint
if [[ "$compose_action" == "rewrite_compose" ]]; then
backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1
if ! prepare_managed_service_assets_for_compose "$existing_compose"; then
return 1
fi
collect_preserved_storage_service_images "$existing_compose"
prepare_compose_env_overrides
elif [[ "$compose_action" == "delete_compose_and_switch_host" ]]; then
backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1
fi
backup_path="$(backup_env_file)"
if [[ -n "$backup_path" ]]; then
log_success "Backed up existing .env to $backup_path"
fi
clear_deprecated_vllm_dtype_state
set_runtime_target "$runtime_target" || return 1
generate_env_file "${REPO_ROOT}/env.example" "${REPO_ROOT}/.env"
log_success "Wrote .env"
case "$compose_action" in
rewrite_compose)
prepare_compose_output_from_existing "$compose_file" "$existing_compose" || return 1
generate_docker_compose "$compose_file"
log_success "Wrote ${compose_file}"
echo " To start: docker compose -f ${compose_file} up -d"
;;
delete_compose_and_switch_host)
remove_existing_compose_file "$existing_compose" || return 1
echo " To start: lightrag-server"
;;
*)
if [[ "$show_host_start_hint" == "yes" ]]; then
echo " To start: lightrag-server"
fi
;;
esac
}
env_server_flow() {
local env_file="${REPO_ROOT}/.env"
if [[ ! -f "$env_file" ]]; then
format_error "No .env file found." "Run 'make env-base' first to configure LLM and embedding."
return 1
fi
reset_state
load_existing_env_if_present
log_info "Server configuration wizard"
echo "This wizard only modifies server, security, and SSL settings."
echo "LLM, embedding, reranker, and storage settings are preserved."
echo ""
log_step "Server configuration"
collect_server_config
echo ""
log_step "Security configuration"
collect_security_config "no" "no"
warn_if_network_exposed_without_auth
echo ""
log_step "SSL configuration"
collect_ssl_config
echo ""
finalize_server_setup
}
finalize_server_setup() {
local backup_path
local compose_file
local existing_compose
local compose_action="write_env_only"
local runtime_target="$DEFAULT_RUNTIME_TARGET"
local show_host_start_hint="no"
if [[ ! -f "${REPO_ROOT}/env.example" ]]; then
format_error "env.example is missing in $REPO_ROOT" "Restore env.example before running setup."
return 1
fi
if [[ ! -w "$REPO_ROOT" ]]; then
format_error "No write permission in $REPO_ROOT" "Run the setup from a writable directory."
return 1
fi
if ! validate_sensitive_env_literals; then
return 1
fi
if ! validate_auth_accounts_runtime_config \
"${ENV_VALUES[AUTH_ACCOUNTS]:-}"; then
return 1
fi
if ! validate_mongo_vector_storage_config \
"${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}" \
"${ENV_VALUES[MONGO_URI]:-}" \
"${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then
return 1
fi
show_summary
if ! confirm_required_yes_no "${COLOR_YELLOW}Ready to proceed and write .env${COLOR_RESET}"; then
log_warn "Setup cancelled."
return 1
fi
existing_compose="$(find_generated_compose_file)"
compose_file="${REPO_ROOT}/docker-compose.final.yml"
record_existing_managed_root_services "$existing_compose"
restore_storage_docker_services_from_env
restore_vllm_docker_services_from_env
configure_mongodb_compose_migration_rewrite "$existing_compose"
resolve_compose_output_action \
"$existing_compose" \
compose_action \
runtime_target \
show_host_start_hint
if [[ "$compose_action" == "rewrite_compose" ]]; then
backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1
if ! prepare_managed_service_assets_for_compose "$existing_compose"; then
return 1
fi
collect_preserved_storage_service_images "$existing_compose"
prepare_compose_env_overrides
elif [[ "$compose_action" == "delete_compose_and_switch_host" ]]; then
backup_existing_compose_for_action "$compose_action" "$existing_compose" || return 1
if [[ -n "$SSL_CERT_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_CERT_SOURCE_PATH"; then
format_error "Invalid SSL_CERTFILE" \
"Set it to an existing certificate file, disable SSL, or rerun the wizard to choose a new certificate."
return 1
fi
if [[ -n "$SSL_KEY_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_KEY_SOURCE_PATH"; then
format_error "Invalid SSL_KEYFILE" \
"Set it to an existing private key file, disable SSL, or rerun the wizard to choose a new key."
return 1
fi
else
if [[ -n "$SSL_CERT_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_CERT_SOURCE_PATH"; then
format_error "Invalid SSL_CERTFILE" \
"Set it to an existing certificate file, disable SSL, or rerun the wizard to choose a new certificate."
return 1
fi
if [[ -n "$SSL_KEY_SOURCE_PATH" ]] && ! validate_existing_file "$SSL_KEY_SOURCE_PATH"; then
format_error "Invalid SSL_KEYFILE" \
"Set it to an existing private key file, disable SSL, or rerun the wizard to choose a new key."
return 1
fi
fi
backup_path="$(backup_env_file)"
if [[ -n "$backup_path" ]]; then
log_success "Backed up existing .env to $backup_path"
fi
clear_deprecated_vllm_dtype_state
set_runtime_target "$runtime_target" || return 1
generate_env_file "${REPO_ROOT}/env.example" "${REPO_ROOT}/.env"
log_success "Wrote .env"
case "$compose_action" in
rewrite_compose)
prepare_compose_output_from_existing "$compose_file" "$existing_compose" || return 1
generate_docker_compose "$compose_file"
log_success "Wrote ${compose_file}"
log_success "Server port and security settings updated in compose."
echo " To restart: docker compose -f ${compose_file} up -d --force-recreate lightrag"
;;
delete_compose_and_switch_host)
remove_existing_compose_file "$existing_compose" || return 1
echo " To start: lightrag-server"
;;
*)
if [[ "$show_host_start_hint" == "yes" ]]; then
echo " To start: lightrag-server"
fi
;;
esac
}
load_env_file() {
local env_file="$1"
local line key value
if [[ ! -f "$env_file" ]]; then
format_error ".env file not found at $env_file" "Run make env-base to generate it."
return 1
fi
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[A-Za-z0-9_]+= ]]; then
key="${line%%=*}"
value="${line#*=}"
if [[ "$value" =~ ^\".*\"$ ]]; then
value="${value:1:${#value}-2}"
value="${value//\\\$/\$}"
value="${value//\\\"/\"}"
value="${value//\\\\/\\}"
elif [[ "$value" =~ ^\'.*\'$ ]]; then
value="${value:1:${#value}-2}"
fi
ENV_VALUES["$key"]="$value"
fi
done < "$env_file"
}
validate_ssl_runtime_path() {
local path="$1"
local runtime_target="${ENV_VALUES[LIGHTRAG_RUNTIME_TARGET]:-$DEFAULT_RUNTIME_TARGET}"
local staged_path=""
if validate_existing_file "$path"; then
return 0
fi
if [[ "$runtime_target" == "compose" && "$path" == /app/data/certs/* ]]; then
staged_path="${REPO_ROOT}/data/certs/${path#/app/data/certs/}"
validate_existing_file "$staged_path"
return $?
fi
return 1
}
validate_env_file() {
local env_file="${REPO_ROOT}/.env"
local errors=0
local kv vector graph doc_status
local runtime_target
local storage db_type
local -A referenced_db_types=()
reset_state
if ! load_env_file "$env_file"; then
return 1
fi
kv="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-}"
vector="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}"
graph="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-}"
doc_status="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-}"
runtime_target="${ENV_VALUES[LIGHTRAG_RUNTIME_TARGET]:-$DEFAULT_RUNTIME_TARGET}"
for storage in "$kv" "$vector" "$graph" "$doc_status"; do
if [[ -z "$storage" ]]; then
continue
fi
db_type="${STORAGE_DB_TYPES[$storage]:-}"
if [[ -n "$db_type" ]]; then
referenced_db_types["$db_type"]=1
fi
done
if ! validate_runtime_target "$runtime_target"; then
errors=1
fi
if [[ -z "$kv" || -z "$vector" || -z "$graph" || -z "$doc_status" ]]; then
format_error "Storage selections are missing in .env" "Set LIGHTRAG_*_STORAGE variables."
return 1
fi
if ! validate_mongo_vector_storage_config \
"$vector" \
"${ENV_VALUES[MONGO_URI]:-}" \
"${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}"; then
errors=1
fi
if ! validate_required_variables "$kv" "$vector" "$graph" "$doc_status"; then
errors=1
fi
if ! validate_auth_accounts_runtime_config \
"${ENV_VALUES[AUTH_ACCOUNTS]:-}"; then
errors=1
fi
if ! validate_sensitive_env_literals; then
errors=1
fi
if [[ "${ENV_VALUES[SSL]:-false}" == "true" ]]; then
if ! validate_ssl_runtime_path "${ENV_VALUES[SSL_CERTFILE]:-}"; then
format_error "Invalid SSL_CERTFILE" "Set it to an existing certificate file when SSL=true."
errors=1
fi
if ! validate_ssl_runtime_path "${ENV_VALUES[SSL_KEYFILE]:-}"; then
format_error "Invalid SSL_KEYFILE" "Set it to an existing private key file when SSL=true."
errors=1
fi
fi
if [[ -n "${referenced_db_types[neo4j]+set}" ]] && [[ -n "${ENV_VALUES[NEO4J_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[NEO4J_URI]}" neo4j; then
format_error "Invalid NEO4J_URI" "Use neo4j:// or bolt:// format."
errors=1
fi
if [[ -n "${referenced_db_types[mongodb]+set}" ]] && [[ -n "${ENV_VALUES[MONGO_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[MONGO_URI]}" mongodb; then
format_error "Invalid MONGO_URI" "Use mongodb:// or mongodb+srv:// format."
errors=1
fi
if [[ -n "${referenced_db_types[redis]+set}" ]] && [[ -n "${ENV_VALUES[REDIS_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[REDIS_URI]}" redis; then
format_error "Invalid REDIS_URI" "Use redis:// or rediss:// format."
errors=1
fi
if [[ -n "${referenced_db_types[milvus]+set}" ]] && [[ -n "${ENV_VALUES[MILVUS_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[MILVUS_URI]}" milvus; then
format_error "Invalid MILVUS_URI" "Use http://host:port format."
errors=1
fi
if [[ -n "${referenced_db_types[qdrant]+set}" ]] && [[ -n "${ENV_VALUES[QDRANT_URL]:-}" ]] && ! validate_uri "${ENV_VALUES[QDRANT_URL]}" qdrant; then
format_error "Invalid QDRANT_URL" "Use http://host:port format."
errors=1
fi
if [[ -n "${referenced_db_types[memgraph]+set}" ]] && [[ -n "${ENV_VALUES[MEMGRAPH_URI]:-}" ]] && ! validate_uri "${ENV_VALUES[MEMGRAPH_URI]}" memgraph; then
format_error "Invalid MEMGRAPH_URI" "Use bolt://host:port format."
errors=1
fi
if [[ -n "${referenced_db_types[postgresql]+set}" ]] && [[ -n "${ENV_VALUES[POSTGRES_PORT]:-}" ]] && ! validate_port "${ENV_VALUES[POSTGRES_PORT]}"; then
format_error "Invalid POSTGRES_PORT" "Use a port between 1 and 65535."
errors=1
fi
if [[ -n "${referenced_db_types[opensearch]+set}" ]] && [[ -v 'ENV_VALUES[OPENSEARCH_HOSTS]' ]] && [[ -z "${ENV_VALUES[OPENSEARCH_HOSTS]}" ]]; then
format_error "Empty OPENSEARCH_HOSTS" "Set it to host:port (e.g. localhost:9200)."
errors=1
fi
if [[ -n "${referenced_db_types[opensearch]+set}" ]]; then
if ! validate_opensearch_config \
"${ENV_VALUES[LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT]:-}" \
"${ENV_VALUES[OPENSEARCH_HOSTS]:-}" \
"${ENV_VALUES[OPENSEARCH_USER]:-}" \
"${ENV_VALUES[OPENSEARCH_PASSWORD]:-}" \
"${ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]-1}" \
"${ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]-0}"; then
errors=1
fi
fi
if ((errors != 0)); then
return 1
fi
log_success "Validation passed."
}
report_security_issue() {
local message="$1"
local suggestion="${2:-}"
echo "${COLOR_YELLOW:-}Security issue:${COLOR_RESET:-} $message"
if [[ -n "$suggestion" ]]; then
echo " Suggestion: $suggestion"
fi
}
security_check_env_file() {
local env_file="${REPO_ROOT}/.env"
local findings=0
local auth_accounts=""
local token_secret=""
local api_key=""
local whitelist_paths=""
local whitelist_is_set="no"
local effective_whitelist=""
local kv=""
local vector=""
local graph=""
local doc_status=""
local storage=""
local db_type=""
local opensearch_in_use="no"
local key value
local invalid_sensitive_keys=()
local -A referenced_db_types=()
reset_state
if ! load_env_file "$env_file"; then
return 1
fi
auth_accounts="${ENV_VALUES[AUTH_ACCOUNTS]:-}"
token_secret="${ENV_VALUES[TOKEN_SECRET]:-}"
api_key="${ENV_VALUES[LIGHTRAG_API_KEY]:-}"
kv="${ENV_VALUES[LIGHTRAG_KV_STORAGE]:-}"
vector="${ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]:-}"
graph="${ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]:-}"
doc_status="${ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]:-}"
if [[ -n "${ENV_VALUES[WHITELIST_PATHS]+set}" ]]; then
whitelist_paths="${ENV_VALUES[WHITELIST_PATHS]}"
whitelist_is_set="yes"
fi
for storage in "$kv" "$vector" "$graph" "$doc_status"; do
if [[ -z "$storage" ]]; then
continue
fi
if [[ ! -v "STORAGE_DB_TYPES[$storage]" ]]; then
continue
fi
db_type="${STORAGE_DB_TYPES[$storage]}"
if [[ -n "$db_type" ]]; then
referenced_db_types["$db_type"]=1
fi
done
if [[ -n "${referenced_db_types[opensearch]+set}" || "${ENV_VALUES[LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT]:-}" == "docker" ]]; then
opensearch_in_use="yes"
fi
for key in "${!ENV_VALUES[@]}"; do
if ! is_sensitive_env_key "$key"; then
continue
fi
value="${ENV_VALUES[$key]:-}"
if [[ -n "$value" ]] && contains_env_interpolation_syntax "$value"; then
invalid_sensitive_keys+=("$key")
fi
done
if ((${#invalid_sensitive_keys[@]} > 0)); then
report_security_issue \
"Sensitive values still contain \${...} interpolation syntax: ${invalid_sensitive_keys[*]}" \
"Replace them with literal values or inject those secrets at runtime."
findings=$((findings + 1))
fi
if [[ -z "$auth_accounts" && -z "$api_key" ]]; then
local no_auth_message="No API protection is configured."
if ! host_is_loopback "${ENV_VALUES[HOST]:-0.0.0.0}"; then
no_auth_message="No API protection is configured while HOST=${ENV_VALUES[HOST]:-0.0.0.0} is reachable from the network."
fi
report_security_issue \
"$no_auth_message" \
"Set AUTH_ACCOUNTS and TOKEN_SECRET, add LIGHTRAG_API_KEY, or put the service behind a trusted reverse proxy."
findings=$((findings + 1))
fi
if [[ -n "$auth_accounts" ]]; then
if ! validate_auth_accounts_format "$auth_accounts"; then
report_security_issue \
"AUTH_ACCOUNTS is malformed." \
"Use comma-separated user:password pairs such as admin:{bcrypt}<hash> or admin:secret,reader:another-secret."
findings=$((findings + 1))
elif ! validate_auth_accounts_password_safety "$auth_accounts"; then
report_security_issue \
"AUTH_ACCOUNTS uses a predictable password prefix." \
"Passwords must not start with 'admin' or 'pass'. Choose a stronger password or use lightrag-hash-password."
findings=$((findings + 1))
fi
if [[ -z "$token_secret" ]]; then
report_security_issue \
"AUTH_ACCOUNTS is set but TOKEN_SECRET is missing." \
"Set a non-empty JWT signing secret before enabling account-based authentication."
findings=$((findings + 1))
elif [[ "$token_secret" == "lightrag-jwt-default-secret" ]]; then
report_security_issue \
"TOKEN_SECRET still uses the built-in default value." \
"Generate a unique JWT signing secret and update TOKEN_SECRET."
findings=$((findings + 1))
fi
effective_whitelist="$whitelist_paths"
if [[ "$whitelist_is_set" != "yes" ]]; then
effective_whitelist="/health,/api/*"
fi
if whitelist_exposes_api_routes "$effective_whitelist"; then
report_security_issue \
"WHITELIST_PATHS exposes /api routes while AUTH_ACCOUNTS is enabled." \
"Use a minimal whitelist such as /health,/docs and keep /api routes authenticated."
findings=$((findings + 1))
fi
fi
if [[ -z "$auth_accounts" && -n "$api_key" ]]; then
effective_whitelist="$whitelist_paths"
if [[ "$whitelist_is_set" != "yes" ]]; then
effective_whitelist="/health,/api/*"
fi
if whitelist_exposes_api_routes "$effective_whitelist"; then
report_security_issue \
"WHITELIST_PATHS exposes /api routes while LIGHTRAG_API_KEY is the only active auth mechanism." \
"Use a minimal whitelist such as /health,/docs and keep /api routes protected by the API key."
findings=$((findings + 1))
fi
fi
if [[ "$opensearch_in_use" == "yes" ]] && [[ -n "${ENV_VALUES[OPENSEARCH_PASSWORD]:-}" ]]; then
local os_pass="${ENV_VALUES[OPENSEARCH_PASSWORD]}"
if [[ "$os_pass" == "admin" || "$os_pass" == "LightRAG2026_!@" ]]; then
report_security_issue \
"OPENSEARCH_PASSWORD uses a well-known default value." \
"Set a unique, strong password for the OpenSearch admin account."
findings=$((findings + 1))
fi
fi
if ((findings == 0)); then
log_success "No obvious security issues found in ${env_file}."
return 0
fi
log_warn "Security check found ${findings} issue(s) in ${env_file}."
return 1
}
backup_only() {
local backup_path
local compose_backup_path
backup_path="$(backup_env_file)"
if [[ -z "$backup_path" ]]; then
format_error "No .env file found to back up." "Create one with make env-base first."
return 1
fi
echo "Backed up .env to $backup_path"
compose_backup_path="$(backup_compose_file)" || return 1
if [[ -n "$compose_backup_path" ]]; then
echo "Backed up compose file to $compose_backup_path"
fi
}
print_help() {
cat <<'HELP'
Usage: scripts/setup/setup.sh [--base|--storage|--server|--validate|--security-check|--backup] [--rewrite-compose]
Options:
--base Configure LLM, embedding, and reranker (run first)
--storage Configure storage backends and databases (requires .env)
--server Configure server, security, and SSL (requires .env)
--validate Validate an existing .env file
--security-check Audit an existing .env for security risks
--backup Backup the current .env and generated compose file when present
--rewrite-compose Force regeneration of all wizard-managed compose services
--debug Enable debug logging
--help Show this help message
HELP
}
_sigint_handler() {
echo ""
echo "Setup interrupted."
exit 130
}
main() {
trap '_sigint_handler' INT
init_colors
local mode="help"
while [[ $# -gt 0 ]]; do
case "$1" in
--base)
mode="base"
;;
--storage)
mode="storage"
;;
--server)
mode="server"
;;
--validate)
mode="validate"
;;
--security-check)
mode="security-check"
;;
--backup)
mode="backup"
;;
--debug)
DEBUG="true"
;;
--rewrite-compose)
FORCE_REWRITE_COMPOSE="yes"
;;
--help|-h)
mode="help"
;;
*)
echo "Unknown option: $1" >&2
print_help
return 1
;;
esac
shift
done
case "$mode" in
base)
env_base_flow
;;
storage)
env_storage_flow
;;
server)
env_server_flow
;;
validate)
validate_env_file
;;
security-check)
security_check_env_file
;;
backup)
backup_only
;;
*)
print_help
;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi