mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* fix(docker): fix container startup crash due to missing OD_API_TOKEN * fix(docker): forward OD_API_TOKEN to fix docker container boot loop * fix(docker): enforce non-empty OD_API_TOKEN for docker-compose * fix(deploy): automate OD_API_TOKEN generation in installer and close compose loop * docs(readme): guide manual deployment users to configure OD_API_TOKEN * docs(readme): align working directory paths for manual deployment instructions * docs(readme): align working directory paths for manual deployment instructions * docs(readme): restore git clone context for first-time users
508 lines
17 KiB
Bash
Executable file
508 lines
17 KiB
Bash
Executable file
#!/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="docker.io/vanjayak/open-design: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"
|