mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
feat(deploy): add one-click Docker/Podman Compose installer for Linux… (#2414)
* feat(deploy): add one-click Docker/Podman Compose installer for Linux and macOS - Add install.sh with interactive wizard, Podman/Docker runtime detection, port conflict check, health verification, and systemd user unit creation - Add update.sh for image pull and restart with health check - Add uninstall.sh with interactive user data backup before removal - Unify CLI output styling with step/ok/warn/error/info helpers - Add install-guide.md documentation - Add install.test.ts integration test suite * feat(deploy): add one-click Docker/Podman Compose installer - interactive setup wizard with port, image, CORS, memory prompts - automatic Docker/Podman detection with install guidance - systemd user unit for Linux, health check polling - update.sh (pull + restart + prune) and uninstall.sh (backup + cleanup) - node:test integration suite and install-guide.md * style(deploy): improve POSIX sh compatibility and systemd unit handling - unify shell shebangs to #!/usr/bin/env bash - add pipefail option for better error handling - fix systemd unit for Podman: remove After/Requires when no service - correct documentation to match actual uninstall behavior * fix(deploy): address review feedback for installer scripts - remove curl | sh path, document clone-first only - isolate tests via docker-compose.override.yml with unique names - support both --image <ref> and --image=<ref> in update.sh - add running container detection before install * docs(install): remove demo scripts and add MCP note
This commit is contained in:
parent
bde64f8bdd
commit
e8b5dd8aaf
6 changed files with 1305 additions and 0 deletions
2
deploy/.gitignore
vendored
Normal file
2
deploy/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.bak
|
||||||
|
.env
|
||||||
505
deploy/scripts/install.sh
Executable file
505
deploy/scripts/install.sh
Executable file
|
|
@ -0,0 +1,505 @@
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
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
|
||||||
|
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"
|
||||||
233
deploy/scripts/uninstall.sh
Executable file
233
deploy/scripts/uninstall.sh
Executable file
|
|
@ -0,0 +1,233 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Open Design — Uninstaller
|
||||||
|
# Stops and removes the Docker Compose deployment
|
||||||
|
#
|
||||||
|
# Usage: ./uninstall.sh [--keep-data] [--non-interactive]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DEPLOY_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
ENV_FILE="${DEPLOY_DIR}/.env"
|
||||||
|
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.yml"
|
||||||
|
OVERRIDE_FILE="${DEPLOY_DIR}/docker-compose.override.yml"
|
||||||
|
|
||||||
|
COMPOSE_FILES=(-f "$COMPOSE_FILE")
|
||||||
|
if [ -f "$OVERRIDE_FILE" ]; then
|
||||||
|
COMPOSE_FILES+=(-f "$OVERRIDE_FILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Detect container runtime
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
COMPOSE_CMD=""
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
elif command -v podman >/dev/null 2>&1 && podman compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="podman compose"
|
||||||
|
elif command -v podman >/dev/null 2>&1 && command -v podman-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="podman-compose"
|
||||||
|
elif command -v docker >/dev/null 2>&1 && command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
else
|
||||||
|
error "No container runtime found. Install Docker or Podman."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUNTIME="${COMPOSE_CMD%% *}"
|
||||||
|
|
||||||
|
NON_INTERACTIVE=0
|
||||||
|
KEEP_DATA=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--non-interactive) NON_INTERACTIVE=1 ;;
|
||||||
|
--keep-data) KEEP_DATA=1 ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: uninstall.sh [options]"
|
||||||
|
echo " --keep-data Preserve the open_design_data volume"
|
||||||
|
echo " --non-interactive Skip confirmation prompts"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
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}Uninstaller${RESET} ${BOLD}│${RESET}\n"
|
||||||
|
printf "${BOLD} │${RESET} ${BOLD}│${RESET}\n"
|
||||||
|
printf "${BOLD} └──────────────────────────────────────┘${RESET}\n"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Find data volume (Compose prepends project name)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CONTAINER_NAME="${COMPOSE_CONTAINER_NAME:-open-design}"
|
||||||
|
VOLUME_BASE="${COMPOSE_VOLUME_NAME:-open_design_data}"
|
||||||
|
PROJECT_NAME="${COMPOSE_PROJECT_NAME:-open-design}"
|
||||||
|
|
||||||
|
DATA_VOLUME=""
|
||||||
|
for _vol in "${PROJECT_NAME}_${VOLUME_BASE}" "${VOLUME_BASE}"; do
|
||||||
|
if $RUNTIME volume inspect "$_vol" >/dev/null 2>&1; then
|
||||||
|
DATA_VOLUME="$_vol"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Backup user data
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
BACKUP_DIR=""
|
||||||
|
|
||||||
|
_do_backup() {
|
||||||
|
_dest="$1"
|
||||||
|
mkdir -p "$_dest"
|
||||||
|
|
||||||
|
step "Backing up user data to ${_dest}..."
|
||||||
|
|
||||||
|
# Try container cp first (works if container exists, running or stopped)
|
||||||
|
if $RUNTIME inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||||
|
$RUNTIME cp "$CONTAINER_NAME":/app/.od/. "$_dest/" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If container cp didn't work or container doesn't exist, use temp container
|
||||||
|
if [ ! -f "${_dest}/app.sqlite" ] && [ -n "$DATA_VOLUME" ]; then
|
||||||
|
_image="$($RUNTIME images -q | head -1)"
|
||||||
|
if [ -n "$_image" ]; then
|
||||||
|
$RUNTIME run --rm \
|
||||||
|
-v "${DATA_VOLUME}:/source:ro" \
|
||||||
|
-v "${_dest}:/backup" \
|
||||||
|
--user root \
|
||||||
|
"$_image" \
|
||||||
|
sh -c "cp -r /source/. /backup/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${_dest}/app.sqlite" ] || [ -d "${_dest}/projects" ]; then
|
||||||
|
ok "Backup saved to ${_dest}"
|
||||||
|
ok "Contents: app database, projects, artifacts, media config"
|
||||||
|
else
|
||||||
|
warn "No user data found to back up."
|
||||||
|
rm -rf "$_dest"
|
||||||
|
BACKUP_DIR=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$KEEP_DATA" = "0" ] && [ -n "$DATA_VOLUME" ]; then
|
||||||
|
if [ "$NON_INTERACTIVE" = "0" ]; then
|
||||||
|
_default_backup="${HOME}/open-design-backup-$(date +%Y%m%d%H%M%S)"
|
||||||
|
prompt_text "Backup destination" "$_default_backup"
|
||||||
|
BACKUP_DIR="$PROMPT_RESULT"
|
||||||
|
_do_backup "$BACKUP_DIR"
|
||||||
|
else
|
||||||
|
BACKUP_DIR="${HOME}/open-design-backup-$(date +%Y%m%d%H%M%S)"
|
||||||
|
_do_backup "$BACKUP_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Confirm destructive action
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "$NON_INTERACTIVE" = "0" ]; then
|
||||||
|
printf "\n"
|
||||||
|
warn "Everything will now be removed: containers, data volume, and config."
|
||||||
|
printf " Continue? [y/N]: "
|
||||||
|
read -r _confirm
|
||||||
|
case "$_confirm" in
|
||||||
|
[Yy]*) ;;
|
||||||
|
*) step "Cancelled."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stop and remove containers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if $COMPOSE_CMD "${COMPOSE_FILES[@]}" ps -q 2>/dev/null | grep -q .; then
|
||||||
|
step "Stopping containers..."
|
||||||
|
$COMPOSE_CMD "${COMPOSE_FILES[@]}" down
|
||||||
|
ok "Containers stopped."
|
||||||
|
else
|
||||||
|
# Try removing stopped container directly
|
||||||
|
if $RUNTIME inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||||
|
step "Removing stopped container..."
|
||||||
|
$RUNTIME rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
step "No containers found."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove data volume (unless --keep-data)
|
||||||
|
if [ "$KEEP_DATA" = "0" ] && [ -n "$DATA_VOLUME" ]; then
|
||||||
|
step "Removing data volume ${DATA_VOLUME}..."
|
||||||
|
$RUNTIME volume rm "$DATA_VOLUME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove systemd unit (Linux)
|
||||||
|
SYSTEMD_UNIT="${HOME}/.config/systemd/user/open-design.service"
|
||||||
|
if [ -f "$SYSTEMD_UNIT" ]; then
|
||||||
|
step "Removing systemd unit..."
|
||||||
|
systemctl --user disable --now open-design 2>/dev/null || true
|
||||||
|
rm -f "$SYSTEMD_UNIT"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
ok "systemd unit removed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove .env
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
step "Removing ${ENV_FILE}..."
|
||||||
|
rm -f "$ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
printf "${BOLD}${GREEN} ── Uninstall Complete ────────────────────────────${RESET}\n"
|
||||||
|
printf "\n"
|
||||||
|
if [ "$KEEP_DATA" = "1" ]; then
|
||||||
|
info "Data volume was preserved."
|
||||||
|
step "Remove it manually: $RUNTIME volume rm $DATA_VOLUME"
|
||||||
|
elif [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
|
||||||
|
info "Your data was backed up to: ${BACKUP_DIR}"
|
||||||
|
step "To restore, copy contents into a new deployment's data volume."
|
||||||
|
fi
|
||||||
|
printf "\n"
|
||||||
141
deploy/scripts/update.sh
Executable file
141
deploy/scripts/update.sh
Executable file
|
|
@ -0,0 +1,141 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Open Design — Updater
|
||||||
|
# Pulls the latest image and restarts the service
|
||||||
|
#
|
||||||
|
# Usage: ./update.sh [--image <ref>] [--non-interactive]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
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"
|
||||||
|
HEALTH_TIMEOUT=60
|
||||||
|
|
||||||
|
COMPOSE_FILES=(-f "$COMPOSE_FILE")
|
||||||
|
if [ -f "$OVERRIDE_FILE" ]; then
|
||||||
|
COMPOSE_FILES+=(-f "$OVERRIDE_FILE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Detect container runtime
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
COMPOSE_CMD=""
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
elif command -v podman >/dev/null 2>&1 && podman compose version >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="podman compose"
|
||||||
|
elif command -v podman >/dev/null 2>&1 && command -v podman-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="podman-compose"
|
||||||
|
elif command -v docker >/dev/null 2>&1 && command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
else
|
||||||
|
error "No container runtime found. Install Docker or Podman."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OPT_IMAGE=""
|
||||||
|
NON_INTERACTIVE=0
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--image) shift; OPT_IMAGE="$1" ;;
|
||||||
|
--image=*) OPT_IMAGE="${1#--image=}" ;;
|
||||||
|
--non-interactive) NON_INTERACTIVE=1 ;;
|
||||||
|
--help|-h)
|
||||||
|
echo "Usage: update.sh [options]"
|
||||||
|
echo " --image <ref> Pull a specific image instead of latest"
|
||||||
|
echo " --non-interactive Skip confirmation prompts"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
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}Updater${RESET} ${BOLD}│${RESET}\n"
|
||||||
|
printf "${BOLD} │${RESET} ${BOLD}│${RESET}\n"
|
||||||
|
printf "${BOLD} └──────────────────────────────────────┘${RESET}\n"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
# Read current port from .env if it exists
|
||||||
|
PORT=7456
|
||||||
|
ENV_FILE="${DEPLOY_DIR}/.env"
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
_port="$(grep '^OPEN_DESIGN_PORT=' "$ENV_FILE" | cut -d= -f2)"
|
||||||
|
if [ -n "$_port" ]; then PORT="$_port"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Override image if specified
|
||||||
|
if [ -n "$OPT_IMAGE" ]; then
|
||||||
|
export OPEN_DESIGN_IMAGE="$OPT_IMAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull latest image
|
||||||
|
step "Pulling latest image..."
|
||||||
|
$COMPOSE_CMD "${COMPOSE_FILES[@]}" pull
|
||||||
|
|
||||||
|
# Restart with new image
|
||||||
|
step "Restarting service..."
|
||||||
|
$COMPOSE_CMD "${COMPOSE_FILES[@]}" up -d --no-build
|
||||||
|
|
||||||
|
# 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 "Update complete. Daemon is healthy."
|
||||||
|
else
|
||||||
|
warn "Health check did not pass."
|
||||||
|
step "Check logs: ${COMPOSE_CMD} \"${COMPOSE_FILES[@]}\" logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up dangling images
|
||||||
|
step "Cleaning up old images..."
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
${COMPOSE_CMD%% *}$ image prune -f >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
printf "${BOLD}${GREEN} ── Update Complete ───────────────────────────────${RESET}\n"
|
||||||
|
printf "\n"
|
||||||
|
printf " URL: http://127.0.0.1:%s\n" "$PORT"
|
||||||
|
printf "\n"
|
||||||
222
deploy/tests/install.test.ts
Normal file
222
deploy/tests/install.test.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { execFile } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
const repoRoot = join(import.meta.dirname, '../..');
|
||||||
|
const installScript = join(repoRoot, 'deploy/scripts/install.sh');
|
||||||
|
const uninstallScript = join(repoRoot, 'deploy/scripts/uninstall.sh');
|
||||||
|
const updateScript = join(repoRoot, 'deploy/scripts/update.sh');
|
||||||
|
|
||||||
|
// Skip entire suite if Docker is not available
|
||||||
|
function isDockerAvailable(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
execFile('docker', ['info'], { timeout: 5000 }, (err) => resolve(!err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const dockerAvailable = await isDockerAvailable();
|
||||||
|
|
||||||
|
// Unique test identifier to isolate from real deployments
|
||||||
|
const TEST_ID = `od-test-${process.pid}`;
|
||||||
|
|
||||||
|
async function waitForHealth(port: number, timeoutMs = 30000): Promise<boolean> {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`http://127.0.0.1:${port}/api/health`);
|
||||||
|
if (resp.ok) return true;
|
||||||
|
} catch {}
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestContext {
|
||||||
|
tmpDir: string;
|
||||||
|
port: number;
|
||||||
|
projectName: string;
|
||||||
|
containerName: string;
|
||||||
|
volumeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupTestDir(port: number): Promise<TestContext> {
|
||||||
|
const projectName = `${TEST_ID}-${port}`;
|
||||||
|
const containerName = projectName;
|
||||||
|
const volumeName = `${projectName}-data`;
|
||||||
|
|
||||||
|
const tmpDir = await mkdtemp(join(tmpdir(), `${TEST_ID}-`));
|
||||||
|
await execFileAsync('cp', ['-r', join(repoRoot, 'deploy/.'), tmpDir]);
|
||||||
|
|
||||||
|
// Write a compose override that replaces the hardcoded names
|
||||||
|
const override = {
|
||||||
|
name: projectName,
|
||||||
|
services: {
|
||||||
|
'open-design': {
|
||||||
|
container_name: containerName,
|
||||||
|
volumes: [`${volumeName}:/app/.od`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
[volumeName]: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await writeFile(
|
||||||
|
join(tmpDir, 'docker-compose.override.yml'),
|
||||||
|
JSON.stringify(override),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { tmpDir, port, projectName, containerName, volumeName };
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEnv(ctx: TestContext): Record<string, string> {
|
||||||
|
return {
|
||||||
|
...process.env as Record<string, string>,
|
||||||
|
COMPOSE_PROJECT_NAME: ctx.projectName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function teardownTestDir(ctx: TestContext): Promise<void> {
|
||||||
|
const script = join(ctx.tmpDir, 'scripts/uninstall.sh');
|
||||||
|
const override = join(ctx.tmpDir, 'docker-compose.override.yml');
|
||||||
|
|
||||||
|
// Run uninstall with the same override file so it targets the test container
|
||||||
|
try {
|
||||||
|
await readFile(override);
|
||||||
|
await execFileAsync('bash', [script, '--non-interactive'], {
|
||||||
|
timeout: 60_000,
|
||||||
|
env: testEnv(ctx),
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Force-remove the named volume as a safety net
|
||||||
|
try {
|
||||||
|
await execFileAsync('docker', ['volume', 'rm', '-f', ctx.volumeName], {
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (ctx.tmpDir) await rm(ctx.tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// help flag tests — do not require Docker
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
test('install.sh --help exits 0', async () => {
|
||||||
|
const { stdout } = await execFileAsync('bash', [installScript, '--help']);
|
||||||
|
assert.match(stdout, /Usage/);
|
||||||
|
assert.match(stdout, /--non-interactive/);
|
||||||
|
assert.match(stdout, /--port/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uninstall.sh --help exits 0', async () => {
|
||||||
|
const { stdout } = await execFileAsync('bash', [uninstallScript, '--help']);
|
||||||
|
assert.match(stdout, /Usage/);
|
||||||
|
assert.match(stdout, /--keep-data/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update.sh --help exits 0', async () => {
|
||||||
|
const { stdout } = await execFileAsync('bash', [updateScript, '--help']);
|
||||||
|
assert.match(stdout, /Usage/);
|
||||||
|
assert.match(stdout, /--image/);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Docker integration tests — skipped when Docker is unavailable
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
test('install.sh --non-interactive creates .env and starts container', { skip: !dockerAvailable ? 'Docker not available' : false }, async () => {
|
||||||
|
const ctx = await setupTestDir(17456);
|
||||||
|
try {
|
||||||
|
const script = join(ctx.tmpDir, 'scripts/install.sh');
|
||||||
|
await execFileAsync('bash', [
|
||||||
|
script,
|
||||||
|
'--non-interactive',
|
||||||
|
`--port=${ctx.port}`,
|
||||||
|
'--no-systemd',
|
||||||
|
], {
|
||||||
|
timeout: 120_000,
|
||||||
|
env: testEnv(ctx),
|
||||||
|
});
|
||||||
|
|
||||||
|
// .env should contain the port
|
||||||
|
const envContent = await readFile(join(ctx.tmpDir, '.env'), 'utf8');
|
||||||
|
assert.match(envContent, new RegExp(`OPEN_DESIGN_PORT=${ctx.port}`));
|
||||||
|
|
||||||
|
// Container should be healthy
|
||||||
|
const healthy = await waitForHealth(ctx.port, 60_000);
|
||||||
|
assert.ok(healthy, 'daemon did not become healthy within 60s');
|
||||||
|
} finally {
|
||||||
|
await teardownTestDir(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update.sh restarts service and remains healthy', { skip: !dockerAvailable ? 'Docker not available' : false }, async () => {
|
||||||
|
const ctx = await setupTestDir(17457);
|
||||||
|
try {
|
||||||
|
const installRun = join(ctx.tmpDir, 'scripts/install.sh');
|
||||||
|
await execFileAsync('bash', [
|
||||||
|
installRun,
|
||||||
|
'--non-interactive',
|
||||||
|
`--port=${ctx.port}`,
|
||||||
|
'--no-systemd',
|
||||||
|
], {
|
||||||
|
timeout: 120_000,
|
||||||
|
env: testEnv(ctx),
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForHealth(ctx.port, 30_000);
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await execFileAsync('bash', [
|
||||||
|
join(ctx.tmpDir, 'scripts/update.sh'),
|
||||||
|
], {
|
||||||
|
timeout: 120_000,
|
||||||
|
cwd: ctx.tmpDir,
|
||||||
|
env: testEnv(ctx),
|
||||||
|
});
|
||||||
|
|
||||||
|
const healthy = await waitForHealth(ctx.port, 30_000);
|
||||||
|
assert.ok(healthy, 'daemon not healthy after update');
|
||||||
|
} finally {
|
||||||
|
await teardownTestDir(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uninstall.sh removes containers and .env', { skip: !dockerAvailable ? 'Docker not available' : false }, async () => {
|
||||||
|
const ctx = await setupTestDir(17458);
|
||||||
|
try {
|
||||||
|
const installRun = join(ctx.tmpDir, 'scripts/install.sh');
|
||||||
|
await execFileAsync('bash', [
|
||||||
|
installRun,
|
||||||
|
'--non-interactive',
|
||||||
|
`--port=${ctx.port}`,
|
||||||
|
'--no-systemd',
|
||||||
|
], {
|
||||||
|
timeout: 120_000,
|
||||||
|
env: testEnv(ctx),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Uninstall
|
||||||
|
await execFileAsync('bash', [
|
||||||
|
join(ctx.tmpDir, 'scripts/uninstall.sh'),
|
||||||
|
'--non-interactive',
|
||||||
|
], {
|
||||||
|
timeout: 60_000,
|
||||||
|
env: testEnv(ctx),
|
||||||
|
});
|
||||||
|
|
||||||
|
// .env should be gone
|
||||||
|
const envGone = await readFile(join(ctx.tmpDir, '.env'), 'utf8').catch(() => null);
|
||||||
|
assert.equal(envGone, null, '.env should have been removed');
|
||||||
|
|
||||||
|
// Container should not be running
|
||||||
|
const { stdout: containers } = await execFileAsync('docker', ['ps', '--format', '{{.Names}}']);
|
||||||
|
assert.ok(!containers.includes(ctx.containerName), 'container should not be running after uninstall');
|
||||||
|
} finally {
|
||||||
|
await teardownTestDir(ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
202
docs/install-guide.md
Normal file
202
docs/install-guide.md
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
# One-Click Install Guide
|
||||||
|
|
||||||
|
**Parent:** [`spec.md`](spec.md) · **Siblings:** [`self-hosting.md`](self-hosting.md) · [`network-security.md`](network-security.md)
|
||||||
|
|
||||||
|
Deploy Open Design on Linux or macOS with a single command. The installer wraps the existing Docker Compose stack — no build step required.
|
||||||
|
|
||||||
|
## Quick reference
|
||||||
|
|
||||||
|
Clone the repository and run the installer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/nexu-io/open-design.git
|
||||||
|
cd open-design
|
||||||
|
bash deploy/scripts/install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
The only requirement is Docker with the Compose plugin.
|
||||||
|
|
||||||
|
| Platform | Minimum version | Install |
|
||||||
|
|----------|----------------|---------|
|
||||||
|
| Docker Engine | 24.0 | [docs.docker.com/engine/install](https://docs.docker.com/engine/install/) |
|
||||||
|
| Docker Compose plugin | 2.20 | Bundled with Docker Desktop; `apt install docker-compose-plugin` on Linux |
|
||||||
|
| Docker Desktop (macOS/Windows) | 4.25 | [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/) |
|
||||||
|
|
||||||
|
The installer checks for Docker and offers to install it automatically on Ubuntu/Debian, Fedora, and macOS (via Homebrew). Use `--skip-docker-install` to skip this step.
|
||||||
|
|
||||||
|
> **MCP note:** Docker/Compose installs run the daemon inside the container. The MCP client snippets shown in Settings are stdio/local-path based and require a local/source install for now. Container-friendly MCP transport will be added in a follow-up.
|
||||||
|
|
||||||
|
## Interactive install walkthrough
|
||||||
|
|
||||||
|
Running the installer without flags launches an interactive wizard:
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════╗
|
||||||
|
║ O P E N D E S I G N ║
|
||||||
|
║ One-Click Installer ║
|
||||||
|
╚══════════════════════════════════════╝
|
||||||
|
|
||||||
|
[open-design] OS: Linux ubuntu 24.04 (x86_64)
|
||||||
|
[open-design] Docker: Docker version 26.1.3, build b72abbb
|
||||||
|
[open-design] Compose: Docker Compose version v2.27.1
|
||||||
|
|
||||||
|
Docker image [docker.io/vanjayak/open-design:latest]:
|
||||||
|
Port [7456]:
|
||||||
|
Allowed origins (CORS, comma-separated, or empty) []:
|
||||||
|
Memory limit [384m]:
|
||||||
|
|
||||||
|
[open-design] Pulling image: docker.io/vanjayak/open-design:latest
|
||||||
|
[open-design] Starting Open Design...
|
||||||
|
[open-design] Waiting for health check (up to 60s)...
|
||||||
|
[open-design] Daemon is healthy (200 OK)
|
||||||
|
```
|
||||||
|
|
||||||
|
### What each prompt does
|
||||||
|
|
||||||
|
| Prompt | Default | Notes |
|
||||||
|
|--------|---------|-------|
|
||||||
|
| **Docker image** | `docker.io/vanjayak/open-design:latest` | Pin a digest for reproducibility: `docker.io/vanjayak/open-design@sha256:<digest>` |
|
||||||
|
| **Port** | `7456` | The port the daemon listens on. Must not be in use. |
|
||||||
|
| **Allowed origins** | _(empty)_ | CORS origins for reverse-proxy setups. See [`network-security.md`](network-security.md). Leave empty for localhost-only use. |
|
||||||
|
| **Memory limit** | `384m` | Container memory cap. Raise for large concurrent agent runs. |
|
||||||
|
|
||||||
|
After you confirm, the installer:
|
||||||
|
|
||||||
|
1. Writes a `deploy/.env` file (backs up any existing one).
|
||||||
|
2. Runs `docker compose pull` to fetch the image.
|
||||||
|
3. Runs `docker compose up -d --no-build` to start the container.
|
||||||
|
4. Polls `/api/health` for up to 60 seconds to confirm the daemon is ready.
|
||||||
|
5. On Linux: installs a `systemd --user` unit so the service starts on login.
|
||||||
|
|
||||||
|
## Non-interactive install
|
||||||
|
|
||||||
|
For CI, headless servers, and automated provisioning:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/scripts/install.sh --non-interactive [--port 7456] [--image <ref>] [--no-systemd]
|
||||||
|
```
|
||||||
|
|
||||||
|
All prompts are skipped and defaults are used. If Docker is not installed, the script exits with an error instead of offering to install it.
|
||||||
|
|
||||||
|
### All flags
|
||||||
|
|
||||||
|
**`install.sh`**
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--non-interactive` | Skip all prompts |
|
||||||
|
| `--port <n>` | Host port (default: `7456`) |
|
||||||
|
| `--image <ref>` | Docker image reference |
|
||||||
|
| `--skip-docker-install` | Never attempt to install Docker |
|
||||||
|
| `--no-systemd` | Skip systemd unit creation |
|
||||||
|
|
||||||
|
## Service management
|
||||||
|
|
||||||
|
### Linux (systemd)
|
||||||
|
|
||||||
|
The installer creates a `systemd --user` unit that wraps Docker Compose. No `sudo` required.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check status
|
||||||
|
systemctl --user status open-design
|
||||||
|
|
||||||
|
# Start / stop / restart
|
||||||
|
systemctl --user start open-design
|
||||||
|
systemctl --user stop open-design
|
||||||
|
systemctl --user restart open-design
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl --user -u open-design -f
|
||||||
|
|
||||||
|
# Disable auto-start
|
||||||
|
systemctl --user disable open-design
|
||||||
|
|
||||||
|
# Re-enable auto-start
|
||||||
|
systemctl --user enable open-design
|
||||||
|
```
|
||||||
|
|
||||||
|
To skip systemd unit creation, pass `--no-systemd` to the installer.
|
||||||
|
|
||||||
|
### macOS (Docker Desktop)
|
||||||
|
|
||||||
|
Docker Desktop manages the container lifecycle. Use Docker Desktop's dashboard to start, stop, or restart the `open-design` container, or use the CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using docker compose directly
|
||||||
|
docker compose -f deploy/docker-compose.yml start
|
||||||
|
docker compose -f deploy/docker-compose.yml stop
|
||||||
|
docker compose -f deploy/docker-compose.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
Pull the latest image and restart with a single command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/scripts/update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
To update to a specific image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/scripts/update.sh --image=docker.io/vanjayak/open-design@sha256:<digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
The update script:
|
||||||
|
1. Pulls the new image.
|
||||||
|
2. Restarts the container with `docker compose up -d --no-build`.
|
||||||
|
3. Waits for `/api/health` to return 200.
|
||||||
|
4. Prunes dangling old images.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove containers and data
|
||||||
|
bash deploy/scripts/uninstall.sh
|
||||||
|
|
||||||
|
# Remove containers but keep data volume
|
||||||
|
bash deploy/scripts/uninstall.sh --keep-data
|
||||||
|
```
|
||||||
|
|
||||||
|
The uninstaller:
|
||||||
|
1. Stops and removes containers (`docker compose down`), then removes the data volume separately.
|
||||||
|
2. On Linux: disables and removes the systemd unit.
|
||||||
|
3. Removes `deploy/.env`.
|
||||||
|
|
||||||
|
> **Data:** By default, the `open_design_data` volume (projects, artifacts, config) is also deleted. Pass `--keep-data` to preserve it. Remove the volume manually later: `docker volume rm open_design_data`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All settings live in `deploy/.env`. Edit it directly or re-run the installer to regenerate it.
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `OPEN_DESIGN_IMAGE` | `docker.io/vanjayak/open-design:latest` | Full image reference |
|
||||||
|
| `OPEN_DESIGN_PORT` | `7456` | Host-side port (bound to `127.0.0.1`) |
|
||||||
|
| `OPEN_DESIGN_ALLOWED_ORIGINS` | _(empty)_ | CORS origins for reverse-proxy setups |
|
||||||
|
| `OPEN_DESIGN_MEM_LIMIT` | `384m` | Container memory cap |
|
||||||
|
| `NODE_OPTIONS` | `--max-old-space-size=192` | Node.js heap cap inside the container |
|
||||||
|
|
||||||
|
The container always binds `127.0.0.1:<port>:7456` — the daemon is never directly exposed to the network. To allow remote access, put an authenticated reverse proxy in front. See [`network-security.md`](network-security.md).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Problem | Likely cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| `Docker is not installed` | Docker not on PATH | Install Docker Desktop or Docker Engine |
|
||||||
|
| `Docker daemon is not running` | Docker Desktop not started | Open Docker Desktop or run `sudo systemctl start docker` |
|
||||||
|
| `Port 7456 is already in use` | Another service on that port | Re-run with `--port 8080` |
|
||||||
|
| Health check times out | Image pull slow or daemon slow to start | Wait and check `docker compose -f deploy/docker-compose.yml logs` |
|
||||||
|
| `Permission denied` on install.sh | Script not executable | Run `chmod +x deploy/scripts/install.sh` |
|
||||||
|
| systemd unit not created | `systemd` not found | Omit `--no-systemd` if systemd is available, or manage via Docker CLI |
|
||||||
|
| `.env` has wrong port after re-install | Old backup not restored | Edit `deploy/.env` directly or delete it and re-run |
|
||||||
|
| Container exits immediately | Image incompatibility | Check `docker compose -f deploy/docker-compose.yml logs` for errors |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Docker Compose config: [`deploy/docker-compose.yml`](../deploy/docker-compose.yml)
|
||||||
|
- Environment template: [`deploy/.env.example`](../deploy/.env.example)
|
||||||
|
- Self-hosting topologies (PM2, systemd native): [`docs/self-hosting.md`](self-hosting.md)
|
||||||
|
- Network security and remote access: [`docs/network-security.md`](network-security.md)
|
||||||
Loading…
Reference in a new issue