mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-21 19:02:45 -05:00
1389 lines
54 KiB
Bash
Executable File
1389 lines
54 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# This script must remain compatible with bash 3.2 — macOS still ships
|
||
# 3.2.57 by default and the curl-pipe installer is invoked with /bin/bash.
|
||
# Avoid bash 4+ features (associative arrays, ${var,,}, etc.). Notably,
|
||
# do not re-enable `set -u`: on bash 3.2, `"${arr[@]}"` errors as
|
||
# "unbound variable" when arr is an explicitly-declared empty array.
|
||
set -eo pipefail
|
||
|
||
# Expected resource requirements (overridden below if --lite)
|
||
EXPECTED_DOCKER_RAM_GB=10
|
||
EXPECTED_DISK_GB=32
|
||
|
||
# Parse command line arguments
|
||
SHUTDOWN_MODE=false
|
||
DELETE_DATA_MODE=false
|
||
INCLUDE_CRAFT=false # Disabled by default, use --include-craft to enable
|
||
LITE_MODE=false # Disabled by default, use --lite to enable
|
||
USE_LOCAL_FILES=false # Disabled by default, use --local to skip downloading config files
|
||
NO_PROMPT=false
|
||
DRY_RUN=false
|
||
VERBOSE=false
|
||
NO_WAIT=false # When false (default), pass --wait to `docker compose up`
|
||
WAIT_TIMEOUT_SECONDS=600
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--shutdown)
|
||
SHUTDOWN_MODE=true
|
||
shift
|
||
;;
|
||
--delete-data)
|
||
DELETE_DATA_MODE=true
|
||
shift
|
||
;;
|
||
--include-craft)
|
||
INCLUDE_CRAFT=true
|
||
shift
|
||
;;
|
||
--lite)
|
||
LITE_MODE=true
|
||
shift
|
||
;;
|
||
--local)
|
||
USE_LOCAL_FILES=true
|
||
shift
|
||
;;
|
||
--no-prompt)
|
||
NO_PROMPT=true
|
||
shift
|
||
;;
|
||
--dry-run)
|
||
DRY_RUN=true
|
||
shift
|
||
;;
|
||
--verbose)
|
||
VERBOSE=true
|
||
shift
|
||
;;
|
||
--no-wait)
|
||
NO_WAIT=true
|
||
shift
|
||
;;
|
||
--help|-h)
|
||
echo "Onyx Installation Script"
|
||
echo ""
|
||
echo "Usage: $0 [OPTIONS]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --include-craft Enable Onyx Craft (AI-powered web app building)"
|
||
echo " --lite Deploy Onyx Lite (no Vespa, Redis, or model servers)"
|
||
echo " --local Use existing config files instead of downloading from GitHub"
|
||
echo " --shutdown Stop (pause) Onyx containers"
|
||
echo " --delete-data Remove all Onyx data (containers, volumes, and files)"
|
||
echo " --no-prompt Run non-interactively with defaults (for CI/automation)"
|
||
echo " --dry-run Show what would be done without making changes"
|
||
echo " --verbose Show detailed output for debugging"
|
||
echo " --no-wait Skip 'docker compose up --wait'; return as soon as"
|
||
echo " containers are started (they may still be initializing)"
|
||
echo " --help, -h Show this help message"
|
||
echo ""
|
||
echo "Examples:"
|
||
echo " $0 # Install Onyx"
|
||
echo " $0 --lite # Install Onyx Lite (minimal deployment)"
|
||
echo " $0 --include-craft # Install Onyx with Craft enabled"
|
||
echo " $0 --shutdown # Pause Onyx services"
|
||
echo " $0 --delete-data # Completely remove Onyx and all data"
|
||
echo " $0 --local # Re-run using existing config files on disk"
|
||
echo " $0 --no-prompt # Non-interactive install with defaults"
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1"
|
||
echo "Use --help for usage information"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if [[ "$VERBOSE" = true ]]; then
|
||
set -x
|
||
fi
|
||
|
||
if [[ "$LITE_MODE" = true ]] && [[ "$INCLUDE_CRAFT" = true ]]; then
|
||
echo "ERROR: --lite and --include-craft cannot be used together."
|
||
echo "Craft requires services (Vespa, Redis, background workers) that lite mode disables."
|
||
exit 1
|
||
fi
|
||
|
||
# When --lite is passed as a flag, lower resource thresholds early (before the
|
||
# resource check). When lite is chosen interactively, the thresholds are adjusted
|
||
# after the resource check has already passed with the standard thresholds —
|
||
# which is the safer direction.
|
||
if [[ "$LITE_MODE" = true ]]; then
|
||
EXPECTED_DOCKER_RAM_GB=4
|
||
EXPECTED_DISK_GB=16
|
||
fi
|
||
|
||
INSTALL_ROOT="${INSTALL_PREFIX:-onyx_data}"
|
||
|
||
LITE_COMPOSE_FILE="docker-compose.onyx-lite.yml"
|
||
CRAFT_COMPOSE_FILE="docker-compose.craft.yml"
|
||
|
||
# Populate COMPOSE_FILE_ARGS with the -f flags for docker compose.
|
||
# Pass "true" as $1 to auto-detect previously-downloaded overlays (used by
|
||
# shutdown/delete-data so users don't need to remember --lite/--include-craft).
|
||
COMPOSE_FILE_ARGS=()
|
||
build_compose_file_args() {
|
||
local auto_detect="${1:-false}"
|
||
COMPOSE_FILE_ARGS=(-f docker-compose.yml)
|
||
if [[ "$LITE_MODE" = true ]] || { [[ "$auto_detect" = true ]] && [[ -f "${INSTALL_ROOT}/deployment/${LITE_COMPOSE_FILE}" ]]; }; then
|
||
COMPOSE_FILE_ARGS+=(-f "${LITE_COMPOSE_FILE}")
|
||
fi
|
||
# Craft Docker sandbox backend is opt-in (--include-craft) so the host
|
||
# Docker socket isn't bind-mounted on default deployments. Layer the
|
||
# craft overlay on top when explicitly enabled, or auto-detect a
|
||
# previously-downloaded overlay on shutdown/delete-data.
|
||
if [[ "$INCLUDE_CRAFT" = true ]] || { [[ "$auto_detect" = true ]] && [[ -f "${INSTALL_ROOT}/deployment/${CRAFT_COMPOSE_FILE}" ]]; }; then
|
||
COMPOSE_FILE_ARGS+=(-f "${CRAFT_COMPOSE_FILE}")
|
||
fi
|
||
}
|
||
|
||
# --- Downloader detection (curl with wget fallback) ---
|
||
DOWNLOADER=""
|
||
detect_downloader() {
|
||
if command -v curl &> /dev/null; then
|
||
DOWNLOADER="curl"
|
||
return 0
|
||
fi
|
||
if command -v wget &> /dev/null; then
|
||
DOWNLOADER="wget"
|
||
return 0
|
||
fi
|
||
echo "ERROR: Neither curl nor wget found. Please install one and retry."
|
||
exit 1
|
||
}
|
||
detect_downloader
|
||
|
||
download_file() {
|
||
local url="$1"
|
||
local output="$2"
|
||
if [[ "$DOWNLOADER" == "curl" ]]; then
|
||
curl -fsSL --retry 3 --retry-delay 2 --retry-connrefused -o "$output" "$url"
|
||
else
|
||
wget -q --tries=3 --timeout=20 -O "$output" "$url"
|
||
fi
|
||
}
|
||
|
||
# Fetches the most recent published release tag from the Onyx GitHub repo.
|
||
# Used to default the installer to a pinned, tested release (instead of the
|
||
# rolling "edge" tag) and to pull compose/nginx files that match it.
|
||
LATEST_RELEASE_TAG=""
|
||
fetch_latest_release_tag() {
|
||
local api_url="https://api.github.com/repos/onyx-dot-app/onyx/releases/latest"
|
||
local response=""
|
||
if [[ "$DOWNLOADER" == "curl" ]]; then
|
||
response=$(curl -fsSL --retry 2 --retry-delay 1 \
|
||
--connect-timeout 5 --max-time 10 \
|
||
-H "Accept: application/vnd.github+json" \
|
||
"$api_url" 2>/dev/null) || return 1
|
||
else
|
||
response=$(wget -q --tries=2 --timeout=10 \
|
||
--header="Accept: application/vnd.github+json" \
|
||
-O- "$api_url" 2>/dev/null) || return 1
|
||
fi
|
||
local tag
|
||
tag=$(echo "$response" \
|
||
| grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"[^"]+"' \
|
||
| head -1 \
|
||
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
||
if [[ -z "$tag" ]]; then
|
||
return 1
|
||
fi
|
||
LATEST_RELEASE_TAG="$tag"
|
||
return 0
|
||
}
|
||
|
||
# Craft sandbox backend for an image tag. The docker backend exists only in
|
||
# v4.0.6+; older pinned tags get "kubernetes" (rolling tags track newest).
|
||
sandbox_backend_for_tag() {
|
||
local ver="${1#v}"; ver="${ver%%-*}"
|
||
printf '%s' "$ver" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo docker; return; }
|
||
local major="${ver%%.*}" rest="${ver#*.}"
|
||
local minor="${rest%%.*}" patch="${rest#*.}"
|
||
if [ "$major" -gt 4 ] ||
|
||
{ [ "$major" -eq 4 ] && [ "$minor" -gt 0 ]; } ||
|
||
{ [ "$major" -eq 4 ] && [ "$minor" -eq 0 ] && [ "$patch" -ge 6 ]; }; then
|
||
echo docker
|
||
else
|
||
echo kubernetes
|
||
fi
|
||
}
|
||
|
||
# Write SANDBOX_BACKEND to .env for image tag $1; sets SANDBOX_BACKEND_VALUE.
|
||
set_sandbox_backend_for_tag() {
|
||
SANDBOX_BACKEND_VALUE="$(sandbox_backend_for_tag "$1")"
|
||
if grep -q "^#* *SANDBOX_BACKEND=" "$ENV_FILE"; then
|
||
sed -i.bak "s/^#* *SANDBOX_BACKEND=.*/SANDBOX_BACKEND=$SANDBOX_BACKEND_VALUE/" "$ENV_FILE"
|
||
else
|
||
echo "SANDBOX_BACKEND=$SANDBOX_BACKEND_VALUE" >> "$ENV_FILE"
|
||
fi
|
||
}
|
||
|
||
# --- Docker Compose detection ---
|
||
# Sets COMPOSE_CMD to "docker compose" (plugin) or "docker-compose" (standalone).
|
||
# Returns 0 if either is available, 1 otherwise. Safe to re-run after installing
|
||
# compose mid-script.
|
||
COMPOSE_CMD=""
|
||
detect_compose_cmd() {
|
||
if docker compose version &> /dev/null; then
|
||
COMPOSE_CMD="docker compose"
|
||
return 0
|
||
fi
|
||
if command -v docker-compose &> /dev/null; then
|
||
COMPOSE_CMD="docker-compose"
|
||
return 0
|
||
fi
|
||
COMPOSE_CMD=""
|
||
return 1
|
||
}
|
||
detect_compose_cmd || true
|
||
|
||
# --- Docker daemon access detection ---
|
||
# On Linux, a non-root user can only talk to /var/run/docker.sock when they're
|
||
# in the "docker" group. Group membership doesn't apply to the current shell,
|
||
# so when we add the user to the group mid-script we instead prefix subsequent
|
||
# docker commands with sudo to finish this run. Future runs (after the user
|
||
# logs out and back in) won't need it.
|
||
#
|
||
# Trailing ``env`` anchors the splat with a literal command so bash 3.2's
|
||
# single-pass parser doesn't misclassify the inline ``VAR=val`` prefixes, and
|
||
# so sudo's ``env_reset`` can't strip them (they're positional args to ``env``).
|
||
DOCKER_SUDO=(env)
|
||
refresh_docker_sudo() {
|
||
DOCKER_SUDO=(env)
|
||
if ! command -v docker &> /dev/null; then
|
||
return
|
||
fi
|
||
if [[ "$(id -u)" -eq 0 ]]; then
|
||
return
|
||
fi
|
||
if docker info &> /dev/null; then
|
||
return
|
||
fi
|
||
if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ -n "${WSL_DISTRO_NAME:-}" ]]; then
|
||
DOCKER_SUDO=(sudo env)
|
||
fi
|
||
}
|
||
refresh_docker_sudo
|
||
|
||
# 0 iff DOCKER_SUDO runs commands under sudo. Wrapped so callers don't
|
||
# open-code the [0]-element check (the ``env``-anchor workaround above
|
||
# precludes the cleaner `${#DOCKER_SUDO[@]} > 0` form).
|
||
is_using_sudo() {
|
||
[[ "${DOCKER_SUDO[0]}" == "sudo" ]]
|
||
}
|
||
|
||
# Ensures a required file is present. With --local, verifies the file exists on
|
||
# disk. Otherwise, downloads it from the given URL. Returns 0 on success, 1 on
|
||
# failure (caller should handle the exit).
|
||
ensure_file() {
|
||
local path="$1"
|
||
local url="$2"
|
||
local desc="$3"
|
||
|
||
if [[ "$USE_LOCAL_FILES" = true ]]; then
|
||
if [[ -f "$path" ]]; then
|
||
print_success "Using existing ${desc}"
|
||
return 0
|
||
fi
|
||
print_error "Required file missing: ${desc} (${path})"
|
||
return 1
|
||
fi
|
||
|
||
print_info "Downloading ${desc}..."
|
||
if download_file "$url" "$path" 2>/dev/null; then
|
||
print_success "${desc} downloaded"
|
||
return 0
|
||
fi
|
||
print_error "Failed to download ${desc}"
|
||
print_info "Please ensure you have internet connection and try again"
|
||
return 1
|
||
}
|
||
|
||
# --- Interactive prompt helpers ---
|
||
is_interactive() {
|
||
[[ "$NO_PROMPT" = false ]] && [[ -r /dev/tty ]] && [[ -w /dev/tty ]]
|
||
}
|
||
|
||
read_prompt_line() {
|
||
local prompt_text="$1"
|
||
if ! is_interactive; then
|
||
REPLY=""
|
||
return
|
||
fi
|
||
[[ -n "$prompt_text" ]] && printf "%s" "$prompt_text" > /dev/tty
|
||
IFS= read -r REPLY < /dev/tty || REPLY=""
|
||
}
|
||
|
||
read_prompt_char() {
|
||
local prompt_text="$1"
|
||
if ! is_interactive; then
|
||
REPLY=""
|
||
return
|
||
fi
|
||
[[ -n "$prompt_text" ]] && printf "%s" "$prompt_text" > /dev/tty
|
||
IFS= read -r -n 1 REPLY < /dev/tty || REPLY=""
|
||
printf "\n" > /dev/tty
|
||
}
|
||
|
||
prompt_or_default() {
|
||
local prompt_text="$1"
|
||
local default_value="$2"
|
||
read_prompt_line "$prompt_text"
|
||
[[ -z "$REPLY" ]] && REPLY="$default_value"
|
||
return 0
|
||
}
|
||
|
||
prompt_yn_or_default() {
|
||
local prompt_text="$1"
|
||
local default_value="$2"
|
||
read_prompt_char "$prompt_text"
|
||
[[ -z "$REPLY" ]] && REPLY="$default_value"
|
||
return 0
|
||
}
|
||
|
||
confirm_action() {
|
||
local description="$1"
|
||
prompt_yn_or_default "Install ${description}? (Y/n) [default: Y] " "Y"
|
||
if [[ "$REPLY" =~ ^[Nn] ]]; then
|
||
print_warning "Skipping: ${description}"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Step counter variables
|
||
CURRENT_STEP=0
|
||
TOTAL_STEPS=9
|
||
|
||
# Print colored output
|
||
print_success() {
|
||
echo -e "${GREEN}✓${NC} $1"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "${RED}✗${NC} $1"
|
||
}
|
||
|
||
print_info() {
|
||
echo -e "${YELLOW}ℹ${NC} $1"
|
||
}
|
||
|
||
print_step() {
|
||
CURRENT_STEP=$((CURRENT_STEP + 1))
|
||
echo ""
|
||
echo -e "${BLUE}${BOLD}=== $1 - Step ${CURRENT_STEP}/${TOTAL_STEPS} ===${NC}"
|
||
echo ""
|
||
}
|
||
|
||
print_warning() {
|
||
echo -e "${YELLOW}⚠${NC} $1"
|
||
}
|
||
|
||
# Handle shutdown mode
|
||
if [ "$SHUTDOWN_MODE" = true ]; then
|
||
echo ""
|
||
echo -e "${BLUE}${BOLD}=== Shutting down Onyx ===${NC}"
|
||
echo ""
|
||
|
||
if [ -d "${INSTALL_ROOT}/deployment" ]; then
|
||
print_info "Stopping Onyx containers..."
|
||
|
||
# Check if docker-compose.yml exists
|
||
if [ -f "${INSTALL_ROOT}/deployment/docker-compose.yml" ]; then
|
||
if [[ -z "$COMPOSE_CMD" ]]; then
|
||
print_error "Docker Compose not found. Cannot stop containers."
|
||
exit 1
|
||
fi
|
||
|
||
# Stop containers (without removing them)
|
||
build_compose_file_args true
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
if (cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} HOST_PORT="${HOST_PORT:-3000}" IMAGE_TAG="${IMAGE_TAG:-edge}" $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" stop); then
|
||
print_success "Onyx containers stopped (paused)"
|
||
else
|
||
print_error "Failed to stop containers"
|
||
exit 1
|
||
fi
|
||
else
|
||
print_warning "docker-compose.yml not found in ${INSTALL_ROOT}/deployment"
|
||
fi
|
||
else
|
||
print_warning "Onyx data directory not found. Nothing to shutdown."
|
||
fi
|
||
|
||
echo ""
|
||
print_success "Onyx shutdown complete!"
|
||
exit 0
|
||
fi
|
||
|
||
# Handle delete data mode
|
||
if [ "$DELETE_DATA_MODE" = true ]; then
|
||
echo ""
|
||
echo -e "${RED}${BOLD}=== WARNING: This will permanently delete all Onyx data ===${NC}"
|
||
echo ""
|
||
print_warning "This action will remove:"
|
||
echo " • All Onyx containers and volumes"
|
||
echo " • All downloaded files and configurations"
|
||
echo " • All user data and documents"
|
||
echo ""
|
||
if is_interactive; then
|
||
prompt_or_default "Are you sure you want to continue? Type 'DELETE' to confirm: " ""
|
||
echo "" > /dev/tty
|
||
if [ "$REPLY" != "DELETE" ]; then
|
||
print_info "Operation cancelled."
|
||
exit 0
|
||
fi
|
||
else
|
||
print_error "Cannot confirm destructive operation in non-interactive mode."
|
||
print_info "Run interactively or remove the ${INSTALL_ROOT} directory manually."
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Removing Onyx containers and volumes..."
|
||
|
||
if [ -d "${INSTALL_ROOT}/deployment" ]; then
|
||
# Check if docker-compose.yml exists
|
||
if [ -f "${INSTALL_ROOT}/deployment/docker-compose.yml" ]; then
|
||
if [[ -z "$COMPOSE_CMD" ]]; then
|
||
print_error "Docker Compose not found. Cannot remove containers."
|
||
exit 1
|
||
fi
|
||
|
||
# Stop and remove containers with volumes
|
||
build_compose_file_args true
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
if (cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} HOST_PORT="${HOST_PORT:-3000}" IMAGE_TAG="${IMAGE_TAG:-edge}" $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" down -v); then
|
||
print_success "Onyx containers and volumes removed"
|
||
else
|
||
print_error "Failed to remove containers and volumes"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
print_info "Removing data directories..."
|
||
if [ -d "${INSTALL_ROOT}" ]; then
|
||
rm -rf "${INSTALL_ROOT}"
|
||
print_success "Data directories removed"
|
||
else
|
||
print_warning "No ${INSTALL_ROOT} directory found"
|
||
fi
|
||
|
||
echo ""
|
||
print_success "All Onyx data has been permanently deleted!"
|
||
exit 0
|
||
fi
|
||
|
||
# --- Auto-install Docker (Linux only) ---
|
||
# Runs before the banner so a group-based re-exec doesn't repeat it.
|
||
install_docker_linux() {
|
||
local distro_id=""
|
||
if [[ -f /etc/os-release ]]; then
|
||
# shellcheck source=/dev/null
|
||
distro_id="$(. /etc/os-release && echo "${ID:-}")"
|
||
fi
|
||
|
||
case "$distro_id" in
|
||
amzn)
|
||
print_info "Detected Amazon Linux — installing Docker via package manager..."
|
||
if command -v dnf &> /dev/null; then
|
||
sudo dnf install -y docker
|
||
else
|
||
sudo yum install -y docker
|
||
fi
|
||
;;
|
||
*)
|
||
print_info "Installing Docker via get.docker.com..."
|
||
download_file "https://get.docker.com" /tmp/get-docker.sh
|
||
sudo sh /tmp/get-docker.sh
|
||
rm -f /tmp/get-docker.sh
|
||
;;
|
||
esac
|
||
|
||
sudo systemctl start docker 2>/dev/null || sudo service docker start 2>/dev/null || true
|
||
sudo systemctl enable docker 2>/dev/null || true
|
||
}
|
||
|
||
# Detect OS (including WSL)
|
||
IS_WSL=false
|
||
if [[ -n "${WSL_DISTRO_NAME:-}" ]] || grep -qi microsoft /proc/version 2>/dev/null; then
|
||
IS_WSL=true
|
||
fi
|
||
|
||
# Resolve the git ref to pull deployment files from, and the default image tag
|
||
# to suggest at the version prompt. Both default to the most recent GitHub
|
||
# release so users land on a pinned, tested version. If the API is unreachable,
|
||
# fall back to main / edge (the pre-existing defaults).
|
||
RELEASE_REF="main"
|
||
DEFAULT_IMAGE_TAG="edge"
|
||
RELEASE_LOOKUP_FAILED=false
|
||
if [[ "$USE_LOCAL_FILES" = false ]]; then
|
||
if fetch_latest_release_tag; then
|
||
RELEASE_REF="$LATEST_RELEASE_TAG"
|
||
DEFAULT_IMAGE_TAG="$LATEST_RELEASE_TAG"
|
||
else
|
||
RELEASE_LOOKUP_FAILED=true
|
||
fi
|
||
fi
|
||
|
||
# Dry-run: show plan and exit
|
||
if [[ "$DRY_RUN" = true ]]; then
|
||
print_info "Dry run mode — showing what would happen:"
|
||
echo " • Install root: ${INSTALL_ROOT}"
|
||
echo " • Lite mode: ${LITE_MODE}"
|
||
echo " • Include Craft: ${INCLUDE_CRAFT}"
|
||
echo " • OS type: ${OSTYPE:-unknown} (WSL: ${IS_WSL})"
|
||
echo " • Downloader: ${DOWNLOADER}"
|
||
echo " • Default image tag: ${DEFAULT_IMAGE_TAG} (config ref: ${RELEASE_REF})"
|
||
echo ""
|
||
print_success "Dry run complete (no changes made)"
|
||
exit 0
|
||
fi
|
||
|
||
if ! command -v docker &> /dev/null; then
|
||
if [[ "$OSTYPE" == "linux-gnu"* ]] || [[ -n "${WSL_DISTRO_NAME:-}" ]]; then
|
||
print_info "Docker is required but not installed."
|
||
if ! confirm_action "Docker Engine"; then
|
||
print_error "Docker is required to run Onyx."
|
||
exit 1
|
||
fi
|
||
install_docker_linux
|
||
if ! command -v docker &> /dev/null; then
|
||
print_error "Docker installation failed."
|
||
echo " Visit: https://docs.docker.com/get-docker/"
|
||
exit 1
|
||
fi
|
||
print_success "Docker installed successfully"
|
||
# Compose plugin may ship with Docker — re-detect.
|
||
detect_compose_cmd || true
|
||
fi
|
||
fi
|
||
|
||
# --- Auto-install Docker Compose plugin (Linux only) ---
|
||
if command -v docker &> /dev/null \
|
||
&& [[ -z "$COMPOSE_CMD" ]] \
|
||
&& { [[ "$OSTYPE" == "linux-gnu"* ]] || [[ -n "${WSL_DISTRO_NAME:-}" ]]; }; then
|
||
|
||
print_info "Docker Compose is required but not installed."
|
||
if ! confirm_action "Docker Compose plugin"; then
|
||
print_error "Docker Compose is required to run Onyx."
|
||
exit 1
|
||
fi
|
||
COMPOSE_ARCH="$(uname -m)"
|
||
COMPOSE_URL="https://github.com/docker/compose/releases/latest/download/docker-compose-linux-${COMPOSE_ARCH}"
|
||
COMPOSE_DIR="/usr/local/lib/docker/cli-plugins"
|
||
COMPOSE_TMP="$(mktemp)"
|
||
sudo mkdir -p "$COMPOSE_DIR"
|
||
if download_file "$COMPOSE_URL" "$COMPOSE_TMP"; then
|
||
sudo mv "$COMPOSE_TMP" "$COMPOSE_DIR/docker-compose"
|
||
sudo chmod +x "$COMPOSE_DIR/docker-compose"
|
||
if detect_compose_cmd && [[ "$COMPOSE_CMD" == "docker compose" ]]; then
|
||
print_success "Docker Compose plugin installed"
|
||
else
|
||
print_error "Docker Compose plugin installed but not detected."
|
||
echo " Visit: https://docs.docker.com/compose/install/"
|
||
exit 1
|
||
fi
|
||
else
|
||
rm -f "$COMPOSE_TMP"
|
||
print_error "Failed to download Docker Compose plugin."
|
||
echo " Visit: https://docs.docker.com/compose/install/"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# On Linux, ensure docker commands can talk to the daemon. If `docker info`
|
||
# fails, fall back to sudo for the rest of this run. Add the user to the
|
||
# "docker" group only when they aren't already a member — `docker info` also
|
||
# fails when the daemon is stopped, and we don't want to silently mutate
|
||
# group membership in that case. The downstream daemon check will exit with
|
||
# a clearer message if the daemon really is down.
|
||
refresh_docker_sudo
|
||
if is_using_sudo; then
|
||
if ! id -nG "$USER" | grep -qw docker; then
|
||
if ! getent group docker &> /dev/null; then
|
||
sudo groupadd docker
|
||
fi
|
||
print_info "Adding $USER to the docker group (effective on next login)..."
|
||
sudo usermod -aG docker "$USER"
|
||
fi
|
||
print_info "Using sudo for docker commands in this run."
|
||
fi
|
||
|
||
# ASCII Art Banner
|
||
echo ""
|
||
echo -e "${BLUE}${BOLD}"
|
||
echo " ____ "
|
||
echo " / __ \ "
|
||
echo "| | | |_ __ _ ___ __ "
|
||
echo "| | | | '_ \| | | \ \/ / "
|
||
echo "| |__| | | | | |_| |> < "
|
||
echo " \____/|_| |_|\__, /_/\_\ "
|
||
echo " __/ | "
|
||
echo " |___/ "
|
||
echo -e "${NC}"
|
||
echo "Welcome to Onyx Installation Script"
|
||
echo "===================================="
|
||
echo ""
|
||
|
||
if [[ "$RELEASE_LOOKUP_FAILED" = true ]]; then
|
||
print_warning "Could not determine latest Onyx release — falling back to main / edge"
|
||
fi
|
||
|
||
# User acknowledgment section
|
||
echo -e "${YELLOW}${BOLD}This script will:${NC}"
|
||
echo "1. Download deployment files for Onyx into a new '${INSTALL_ROOT}' directory"
|
||
echo "2. Check your system resources (Docker, memory, disk space)"
|
||
echo "3. Guide you through deployment options (version, authentication)"
|
||
echo ""
|
||
|
||
if is_interactive; then
|
||
echo -e "${YELLOW}${BOLD}Please acknowledge and press Enter to continue...${NC}"
|
||
read_prompt_line ""
|
||
echo ""
|
||
else
|
||
echo -e "${YELLOW}${BOLD}Running in non-interactive mode - proceeding automatically...${NC}"
|
||
echo ""
|
||
fi
|
||
|
||
# GitHub repo base URL — pinned to the resolved release ref (latest release tag
|
||
# if reachable, otherwise main).
|
||
GITHUB_RAW_URL="https://raw.githubusercontent.com/onyx-dot-app/onyx/${RELEASE_REF}/deployment/docker_compose"
|
||
|
||
# Check system requirements
|
||
print_step "Verifying Docker installation"
|
||
|
||
# Check Docker
|
||
if ! command -v docker &> /dev/null; then
|
||
print_error "Docker is not installed. Please install Docker first."
|
||
echo "Visit: https://docs.docker.com/get-docker/"
|
||
exit 1
|
||
fi
|
||
DOCKER_VERSION=$(docker --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||
print_success "Docker $DOCKER_VERSION is installed"
|
||
|
||
# Check Docker Compose
|
||
if [[ -z "$COMPOSE_CMD" ]]; then
|
||
print_error "Docker Compose is not installed. Please install Docker Compose first."
|
||
echo "Visit: https://docs.docker.com/compose/install/"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ "$COMPOSE_CMD" == "docker compose" ]]; then
|
||
COMPOSE_VERSION=$($COMPOSE_CMD version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||
COMPOSE_TYPE="plugin"
|
||
else
|
||
COMPOSE_VERSION=$($COMPOSE_CMD --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||
COMPOSE_TYPE="standalone"
|
||
fi
|
||
if [ -z "$COMPOSE_VERSION" ]; then
|
||
# Handle non-standard versions like "dev" - assume recent enough
|
||
COMPOSE_VERSION="dev"
|
||
print_success "Docker Compose (dev build) is installed (${COMPOSE_TYPE})"
|
||
else
|
||
print_success "Docker Compose $COMPOSE_VERSION is installed (${COMPOSE_TYPE})"
|
||
fi
|
||
|
||
# Returns 0 if $1 <= $2, 1 if $1 > $2
|
||
# Handles missing or non-numeric parts gracefully (treats them as 0)
|
||
version_compare() {
|
||
local version1="${1:-0.0.0}"
|
||
local version2="${2:-0.0.0}"
|
||
|
||
local v1_major v1_minor v1_patch v2_major v2_minor v2_patch
|
||
v1_major=$(echo "$version1" | cut -d. -f1)
|
||
v1_minor=$(echo "$version1" | cut -d. -f2)
|
||
v1_patch=$(echo "$version1" | cut -d. -f3)
|
||
v2_major=$(echo "$version2" | cut -d. -f1)
|
||
v2_minor=$(echo "$version2" | cut -d. -f2)
|
||
v2_patch=$(echo "$version2" | cut -d. -f3)
|
||
|
||
# Default non-numeric or empty parts to 0
|
||
[[ "$v1_major" =~ ^[0-9]+$ ]] || v1_major=0
|
||
[[ "$v1_minor" =~ ^[0-9]+$ ]] || v1_minor=0
|
||
[[ "$v1_patch" =~ ^[0-9]+$ ]] || v1_patch=0
|
||
[[ "$v2_major" =~ ^[0-9]+$ ]] || v2_major=0
|
||
[[ "$v2_minor" =~ ^[0-9]+$ ]] || v2_minor=0
|
||
[[ "$v2_patch" =~ ^[0-9]+$ ]] || v2_patch=0
|
||
|
||
if [ "$v1_major" -lt "$v2_major" ]; then return 0
|
||
elif [ "$v1_major" -gt "$v2_major" ]; then return 1; fi
|
||
|
||
if [ "$v1_minor" -lt "$v2_minor" ]; then return 0
|
||
elif [ "$v1_minor" -gt "$v2_minor" ]; then return 1; fi
|
||
|
||
[ "$v1_patch" -le "$v2_patch" ]
|
||
}
|
||
|
||
# Check Docker daemon
|
||
if ! ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} docker info &> /dev/null; then
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
print_info "Docker daemon is not running. Starting Docker Desktop..."
|
||
open -a Docker
|
||
# Wait up to 120 seconds for Docker to be ready
|
||
DOCKER_WAIT=0
|
||
DOCKER_MAX_WAIT=120
|
||
while ! docker info &> /dev/null; do
|
||
if [ $DOCKER_WAIT -ge $DOCKER_MAX_WAIT ]; then
|
||
print_error "Docker Desktop did not start within ${DOCKER_MAX_WAIT} seconds."
|
||
print_info "Please start Docker Desktop manually and re-run this script."
|
||
exit 1
|
||
fi
|
||
printf "\r\033[KWaiting for Docker Desktop to start... (%ds)" "$DOCKER_WAIT"
|
||
sleep 2
|
||
DOCKER_WAIT=$((DOCKER_WAIT + 2))
|
||
done
|
||
echo ""
|
||
print_success "Docker Desktop is now running"
|
||
else
|
||
print_error "Docker daemon is not running. Please start Docker."
|
||
exit 1
|
||
fi
|
||
else
|
||
print_success "Docker daemon is running"
|
||
fi
|
||
|
||
# Check Docker resources
|
||
print_step "Verifying Docker resources"
|
||
|
||
# Try to get memory allocation (method varies by platform)
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
# macOS - Docker Desktop
|
||
if command -v jq &> /dev/null && [ -f ~/Library/Group\ Containers/group.com.docker/settings.json ]; then
|
||
MEMORY_MB=$(cat ~/Library/Group\ Containers/group.com.docker/settings.json 2>/dev/null | jq '.memoryMiB // 0' 2>/dev/null || echo "0")
|
||
else
|
||
# Try to get from docker system info
|
||
MEMORY_BYTES=$(docker system info 2>/dev/null | grep -i "total memory" | grep -oE '[0-9]+\.[0-9]+' | head -1)
|
||
if [ -n "$MEMORY_BYTES" ]; then
|
||
# Convert from GiB to MB (multiply by 1024)
|
||
MEMORY_MB=$(echo "$MEMORY_BYTES * 1024" | bc 2>/dev/null | cut -d. -f1)
|
||
if [ -z "$MEMORY_MB" ]; then
|
||
MEMORY_MB="0"
|
||
fi
|
||
else
|
||
MEMORY_MB="0"
|
||
fi
|
||
fi
|
||
else
|
||
# Linux - Native Docker
|
||
MEMORY_KB=$(grep MemTotal /proc/meminfo | grep -oE '[0-9]+' || echo "0")
|
||
MEMORY_MB=$((MEMORY_KB / 1024))
|
||
fi
|
||
|
||
# Convert to GB for display
|
||
if [ "$MEMORY_MB" -gt 0 ]; then
|
||
MEMORY_GB=$(awk "BEGIN {printf \"%.1f\", $MEMORY_MB / 1024}")
|
||
if [ "$(awk "BEGIN {print ($MEMORY_MB >= 1024)}")" = "1" ]; then
|
||
MEMORY_DISPLAY="~${MEMORY_GB}GB"
|
||
else
|
||
MEMORY_DISPLAY="${MEMORY_MB}MB"
|
||
fi
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
print_info "Docker memory allocation: ${MEMORY_DISPLAY}"
|
||
else
|
||
print_info "System memory: ${MEMORY_DISPLAY} (Docker uses host memory directly)"
|
||
fi
|
||
else
|
||
print_warning "Could not determine memory allocation"
|
||
MEMORY_DISPLAY="unknown"
|
||
MEMORY_MB=0
|
||
fi
|
||
|
||
# Check disk space (different commands for macOS vs Linux)
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
# macOS uses -g for GB
|
||
DISK_AVAILABLE=$(df -g . | awk 'NR==2 {print $4}')
|
||
else
|
||
# Linux uses -BG for GB
|
||
DISK_AVAILABLE=$(df -BG . | awk 'NR==2 {print $4}' | sed 's/G//')
|
||
fi
|
||
print_info "Available disk space: ${DISK_AVAILABLE}GB"
|
||
|
||
# Resource requirements check
|
||
RESOURCE_WARNING=false
|
||
EXPECTED_RAM_MB=$((EXPECTED_DOCKER_RAM_GB * 1024))
|
||
|
||
if [ "$MEMORY_MB" -gt 0 ] && [ "$MEMORY_MB" -lt "$EXPECTED_RAM_MB" ]; then
|
||
print_warning "Less than ${EXPECTED_DOCKER_RAM_GB}GB RAM available (found: ${MEMORY_DISPLAY})"
|
||
RESOURCE_WARNING=true
|
||
fi
|
||
|
||
if [ "$DISK_AVAILABLE" -lt "$EXPECTED_DISK_GB" ]; then
|
||
print_warning "Less than ${EXPECTED_DISK_GB}GB disk space available (found: ${DISK_AVAILABLE}GB)"
|
||
RESOURCE_WARNING=true
|
||
fi
|
||
|
||
if [ "$RESOURCE_WARNING" = true ]; then
|
||
echo ""
|
||
print_warning "Onyx recommends at least ${EXPECTED_DOCKER_RAM_GB}GB RAM and ${EXPECTED_DISK_GB}GB disk space for optimal performance in standard mode."
|
||
print_warning "Lite mode requires less resources (1-4GB RAM, 8-16GB disk depending on usage), but does not include a vector database."
|
||
echo ""
|
||
prompt_yn_or_default "Do you want to continue anyway? (Y/n): " "y"
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
print_info "Installation cancelled. Please allocate more resources and try again."
|
||
exit 1
|
||
fi
|
||
print_info "Proceeding with installation despite resource limitations..."
|
||
fi
|
||
|
||
# Create directory structure
|
||
print_step "Creating directory structure"
|
||
if [ -d "${INSTALL_ROOT}" ]; then
|
||
print_info "Directory structure already exists"
|
||
print_success "Using existing ${INSTALL_ROOT} directory"
|
||
fi
|
||
mkdir -p "${INSTALL_ROOT}/deployment"
|
||
mkdir -p "${INSTALL_ROOT}/data/nginx/local"
|
||
print_success "Directory structure created"
|
||
|
||
# Ensure all required configuration files are present
|
||
NGINX_BASE_URL="https://raw.githubusercontent.com/onyx-dot-app/onyx/${RELEASE_REF}/deployment/data/nginx"
|
||
|
||
if [[ "$USE_LOCAL_FILES" = true ]]; then
|
||
print_step "Verifying existing configuration files"
|
||
else
|
||
print_step "Downloading Onyx configuration files"
|
||
print_info "This step downloads all necessary configuration files from GitHub..."
|
||
fi
|
||
|
||
ensure_file "${INSTALL_ROOT}/deployment/docker-compose.yml" \
|
||
"${GITHUB_RAW_URL}/docker-compose.yml" "docker-compose.yml" || exit 1
|
||
|
||
# Check Docker Compose version compatibility after obtaining docker-compose.yml
|
||
if [ "$COMPOSE_VERSION" != "dev" ] && version_compare "$COMPOSE_VERSION" "2.24.0"; then
|
||
print_warning "Docker Compose version $COMPOSE_VERSION is older than 2.24.0"
|
||
echo ""
|
||
print_warning "The docker-compose.yml file uses the newer env_file format that requires Docker Compose 2.24.0 or later."
|
||
echo ""
|
||
print_info "To use this configuration with your current Docker Compose version, you have two options:"
|
||
echo ""
|
||
echo "1. Upgrade Docker Compose to version 2.24.0 or later (recommended)"
|
||
echo " Visit: https://docs.docker.com/compose/install/"
|
||
echo ""
|
||
echo "2. Manually replace all env_file sections in docker-compose.yml"
|
||
echo " Change from:"
|
||
echo " env_file:"
|
||
echo " - path: .env"
|
||
echo " required: false"
|
||
echo " To:"
|
||
echo " env_file: .env"
|
||
echo ""
|
||
print_warning "The installation will continue, but may fail if Docker Compose cannot parse the file."
|
||
echo ""
|
||
prompt_yn_or_default "Do you want to continue anyway? (Y/n): " "y"
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
print_info "Installation cancelled. Please upgrade Docker Compose or manually edit the docker-compose.yml file."
|
||
exit 1
|
||
fi
|
||
print_info "Proceeding with installation despite Docker Compose version compatibility issues..."
|
||
fi
|
||
|
||
# Ask for deployment mode (standard vs lite) unless already set via --lite flag
|
||
if [[ "$LITE_MODE" = false ]]; then
|
||
print_info "Which deployment mode would you like?"
|
||
echo ""
|
||
echo " 1) Lite - Minimal deployment (no OpenSearch, Redis, or model servers)"
|
||
echo " LLM chat, tools, file uploads, and Projects still work"
|
||
echo " 2) Standard - Full deployment with search, connectors, and RAG"
|
||
echo ""
|
||
prompt_or_default "Choose a mode (1 or 2) [default: 1]: " "1"
|
||
echo ""
|
||
|
||
case "$REPLY" in
|
||
2)
|
||
print_info "Selected: Standard mode"
|
||
;;
|
||
*)
|
||
LITE_MODE=true
|
||
print_info "Selected: Lite mode"
|
||
;;
|
||
esac
|
||
else
|
||
print_info "Deployment mode: Lite (set via --lite flag)"
|
||
fi
|
||
|
||
if [[ "$LITE_MODE" = true ]] && [[ "$INCLUDE_CRAFT" = true ]]; then
|
||
print_error "--include-craft cannot be used with Lite mode."
|
||
print_info "Craft requires services (Vespa, Redis, background workers) that lite mode disables."
|
||
exit 1
|
||
fi
|
||
|
||
if [[ "$LITE_MODE" = true ]]; then
|
||
EXPECTED_DOCKER_RAM_GB=4
|
||
EXPECTED_DISK_GB=16
|
||
fi
|
||
|
||
# Handle lite overlay file based on selected mode
|
||
if [[ "$LITE_MODE" = true ]]; then
|
||
ensure_file "${INSTALL_ROOT}/deployment/${LITE_COMPOSE_FILE}" \
|
||
"${GITHUB_RAW_URL}/${LITE_COMPOSE_FILE}" "${LITE_COMPOSE_FILE}" || exit 1
|
||
elif [[ -f "${INSTALL_ROOT}/deployment/${LITE_COMPOSE_FILE}" ]]; then
|
||
rm -f "${INSTALL_ROOT}/deployment/${LITE_COMPOSE_FILE}"
|
||
print_info "Removed previous lite overlay (switching to standard mode)"
|
||
fi
|
||
|
||
# Handle craft overlay file based on selected mode. Keeps the Docker socket
|
||
# bind-mount out of the default file so non-Craft deployments never inherit
|
||
# it; only `--include-craft` (or an existing overlay from a prior install)
|
||
# adds it back.
|
||
if [[ "$INCLUDE_CRAFT" = true ]]; then
|
||
ensure_file "${INSTALL_ROOT}/deployment/${CRAFT_COMPOSE_FILE}" \
|
||
"${GITHUB_RAW_URL}/${CRAFT_COMPOSE_FILE}" "${CRAFT_COMPOSE_FILE}" || exit 1
|
||
elif [[ -f "${INSTALL_ROOT}/deployment/${CRAFT_COMPOSE_FILE}" ]]; then
|
||
rm -f "${INSTALL_ROOT}/deployment/${CRAFT_COMPOSE_FILE}"
|
||
print_info "Removed previous craft overlay (Craft disabled this run)"
|
||
fi
|
||
|
||
ensure_file "${INSTALL_ROOT}/deployment/env.template" \
|
||
"${GITHUB_RAW_URL}/env.template" "env.template" || exit 1
|
||
|
||
ensure_file "${INSTALL_ROOT}/data/nginx/app.conf.template" \
|
||
"$NGINX_BASE_URL/app.conf.template" "nginx/app.conf.template" || exit 1
|
||
|
||
ensure_file "${INSTALL_ROOT}/data/nginx/run-nginx.sh" \
|
||
"$NGINX_BASE_URL/run-nginx.sh" "nginx/run-nginx.sh" || exit 1
|
||
chmod +x "${INSTALL_ROOT}/data/nginx/run-nginx.sh"
|
||
|
||
ensure_file "${INSTALL_ROOT}/README.md" \
|
||
"${GITHUB_RAW_URL}/README.md" "README.md" || exit 1
|
||
|
||
touch "${INSTALL_ROOT}/data/nginx/local/.gitkeep"
|
||
print_success "All configuration files ready"
|
||
|
||
# Set up deployment configuration
|
||
print_step "Setting up deployment configs"
|
||
ENV_FILE="${INSTALL_ROOT}/deployment/.env"
|
||
ENV_TEMPLATE="${INSTALL_ROOT}/deployment/env.template"
|
||
# Check if services are already running
|
||
if [ -d "${INSTALL_ROOT}/deployment" ] && [ -f "${INSTALL_ROOT}/deployment/docker-compose.yml" ]; then
|
||
build_compose_file_args true
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
RUNNING_CONTAINERS=$(cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" ps -q 2>/dev/null | wc -l)
|
||
if [ "$RUNNING_CONTAINERS" -gt 0 ]; then
|
||
print_error "Onyx services are currently running!"
|
||
echo ""
|
||
print_info "To make configuration changes, you must first shut down the services."
|
||
echo ""
|
||
print_info "Please run the following command to shut down Onyx:"
|
||
echo -e " ${BOLD}./install.sh --shutdown${NC}"
|
||
echo ""
|
||
print_info "Then run this script again to make your changes."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [ -f "$ENV_FILE" ]; then
|
||
print_info "Existing .env file found. What would you like to do?"
|
||
echo ""
|
||
echo "• Press Enter to restart with current configuration"
|
||
echo "• Type 'update' to update to a newer version"
|
||
echo ""
|
||
prompt_or_default "Choose an option [default: restart]: " ""
|
||
echo ""
|
||
|
||
if [ "$REPLY" = "update" ]; then
|
||
# Craft uses the regular backend image (ENABLE_CRAFT is a runtime flag),
|
||
# so the tag is prompted for normally even with --include-craft.
|
||
print_info "Update selected. Which tag would you like to deploy?"
|
||
echo ""
|
||
echo "• Press Enter for ${DEFAULT_IMAGE_TAG} (recommended)"
|
||
echo "• Type a specific tag (e.g., v0.1.0)"
|
||
echo ""
|
||
prompt_or_default "Enter tag [default: ${DEFAULT_IMAGE_TAG}]: " "${DEFAULT_IMAGE_TAG}"
|
||
VERSION="$REPLY"
|
||
echo ""
|
||
|
||
if [ "$VERSION" = "edge" ]; then
|
||
print_info "Selected: edge (latest nightly)"
|
||
else
|
||
print_info "Selected: $VERSION"
|
||
fi
|
||
|
||
# Update .env file with new version
|
||
print_info "Updating configuration for version $VERSION..."
|
||
if grep -q "^IMAGE_TAG=" "$ENV_FILE"; then
|
||
# Update existing IMAGE_TAG line
|
||
sed -i.bak "s/^IMAGE_TAG=.*/IMAGE_TAG=$VERSION/" "$ENV_FILE"
|
||
else
|
||
# Add IMAGE_TAG line if it doesn't exist
|
||
echo "IMAGE_TAG=$VERSION" >> "$ENV_FILE"
|
||
fi
|
||
print_success "Updated IMAGE_TAG to $VERSION in .env file"
|
||
print_success "Configuration updated for upgrade"
|
||
else
|
||
print_info "Keeping existing configuration..."
|
||
print_success "Will restart with current settings"
|
||
fi
|
||
|
||
# Honor --include-craft on existing installs regardless of update/restart:
|
||
# enable Craft at runtime and align SANDBOX_BACKEND with the effective tag
|
||
# (the one just chosen, or the pinned IMAGE_TAG already in .env on a restart).
|
||
if [ "$INCLUDE_CRAFT" = true ]; then
|
||
if grep -q "^#* *ENABLE_CRAFT=" "$ENV_FILE"; then
|
||
sed -i.bak 's/^#* *ENABLE_CRAFT=.*/ENABLE_CRAFT=true/' "$ENV_FILE"
|
||
else
|
||
echo "ENABLE_CRAFT=true" >> "$ENV_FILE"
|
||
fi
|
||
EFFECTIVE_TAG="${VERSION:-$(grep -E '^IMAGE_TAG=' "$ENV_FILE" | head -1 | cut -d= -f2-)}"
|
||
set_sandbox_backend_for_tag "$EFFECTIVE_TAG"
|
||
print_success "Onyx Craft enabled (ENABLE_CRAFT=true, SANDBOX_BACKEND=$SANDBOX_BACKEND_VALUE, image tag: ${EFFECTIVE_TAG})"
|
||
fi
|
||
|
||
# Ensure COMPOSE_PROFILES is cleared when running in lite mode on an
|
||
# existing .env (the template ships with s3-filestore enabled).
|
||
if [[ "$LITE_MODE" = true ]] && grep -q "^COMPOSE_PROFILES=.*s3-filestore" "$ENV_FILE" 2>/dev/null; then
|
||
sed -i.bak 's/^COMPOSE_PROFILES=.*/COMPOSE_PROFILES=/' "$ENV_FILE" 2>/dev/null || true
|
||
print_success "Cleared COMPOSE_PROFILES for lite mode"
|
||
fi
|
||
else
|
||
print_info "No existing .env file found. Setting up new deployment..."
|
||
echo ""
|
||
|
||
# Craft uses the regular backend image (ENABLE_CRAFT is a runtime flag), so
|
||
# the tag is prompted for normally even with --include-craft.
|
||
print_info "Which tag would you like to deploy?"
|
||
echo ""
|
||
echo "• Press Enter for ${DEFAULT_IMAGE_TAG} (recommended)"
|
||
echo "• Type a specific tag (e.g., v0.1.0)"
|
||
echo ""
|
||
prompt_or_default "Enter tag [default: ${DEFAULT_IMAGE_TAG}]: " "${DEFAULT_IMAGE_TAG}"
|
||
VERSION="$REPLY"
|
||
echo ""
|
||
|
||
if [ "$VERSION" = "edge" ]; then
|
||
print_info "Selected: edge (latest nightly)"
|
||
else
|
||
print_info "Selected: $VERSION"
|
||
fi
|
||
|
||
# Create .env file from template
|
||
print_info "Creating .env file with your selections..."
|
||
cp "$ENV_TEMPLATE" "$ENV_FILE"
|
||
|
||
# Update IMAGE_TAG with selected version
|
||
print_info "Setting IMAGE_TAG to $VERSION..."
|
||
sed -i.bak "s/^IMAGE_TAG=.*/IMAGE_TAG=$VERSION/" "$ENV_FILE"
|
||
print_success "IMAGE_TAG set to $VERSION"
|
||
|
||
# In lite mode, MinIO never starts (COMPOSE_PROFILES cleared) and the
|
||
# onyx-lite overlay forces FILE_STORE_BACKEND=postgres at runtime; set it in
|
||
# .env too so the file isn't misleadingly left on s3 (the MinIO/S3 creds
|
||
# generated below are then unused, but stay ready if s3-filestore is enabled).
|
||
if [[ "$LITE_MODE" = true ]]; then
|
||
sed -i.bak 's/^COMPOSE_PROFILES=.*/COMPOSE_PROFILES=/' "$ENV_FILE" 2>/dev/null || true
|
||
sed -i.bak 's/^FILE_STORE_BACKEND=.*/FILE_STORE_BACKEND=postgres/' "$ENV_FILE" 2>/dev/null || true
|
||
print_success "Cleared COMPOSE_PROFILES and set FILE_STORE_BACKEND=postgres for lite mode"
|
||
fi
|
||
|
||
# Check if openssl is available
|
||
if ! command -v openssl &> /dev/null; then
|
||
print_error "openssl is required to generate secure secrets but was not found."
|
||
exit 1
|
||
fi
|
||
|
||
# Generate a secure USER_AUTH_SECRET
|
||
USER_AUTH_SECRET=$(openssl rand -hex 32)
|
||
sed -i.bak "s/^USER_AUTH_SECRET=.*/USER_AUTH_SECRET=\"$USER_AUTH_SECRET\"/" "$ENV_FILE" 2>/dev/null || true
|
||
|
||
# Random MinIO/S3 credentials instead of the minioadmin default. The app uses
|
||
# MinIO's root creds, so the same pair goes to both the server and app vars.
|
||
MINIO_ACCESS_KEY=$(openssl rand -hex 16)
|
||
MINIO_SECRET_KEY=$(openssl rand -hex 32)
|
||
sed -i.bak "s/^MINIO_ROOT_USER=.*/MINIO_ROOT_USER=$MINIO_ACCESS_KEY/" "$ENV_FILE" 2>/dev/null || true
|
||
sed -i.bak "s/^MINIO_ROOT_PASSWORD=.*/MINIO_ROOT_PASSWORD=$MINIO_SECRET_KEY/" "$ENV_FILE" 2>/dev/null || true
|
||
sed -i.bak "s/^S3_AWS_ACCESS_KEY_ID=.*/S3_AWS_ACCESS_KEY_ID=$MINIO_ACCESS_KEY/" "$ENV_FILE" 2>/dev/null || true
|
||
sed -i.bak "s/^S3_AWS_SECRET_ACCESS_KEY=.*/S3_AWS_SECRET_ACCESS_KEY=$MINIO_SECRET_KEY/" "$ENV_FILE" 2>/dev/null || true
|
||
|
||
# Configure Craft based on --include-craft.
|
||
# By default, env.template has Craft commented out (disabled).
|
||
if [ "$INCLUDE_CRAFT" = true ]; then
|
||
# Set ENABLE_CRAFT=true for runtime configuration (handles commented and uncommented lines)
|
||
sed -i.bak 's/^#* *ENABLE_CRAFT=.*/ENABLE_CRAFT=true/' "$ENV_FILE" 2>/dev/null || true
|
||
# docker backend exists only in v4.0.6+; older tags get kubernetes.
|
||
set_sandbox_backend_for_tag "$VERSION"
|
||
print_success "Onyx Craft enabled (ENABLE_CRAFT=true, SANDBOX_BACKEND=$SANDBOX_BACKEND_VALUE)"
|
||
|
||
if [ "$SANDBOX_BACKEND_VALUE" = "docker" ]; then
|
||
echo ""
|
||
print_warning "Craft + docker backend: api_server and background bind-mount"
|
||
print_warning "/var/run/docker.sock (RW = root on host on compromise);"
|
||
print_warning "sandbox-proxy bind-mounts it RO (still exposes container env,"
|
||
print_warning "labels, and the events stream). Only enable on hosts you fully"
|
||
print_warning "control. On EC2, require IMDSv2 (HttpTokens=required) so"
|
||
print_warning "sandboxes cannot pull IAM credentials from instance metadata."
|
||
echo ""
|
||
else
|
||
print_info "Image tag ${VERSION} predates the docker sandbox backend (v4.0.6+); using SANDBOX_BACKEND=${SANDBOX_BACKEND_VALUE}."
|
||
fi
|
||
else
|
||
print_info "Onyx Craft disabled (use --include-craft to enable)"
|
||
fi
|
||
|
||
print_success ".env file created with your preferences"
|
||
echo ""
|
||
print_info "IMPORTANT: The .env file has been configured with your selections."
|
||
print_info "You can customize it later for:"
|
||
echo " • AI model configuration"
|
||
echo " • Domain settings (for production)"
|
||
echo " • Onyx Craft (set ENABLE_CRAFT=true)"
|
||
echo ""
|
||
fi
|
||
|
||
# Pre-create the sandbox bridge — compose overlay references it as external.
|
||
if [ "$INCLUDE_CRAFT" = true ]; then
|
||
SANDBOX_NET="${SANDBOX_DOCKER_NETWORK:-onyx_craft_sandbox}"
|
||
if ! ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} docker network inspect "$SANDBOX_NET" >/dev/null 2>&1; then
|
||
if ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} docker network create "$SANDBOX_NET" >/dev/null 2>&1; then
|
||
print_success "Created sandbox bridge network: $SANDBOX_NET"
|
||
else
|
||
print_warning "Could not create sandbox network $SANDBOX_NET — create it manually:"
|
||
echo " docker network create $SANDBOX_NET"
|
||
fi
|
||
fi
|
||
|
||
# Same for the CA volume. Name pinned to match
|
||
# configs.SANDBOX_PROXY_CA_VOLUME_NAME.
|
||
SANDBOX_PROXY_CA_VOL="sandbox_proxy_ca"
|
||
if ! ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} docker volume inspect "$SANDBOX_PROXY_CA_VOL" >/dev/null 2>&1; then
|
||
if ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} docker volume create "$SANDBOX_PROXY_CA_VOL" >/dev/null 2>&1; then
|
||
print_success "Created sandbox proxy CA volume: $SANDBOX_PROXY_CA_VOL"
|
||
else
|
||
print_warning "Could not create sandbox proxy CA volume $SANDBOX_PROXY_CA_VOL — create it manually:"
|
||
echo " docker volume create $SANDBOX_PROXY_CA_VOL"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Function to check if a port is available
|
||
is_port_available() {
|
||
local port=$1
|
||
|
||
# Try netcat first if available
|
||
if command -v nc &> /dev/null; then
|
||
# Try to connect to the port, if it fails, the port is available
|
||
if nc -z localhost "$port" 2>/dev/null; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
# Fallback using curl/telnet approach
|
||
elif command -v curl &> /dev/null; then
|
||
# Try to connect with curl, if it fails, the port might be available
|
||
if curl -s --max-time 1 --connect-timeout 1 "http://localhost:$port" >/dev/null 2>&1; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
# Final fallback using lsof if available
|
||
elif command -v lsof &> /dev/null; then
|
||
# Check if any process is listening on the port
|
||
if lsof -i ":$port" >/dev/null 2>&1; then
|
||
return 1
|
||
fi
|
||
return 0
|
||
else
|
||
# No port checking tools available, assume port is available
|
||
print_warning "No port checking tools available (nc, curl, lsof). Assuming port $port is available."
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
# Function to find the first available port starting from a given port
|
||
find_available_port() {
|
||
local start_port=${1:-3000}
|
||
local port=$start_port
|
||
|
||
while [ "$port" -le 65535 ]; do
|
||
if is_port_available "$port"; then
|
||
echo "$port"
|
||
return 0
|
||
fi
|
||
port=$((port + 1))
|
||
done
|
||
|
||
# If no port found, return the original port as fallback
|
||
echo "$start_port"
|
||
return 1
|
||
}
|
||
|
||
# Check for port checking tools availability
|
||
PORT_CHECK_AVAILABLE=false
|
||
if command -v nc &> /dev/null || command -v curl &> /dev/null || command -v lsof &> /dev/null; then
|
||
PORT_CHECK_AVAILABLE=true
|
||
fi
|
||
|
||
if [ "$PORT_CHECK_AVAILABLE" = false ]; then
|
||
print_warning "No port checking tools found (nc, curl, lsof). Port detection may not work properly."
|
||
print_info "Consider installing one of these tools for reliable automatic port detection."
|
||
fi
|
||
|
||
# Find available port for nginx
|
||
print_step "Checking for available ports"
|
||
AVAILABLE_PORT=$(find_available_port 3000)
|
||
|
||
if [ "$AVAILABLE_PORT" != "3000" ]; then
|
||
print_info "Port 3000 is in use, found available port: $AVAILABLE_PORT"
|
||
else
|
||
print_info "Port 3000 is available"
|
||
fi
|
||
|
||
# Export HOST_PORT for docker-compose
|
||
export HOST_PORT=$AVAILABLE_PORT
|
||
print_success "Using port $AVAILABLE_PORT for nginx"
|
||
|
||
# Determine if we're using a floating tag (edge, latest) that should force pull.
|
||
# Read IMAGE_TAG from .env file and remove any quotes or whitespace.
|
||
CURRENT_IMAGE_TAG=$(grep "^IMAGE_TAG=" "$ENV_FILE" | head -1 | cut -d'=' -f2 | tr -d ' "'"'"'')
|
||
|
||
if [ "$CURRENT_IMAGE_TAG" = "edge" ] || [ "$CURRENT_IMAGE_TAG" = "latest" ]; then
|
||
USE_LATEST=true
|
||
# Floating tags track main, so configs should come from main.
|
||
CONFIG_REF="main"
|
||
print_info "Using '$CURRENT_IMAGE_TAG' tag - will force pull and recreate containers"
|
||
else
|
||
USE_LATEST=false
|
||
# Pinned release tags use their own ref so configs match the images.
|
||
CONFIG_REF="$CURRENT_IMAGE_TAG"
|
||
fi
|
||
|
||
# The initial download pulled configs from RELEASE_REF (latest release, or main
|
||
# on fallback). If the chosen image tag wants configs from a different ref,
|
||
# re-fetch them so the compose file matches the images being pulled.
|
||
if [[ "$CONFIG_REF" != "$RELEASE_REF" ]] && [[ "$USE_LOCAL_FILES" = false ]]; then
|
||
CONFIG_BASE="https://raw.githubusercontent.com/onyx-dot-app/onyx/${CONFIG_REF}/deployment"
|
||
print_info "Fetching config files matching ${CONFIG_REF}..."
|
||
if download_file "${CONFIG_BASE}/docker_compose/docker-compose.yml" "${INSTALL_ROOT}/deployment/docker-compose.yml" 2>/dev/null; then
|
||
download_file "${CONFIG_BASE}/data/nginx/app.conf.template" "${INSTALL_ROOT}/data/nginx/app.conf.template" 2>/dev/null || true
|
||
download_file "${CONFIG_BASE}/data/nginx/run-nginx.sh" "${INSTALL_ROOT}/data/nginx/run-nginx.sh" 2>/dev/null || true
|
||
chmod +x "${INSTALL_ROOT}/data/nginx/run-nginx.sh"
|
||
if [[ "$LITE_MODE" = true ]]; then
|
||
download_file "${CONFIG_BASE}/docker_compose/${LITE_COMPOSE_FILE}" \
|
||
"${INSTALL_ROOT}/deployment/${LITE_COMPOSE_FILE}" 2>/dev/null || true
|
||
fi
|
||
if [[ "$INCLUDE_CRAFT" = true ]]; then
|
||
download_file "${CONFIG_BASE}/docker_compose/${CRAFT_COMPOSE_FILE}" \
|
||
"${INSTALL_ROOT}/deployment/${CRAFT_COMPOSE_FILE}" 2>/dev/null || true
|
||
fi
|
||
print_success "Config files updated to match ${CONFIG_REF}"
|
||
else
|
||
print_warning "Ref ${CONFIG_REF} not found on GitHub — keeping configs from ${RELEASE_REF}"
|
||
fi
|
||
fi
|
||
|
||
# Pull Docker images with reduced output
|
||
print_step "Pulling Docker images"
|
||
print_info "This may take several minutes depending on your internet connection..."
|
||
echo ""
|
||
print_info "Downloading Docker images (this may take a while)..."
|
||
# Pass IMAGE_TAG inline so the value the user picked at the prompt wins over
|
||
# any `export IMAGE_TAG=...` inherited from the caller's shell. Compose's
|
||
# substitution prefers shell env over .env, so without this an exported
|
||
# IMAGE_TAG in the user's shellrc would silently swap the images being pulled.
|
||
build_compose_file_args
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
if (cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} HOST_PORT="$HOST_PORT" IMAGE_TAG="$CURRENT_IMAGE_TAG" $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" pull --quiet); then
|
||
print_success "Docker images downloaded successfully"
|
||
else
|
||
print_error "Failed to download Docker images"
|
||
exit 1
|
||
fi
|
||
|
||
# Build the up flags. With --wait (default), Compose waits until every
|
||
# container with a healthcheck reports healthy before returning, and names any
|
||
# service that stays unhealthy past the timeout.
|
||
UP_WAIT_ARGS=()
|
||
if [[ "$NO_WAIT" = false ]]; then
|
||
UP_WAIT_ARGS=(--wait --wait-timeout "${WAIT_TIMEOUT_SECONDS}")
|
||
fi
|
||
|
||
# Start services
|
||
print_step "Starting Onyx services"
|
||
print_info "Launching containers..."
|
||
if [[ "$NO_WAIT" = false ]]; then
|
||
print_info "Waiting up to ${WAIT_TIMEOUT_SECONDS}s for all services to become healthy..."
|
||
fi
|
||
echo ""
|
||
UP_EXIT=0
|
||
if [ "$USE_LATEST" = true ]; then
|
||
print_info "Force pulling latest images and recreating containers..."
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
(cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} HOST_PORT="$HOST_PORT" IMAGE_TAG="$CURRENT_IMAGE_TAG" $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" up -d --pull always --force-recreate "${UP_WAIT_ARGS[@]}") || UP_EXIT=$?
|
||
else
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
(cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} HOST_PORT="$HOST_PORT" IMAGE_TAG="$CURRENT_IMAGE_TAG" $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" up -d "${UP_WAIT_ARGS[@]}") || UP_EXIT=$?
|
||
fi
|
||
if [ $UP_EXIT -ne 0 ]; then
|
||
print_error "Failed to start Onyx services"
|
||
echo ""
|
||
print_info "Current container status:"
|
||
# shellcheck disable=SC2086 # COMPOSE_CMD is "docker compose" and needs word-splitting
|
||
(cd "${INSTALL_ROOT}/deployment" && ${DOCKER_SUDO[@]+"${DOCKER_SUDO[@]}"} HOST_PORT="$HOST_PORT" IMAGE_TAG="$CURRENT_IMAGE_TAG" $COMPOSE_CMD "${COMPOSE_FILE_ARGS[@]}" ps)
|
||
echo ""
|
||
print_info "Check the logs of any unhealthy service:"
|
||
echo " (cd \"${INSTALL_ROOT}/deployment\" && $(is_using_sudo && echo "sudo ")$COMPOSE_CMD ${COMPOSE_FILE_ARGS[*]} logs <service>)"
|
||
echo ""
|
||
print_info "If the issue persists, please contact: founders@onyx.app"
|
||
exit 1
|
||
fi
|
||
|
||
# Success message
|
||
print_step "Installation Complete!"
|
||
echo ""
|
||
if [[ "$NO_WAIT" = false ]]; then
|
||
echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${GREEN}${BOLD} 🎉 Onyx service is ready! 🎉${NC}"
|
||
echo -e "${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
else
|
||
echo -e "${YELLOW}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${YELLOW}${BOLD} ⚠️ Onyx containers started ⚠️${NC}"
|
||
echo -e "${YELLOW}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
print_info "Services may still be initializing. Check status with:"
|
||
echo " (cd \"${INSTALL_ROOT}/deployment\" && $(is_using_sudo && echo "sudo ")$COMPOSE_CMD ${COMPOSE_FILE_ARGS[*]} ps)"
|
||
fi
|
||
echo ""
|
||
print_info "Access Onyx at:"
|
||
echo -e " ${BOLD}http://localhost:${HOST_PORT}${NC}"
|
||
echo ""
|
||
print_info "If authentication is enabled, you can create your admin account here:"
|
||
echo " • Visit http://localhost:${HOST_PORT}/auth/signup to create your admin account"
|
||
echo " • The first user created will automatically have admin privileges"
|
||
echo ""
|
||
if [[ "$LITE_MODE" = true ]]; then
|
||
echo ""
|
||
print_info "Running in Lite mode — the following services are NOT started:"
|
||
echo " • Vespa (vector database)"
|
||
echo " • Redis (cache)"
|
||
echo " • Model servers (embedding/inference)"
|
||
echo " • Background workers (Celery)"
|
||
echo ""
|
||
print_info "Connectors and RAG search are disabled. LLM chat, tools, user file"
|
||
print_info "uploads, Projects, Agent knowledge, and code interpreter still work."
|
||
fi
|
||
echo ""
|
||
print_info "Refer to the README in the ${INSTALL_ROOT} directory for more information."
|
||
echo ""
|
||
print_info "For help or issues, contact: founders@onyx.app"
|
||
echo ""
|
||
|
||
# --- GitHub star prompt (inspired by oh-my-codex) ---
|
||
# Only prompt in interactive mode and only if gh CLI is available.
|
||
# Uses the GitHub API directly (PUT /user/starred) like oh-my-codex.
|
||
if is_interactive && command -v gh &>/dev/null; then
|
||
prompt_yn_or_default "Enjoying Onyx? Star the repo on GitHub? [Y/n] " "Y"
|
||
if [[ ! "$REPLY" =~ ^[Nn] ]]; then
|
||
if GH_PAGER='' gh api -X PUT /user/starred/onyx-dot-app/onyx < /dev/null >/dev/null 2>&1; then
|
||
print_success "Thanks for the star!"
|
||
else
|
||
print_info "Star us at: https://github.com/onyx-dot-app/onyx"
|
||
fi
|
||
fi
|
||
fi
|