Files
nexu-io-open-design/deploy/scripts/install.sh
Marc Chan 7abb7888df fix(deploy): align Docker defaults with GHCR releases (#4327)
* fix(deploy): align Docker defaults with GHCR releases

Generated-By: looper 0.9.9 (runner=worker, agent=opencode)

* fix(ci): publish stable Docker tags from release workflow

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)

* fix(ci): fold reusable workflow guard expression

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)

* fix(ci): gate Docker release publish

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)

* fix(ci): publish stable Docker tags after release

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)

* fix(ci): guard Docker latest tag enable expression

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)

* fix(deploy): update Helm chart GHCR defaults

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)

* fix(ci): publish latest from release workflow inputs

Generated-By: looper 0.9.9 (runner=fixer, agent=opencode)
2026-06-17 12:25:18 +08:00

509 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Open Design — One-Click Installer
# Docker Compose wrapper for Linux and macOS
#
# Usage:
# Clone this repo, then run:
# ./install.sh [--non-interactive] [--port 7456] [--image <ref>] [--skip-docker-install] [--no-systemd]
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DEFAULT_PORT=7456
DEFAULT_IMAGE="ghcr.io/nexu-io/od:latest"
DEFAULT_MEM_LIMIT="384m"
HEALTH_TIMEOUT=60
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.yml"
OVERRIDE_FILE="${DEPLOY_DIR}/docker-compose.override.yml"
# Build the -f argument list: always include the base file,
# and add the override if it exists (used by tests for isolation).
COMPOSE_FILES=(-f "$COMPOSE_FILE")
if [ -f "$OVERRIDE_FILE" ]; then
COMPOSE_FILES+=(-f "$OVERRIDE_FILE")
fi
if [ ! -f "$COMPOSE_FILE" ]; then
echo "WARN: docker-compose.yml not found at ${COMPOSE_FILE}" >&2
echo " Clone the repository and run this script from deploy/scripts/." >&2
exit 1
fi
ENV_FILE="${DEPLOY_DIR}/.env"
# ---------------------------------------------------------------------------
# Colors & formatting
# ---------------------------------------------------------------------------
BOLD="" DIM="" RED="" GREEN="" YELLOW="" CYAN="" RESET=""
if [ -t 1 ]; then
BOLD="\033[1m" DIM="\033[2m" RED="\033[31m" GREEN="\033[32m"
YELLOW="\033[33m" CYAN="\033[36m" RESET="\033[0m"
fi
step() { printf " ${DIM}${RESET} %s\n" "$1"; }
ok() { printf " ${GREEN}${RESET} %s\n" "$1"; }
warn() { printf " ${YELLOW}!${RESET} %s\n" "$1" >&2; }
error() { printf " ${RED}${RESET} %s\n" "$1" >&2; }
info() { printf " ${CYAN}${RESET} %s\n" "$1"; }
prompt_text() {
_prompt="$1" _default="$2"
printf "%s [%s]: " "$_prompt" "$_default" >&2
read -r _val
PROMPT_RESULT="${_val:-$_default}"
}
prompt_confirm() {
_question="$1" _default="$2"
if [ "$NON_INTERACTIVE" = "1" ]; then
return 0
fi
_yn_default="y"
if [ "$_default" = "0" ]; then _yn_default="n"; fi
printf "%s [%s]: " "$_question" "$_yn_default" >&2
read -r _yn
case "$_yn" in
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) [ "$_default" = "1" ] && return 0; return 1 ;;
esac
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
NON_INTERACTIVE=0
OPT_PORT=""
OPT_IMAGE=""
OPT_SKIP_DOCKER=0
OPT_NO_SYSTEMD=0
while [ $# -gt 0 ]; do
case "$1" in
--non-interactive) NON_INTERACTIVE=1 ;;
--port) shift; OPT_PORT="$1" ;;
--port=*) OPT_PORT="${1#--port=}" ;;
--image) shift; OPT_IMAGE="$1" ;;
--image=*) OPT_IMAGE="${1#--image=}" ;;
--skip-docker-install) OPT_SKIP_DOCKER=1 ;;
--no-systemd) OPT_NO_SYSTEMD=1 ;;
--help|-h)
echo "Usage: install.sh [options]"
echo ""
echo "Options:"
echo " --non-interactive Use defaults for all prompts"
echo " --port <n> Host port (default: ${DEFAULT_PORT})"
echo " --image <ref> Docker image reference"
echo " --skip-docker-install Do not attempt to install Docker"
echo " --no-systemd Skip systemd unit creation"
echo " --help Show this help"
exit 0
;;
*)
warn "Unknown argument: $1 (ignored)"
;;
esac
shift
done
# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
printf "\n"
printf "${BOLD} ┌──────────────────────────────────────┐${RESET}\n"
printf "${BOLD}${RESET} ${BOLD}${RESET}\n"
printf "${BOLD}${RESET} ${CYAN}${RESET} ${BOLD}Open Design${RESET} ${BOLD}${RESET}\n"
printf "${BOLD}${RESET} ${DIM}One-Click Installer${RESET} ${BOLD}${RESET}\n"
printf "${BOLD}${RESET} ${BOLD}${RESET}\n"
printf "${BOLD} └──────────────────────────────────────┘${RESET}\n"
printf "\n"
# ---------------------------------------------------------------------------
# 1. OS detection
# ---------------------------------------------------------------------------
OS="$(uname -s)"
ARCH="$(uname -m)"
case "$OS" in
Linux)
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO="$ID"
DISTRO_VERSION="$VERSION_ID"
else
DISTRO="unknown"
DISTRO_VERSION=""
fi
step "OS: Linux ${DISTRO} ${DISTRO_VERSION} (${ARCH})"
;;
Darwin)
step "OS: macOS $(sw_vers -productVersion) (${ARCH})"
DISTRO="macos"
;;
*)
error "Unsupported OS: ${OS}. This script supports Linux and macOS."
exit 1
;;
esac
# ---------------------------------------------------------------------------
# 2. Container runtime detection (Docker or Podman)
# ---------------------------------------------------------------------------
CONTAINER_CMD=""
CONTAINER_RUNTIME=""
detect_container_runtime() {
if command -v docker >/dev/null 2>&1; then
CONTAINER_CMD="docker"
CONTAINER_RUNTIME="docker"
return 0
fi
if command -v podman >/dev/null 2>&1; then
CONTAINER_CMD="podman"
CONTAINER_RUNTIME="podman"
return 0
fi
return 1
}
detect_compose() {
if $CONTAINER_CMD compose version >/dev/null 2>&1; then
COMPOSE_CMD="${CONTAINER_CMD} compose"
return 0
fi
if [ "$CONTAINER_RUNTIME" = "podman" ] && command -v podman-compose >/dev/null 2>&1; then
COMPOSE_CMD="podman-compose"
return 0
fi
if [ "$CONTAINER_RUNTIME" = "docker" ] && command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
return 0
fi
return 1
}
check_runtime_running() {
$CONTAINER_CMD info >/dev/null 2>&1
}
if ! detect_container_runtime; then
if [ "$OPT_SKIP_DOCKER" = "1" ]; then
error "No container runtime found (Docker or Podman) and --skip-docker-install was set."
exit 1
fi
warn "No container runtime found (Docker or Podman)."
case "$DISTRO" in
ubuntu|debian)
install_cmd="sudo apt-get update && sudo apt-get install -y docker.io docker-compose-plugin"
step "Install Docker with: ${install_cmd}"
step "Or Podman: sudo apt-get install -y podman podman-compose"
;;
fedora)
install_cmd="sudo dnf install -y docker docker-compose-plugin"
step "Install Docker with: ${install_cmd}"
step "Or Podman: sudo dnf install -y podman podman-compose"
;;
centos|rhel|rocky|alma)
install_cmd="sudo yum install -y docker docker-compose-plugin"
step "Install Docker with: ${install_cmd}"
step "Or Podman: sudo yum install -y podman podman-compose"
;;
macos)
install_cmd="brew install --cask docker"
step "Install Docker with: ${install_cmd}"
step "Or Podman: brew install podman && podman machine init && podman machine start"
;;
*)
error "Cannot auto-detect install method for: ${DISTRO}"
step "Install Docker: https://docs.docker.com/get-docker/"
step "Or Podman: https://podman.io/getting-started/installation"
exit 1
;;
esac
if [ "$NON_INTERACTIVE" = "0" ]; then
if prompt_confirm "Install Docker now?" 0; then
step "Running: ${install_cmd}"
eval "$install_cmd"
if ! detect_container_runtime; then
error "Installation did not succeed. Install manually and re-run."
exit 1
fi
else
error "A container runtime is required. Install Docker or Podman and re-run."
exit 1
fi
else
error "A container runtime is required in non-interactive mode. Install Docker or Podman first."
exit 1
fi
fi
if ! detect_compose; then
error "Container Compose is not available."
if [ "$CONTAINER_RUNTIME" = "podman" ]; then
case "$OS" in
Darwin) step "Install with: brew install podman-compose" ;;
*) step "Install with: sudo apt-get install podman-compose (or equivalent for your distro)" ;;
esac
else
step "Install the compose plugin: https://docs.docker.com/compose/install/"
fi
exit 1
fi
if ! check_runtime_running; then
warn "${CONTAINER_RUNTIME} is not running."
case "$DISTRO" in
ubuntu|debian|fedora|centos|rhel|rocky|alma)
if [ "$CONTAINER_RUNTIME" = "podman" ]; then
step "Start with: podman machine init && podman machine start"
else
step "Start with: sudo systemctl start docker"
fi
;;
macos)
if [ "$CONTAINER_RUNTIME" = "podman" ]; then
step "Start with: podman machine start"
else
step "Open Docker Desktop from your Applications folder."
fi
;;
esac
if [ "$NON_INTERACTIVE" = "0" ]; then
if prompt_confirm "Retry after starting ${CONTAINER_RUNTIME}?" 1; then
if ! check_runtime_running; then
error "${CONTAINER_RUNTIME} is still not running. Start it and re-run."
exit 1
fi
else
exit 1
fi
else
exit 1
fi
fi
RUNTIME_VERSION="$(${CONTAINER_CMD} --version 2>/dev/null || echo 'unknown')"
COMPOSE_VERSION="$(${COMPOSE_CMD} version 2>/dev/null || echo 'unknown')"
step "Runtime: ${CONTAINER_RUNTIME} ${RUNTIME_VERSION}"
step "Compose: ${COMPOSE_VERSION}"
# ---------------------------------------------------------------------------
# 2b. Check if Open Design is already running
# ---------------------------------------------------------------------------
if $CONTAINER_CMD ps --filter "name=open-design" --format '{{.Names}}' 2>/dev/null | grep -q 'open-design'; then
STATUS="$($CONTAINER_CMD inspect --format '{{.State.Status}}' open-design 2>/dev/null || echo 'unknown')"
IMAGE="$($CONTAINER_CMD inspect --format '{{.Config.Image}}' open-design 2>/dev/null || echo 'unknown')"
PORTS="$($CONTAINER_CMD port open-design 2>/dev/null || echo 'unknown')"
error "Open Design is already running."
printf "\n"
printf " Container: open-design\n"
printf " Status: %s\n" "$STATUS"
printf " Image: %s\n" "$IMAGE"
printf " Ports: %s\n" "$PORTS"
printf "\n"
step "To update: ${SCRIPT_DIR}/update.sh"
step "To remove: ${SCRIPT_DIR}/uninstall.sh"
printf "\n"
exit 1
fi
# ---------------------------------------------------------------------------
# 3. Port conflict detection
# ---------------------------------------------------------------------------
PORT="${OPT_PORT:-$DEFAULT_PORT}"
check_port_in_use() {
if command -v ss >/dev/null 2>&1; then
ss -tlnp 2>/dev/null | grep -q ":${1} " && return 0
elif command -v lsof >/dev/null 2>&1; then
lsof -i :"$1" 2>/dev/null | grep -q LISTEN && return 0
fi
return 1
}
if check_port_in_use "$PORT"; then
warn "Port ${PORT} is already in use."
if [ "$NON_INTERACTIVE" = "0" ]; then
_suggest=$((PORT + 1))
prompt_text "Enter a different port" "$_suggest"
PORT="$PROMPT_RESULT"
if check_port_in_use "$PORT"; then
error "Port ${PORT} is also in use. Please pick a free port."
exit 1
fi
else
error "Port ${PORT} is occupied. Use --port to specify a different one."
exit 1
fi
fi
# ---------------------------------------------------------------------------
# 4. Interactive prompts
# ---------------------------------------------------------------------------
IMAGE="${OPT_IMAGE:-$DEFAULT_IMAGE}"
ALLOWED_ORIGINS=""
MEM_LIMIT="$DEFAULT_MEM_LIMIT"
if [ "$NON_INTERACTIVE" = "0" ]; then
printf "\n"
prompt_text "Docker image" "$DEFAULT_IMAGE"
IMAGE="$PROMPT_RESULT"
prompt_text "Port" "$PORT"
PORT="$PROMPT_RESULT"
prompt_text "Allowed origins (CORS, comma-separated, or empty)" ""
ALLOWED_ORIGINS="$PROMPT_RESULT"
prompt_text "Memory limit" "$DEFAULT_MEM_LIMIT"
MEM_LIMIT="$PROMPT_RESULT"
if check_port_in_use "$PORT"; then
error "Port ${PORT} is already in use. Aborting."
exit 1
fi
fi
# ---------------------------------------------------------------------------
# 5. Generate .env
# ---------------------------------------------------------------------------
if [ -f "$ENV_FILE" ]; then
BACKUP="${ENV_FILE}.$(date +%Y%m%d%H%M%S).bak"
step "Existing .env found. Backing up to ${BACKUP}"
cp "$ENV_FILE" "$BACKUP"
fi
GENERATED_TOKEN=$(openssl rand -hex 32 2>/dev/null || od -vAn -N32 -tx1 /dev/urandom | tr -d ' \n' 2>/dev/null)
cat > "$ENV_FILE" << ENVFILE
# Generated by install.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
OPEN_DESIGN_IMAGE=${IMAGE}
OPEN_DESIGN_PORT=${PORT}
OPEN_DESIGN_ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
OPEN_DESIGN_MEM_LIMIT=${MEM_LIMIT}
NODE_OPTIONS=--max-old-space-size=192
OD_API_TOKEN=${GENERATED_TOKEN}
ENVFILE
ok "Written ${ENV_FILE}"
# ---------------------------------------------------------------------------
# 6. Pull and start
# ---------------------------------------------------------------------------
step "Pulling image: ${IMAGE}"
$COMPOSE_CMD "${COMPOSE_FILES[@]}" pull
step "Starting Open Design..."
$COMPOSE_CMD "${COMPOSE_FILES[@]}" up -d --no-build
# ---------------------------------------------------------------------------
# 7. Health check
# ---------------------------------------------------------------------------
step "Waiting for health check (up to ${HEALTH_TIMEOUT}s)..."
HEALTH_URL="http://127.0.0.1:${PORT}/api/health"
HEALTH_OK=0
ELAPSED=0
while [ "$ELAPSED" -lt "$HEALTH_TIMEOUT" ]; do
if command -v curl >/dev/null 2>&1; then
HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo '000')"
elif command -v wget >/dev/null 2>&1; then
HTTP_CODE="$(wget -q -O /dev/null --server-response "$HEALTH_URL" 2>&1 | grep 'HTTP/' | tail -1 | awk '{print $2}')"
else
HTTP_CODE="000"
fi
if [ "$HTTP_CODE" = "200" ]; then
HEALTH_OK=1
break
fi
sleep 2
ELAPSED=$((ELAPSED + 2))
done
if [ "$HEALTH_OK" = "1" ]; then
ok "Daemon is healthy (${HTTP_CODE} OK)"
else
warn "Health check did not pass within ${HEALTH_TIMEOUT}s."
step "Check status: ${COMPOSE_CMD} \"${COMPOSE_FILES[@]}\" logs"
fi
# ---------------------------------------------------------------------------
# 8. systemd unit (Linux only)
# ---------------------------------------------------------------------------
if [ "$OS" = "Linux" ] && [ "$OPT_NO_SYSTEMD" = "0" ]; then
if command -v systemctl >/dev/null 2>&1; then
SYSTEMD_DIR="${HOME}/.config/systemd/user"
SYSTEMD_UNIT="${SYSTEMD_DIR}/open-design.service"
mkdir -p "$SYSTEMD_DIR"
CONTAINER_BIN="$(command -v "$CONTAINER_CMD")"
{
echo "[Unit]"
echo "Description=Open Design daemon (${CONTAINER_RUNTIME} Compose)"
if [ "$CONTAINER_RUNTIME" = "docker" ]; then
echo "After=${CONTAINER_RUNTIME}.service"
echo "Requires=${CONTAINER_RUNTIME}.service"
fi
echo ""
echo "[Service]"
echo "Type=oneshot"
echo "RemainAfterExit=yes"
echo "WorkingDirectory=${DEPLOY_DIR}"
if [ -f "$OVERRIDE_FILE" ]; then
echo "ExecStart=${CONTAINER_BIN} compose -f ${COMPOSE_FILE} -f ${OVERRIDE_FILE} up -d --no-build"
echo "ExecStop=${CONTAINER_BIN} compose -f ${COMPOSE_FILE} -f ${OVERRIDE_FILE} down"
else
echo "ExecStart=${CONTAINER_BIN} compose -f ${COMPOSE_FILE} up -d --no-build"
echo "ExecStop=${CONTAINER_BIN} compose -f ${COMPOSE_FILE} down"
fi
echo "TimeoutStartSec=120"
echo ""
echo "[Install]"
echo "WantedBy=default.target"
} > "$SYSTEMD_UNIT"
systemctl --user daemon-reload
systemctl --user enable open-design 2>/dev/null || true
ok "systemd unit installed: open-design.service"
step "Manage with: systemctl --user start|stop|status open-design"
else
warn "systemd not found. Skipping service installation."
fi
fi
# ---------------------------------------------------------------------------
# 9. Summary
# ---------------------------------------------------------------------------
printf "\n"
printf "${BOLD}${GREEN} ── Installation Complete ──────────────────────────${RESET}\n"
printf "\n"
printf " URL: http://127.0.0.1:%s\n" "$PORT"
printf " Image: %s\n" "$IMAGE"
printf " Data volume: open_design_data\n"
printf " Config: %s\n" "$ENV_FILE"
if [ "$OS" = "Linux" ] && [ -f "${HOME}/.config/systemd/user/open-design.service" ]; then
printf " Service: systemd (open-design.service)\n"
fi
printf "\n"
printf " Next steps:\n"
printf " Update: %s/update.sh\n" "$SCRIPT_DIR"
printf " Uninstall: %s/uninstall.sh\n" "$SCRIPT_DIR"
printf " Logs: %s logs -f\n" "$COMPOSE_CMD"
printf "\n"
printf " Open http://127.0.0.1:%s in your browser\n\n" "$PORT"