open-design/deploy/scripts/install.sh
hahalolo 1aa72b6d09
fix(docker): fix container startup crash due to missing OD_API_TOKEN (#2928)
* 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
2026-05-26 06:15:48 +00:00

508 lines
17 KiB
Bash
Executable file
Raw 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="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"