#!/usr/bin/env bash # # nginx-webui — interactive installer. # # Offers two deployment modes: # 1. Docker + host control agent : web-UI container managing nginx installed # on this machine (status/check/reload/restart via the host agent) # 2. Manual (all on host) : nginx and the web UI installed directly # on this machine (systemd + venv) # # Usage: sudo ./install.sh (or: ./install.sh --check | --help) # One-liner: curl -fsSL https://raw.githubusercontent.com/himalsimkhada/nginx-webui/main/install.sh | bash # Headless: NGINX_MODE=1 WEBUI_PASSWORD=x TARGET_DIR=~/nginx-webui curl -fsSL ... | bash set -euo pipefail REPO_URL="https://github.com/himalsimkhada/nginx-webui.git" WEBUI_REPO_URL="https://github.com/himalsimkhada/webui.git" WEBUI_PORT="${WEBUI_PORT:-8080}" DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DIR" # ── Output helpers ─────────────────────────────────────────────────────── if [ -t 1 ]; then C_RESET=$'\e[0m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'; C_RED=$'\e[31m'; C_BOLD=$'\e[1m' else C_RESET=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_BOLD="" fi info() { printf '%s\n' "${C_BOLD}==>${C_RESET} $*"; } ok() { printf '%s%s%s\n' "${C_GREEN} $*${C_RESET}"; } warn() { printf '%s%s%s\n' "${C_YELLOW}!! $*${C_RESET}"; } die() { printf '%s%s%s\n' "${C_RED}FATAL:$*${C_RESET}" >&2; exit 1; } has_cmd() { command -v "$1" >/dev/null 2>&1; } # ── Input helpers ──────────────────────────────────────────────────────── # Under `curl ... | bash` stdin is the script stream, and a nested run # inherits that consumed stream, so bare `read` gets EOF instantly. Read from # the controlling terminal when one exists; headless runs use env vars. have_tty() { ( exec /dev/null; } read_input() { local var="$1" prompt="${2:-}" local envval="" case "$var" in target) envval="${TARGET_DIR:-}";; choice) envval="${NGINX_MODE:-}";; ans) envval="${NGINX_YES:-}";; esac if [ -n "$envval" ]; then printf -v "$var" '%s' "$envval" return 0 fi if have_tty; then read -r -p "$prompt" "$var" < /dev/tty else die "No terminal available and \$$var was not set (${prompt%:}). Re-run from a terminal or set the env var." fi } read_input_silent() { local var="$1" prompt="${2:-}" if have_tty; then read -r -s -p "$prompt" "$var" < /dev/tty else die "No terminal available for password input. Set WEBUI_PASSWORD=... and re-run." fi echo "" } # ── Self-bootstrap ──────────────────────────────────────────────────────── # Support one-liner installs (curl ... | bash): when the script is streamed # there is no repo checkout in $DIR, so fetch the repository first and then # re-run this installer from inside it (finishes with the user's chosen mode). if [ ! -f "$DIR/app.py" ] || [ ! -f "$DIR/docker-compose.yml" ]; then echo "==> One-liner install: no repo checkout in \"$DIR\"." has_cmd git || die "git is required for the one-liner install (curl | bash)." has_cmd curl || has_cmd wget || warn "Neither curl nor wget found; check the URL you piped." default_target="$HOME/nginx-webui" target="" read_input target "Install the project into [$default_target]: " || true target="${target:-$default_target}" mkdir -p "$(dirname "$target")" if [ -d "$target" ] && [ -f "$target/app.py" ]; then info "Updating existing checkout at $target" (cd "$target" && git pull --ff-only) >/dev/null 2>&1 || true else info "Cloning $REPO_URL into $target" git clone --quiet --depth 1 "$REPO_URL" "$target" fi cd "$target" exec bash "$target/install.sh" "$@" fi # ── System detection ───────────────────────────────────────────────────── OS_ID="unknown" OS_NAME="unknown" if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-unknown}" OS_NAME="${NAME:-$OS_ID}" fi is_deb() { case "$OS_ID" in debian|ubuntu|linuxmint|pop|elementary|kali|raspbian) return 0;; *) return 1;; esac; } is_rpm() { case "$OS_ID" in rhel|fedora|centos|almalinux|rocky|ol|amazon) return 0;; *) return 1;; esac; } is_arch() { case "$OS_ID" in arch|manjaro|endeavouros) return 0;; *) return 1;; esac; } pkg_install() { if is_deb; then sudo apt-get update -qq sudo apt-get install -y -qq "$@" elif is_rpm; then if command -v dnf >/dev/null 2>&1; then sudo dnf install -y "$@" else sudo yum install -y "$@"; fi elif is_arch; then sudo pacman -S --noconfirm --needed "$@" else die "Unsupported distro ($OS_NAME). Please install dependencies manually." fi } # ── Generic helpers ────────────────────────────────────────────────────── random_secret() { if has_cmd openssl; then openssl rand -hex 32 else od -An -N32 -tx1 /dev/urandom | tr -d ' \n' fi } ask_password() { # Accepts WEBUI_PASSWORD from the environment (headless runs) or prompts. if [ -z "${WEBUI_PASSWORD:-}" ]; then WEBUI_PASSWORD="" while [ -z "$WEBUI_PASSWORD" ]; do read_input_silent WEBUI_PASSWORD " Web UI password (used to log in): " if [ -z "$WEBUI_PASSWORD" ]; then warn "Password cannot be empty. Leaving it blank is not supported." else read_input_silent WEBUI_PASSWORD_CONFIRM " Confirm password: " if [ "$WEBUI_PASSWORD" != "$WEBUI_PASSWORD_CONFIRM" ]; then warn "Passwords do not match. Try again." WEBUI_PASSWORD="" fi fi done else ok "Using WEBUI_PASSWORD from the environment" fi SECRET_KEY="$(random_secret)" } write_env_file() { # Writes .env from an array of KEY=VALUE lines passed on stdin. local envpath="$DIR/.env" info "Writing $envpath" cat > "$envpath" chmod 600 "$envpath" 2>/dev/null || true } # ── webui admin facade (companion dashboard) ──────────────────────────── webui_detected() { # True when the webui admin facade is already answering on the expected port. if has_cmd curl && curl -fsS --max-time 3 "http://127.0.0.1:${WEBUI_PORT}/healthz" >/dev/null 2>&1; then return 0 fi if has_cmd docker && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^webui-admin$'; then return 0 fi return 1 } offer_webui_portal() { # The dashboard UI lives in the companion webui facade. If it is not running, # offer to install it now. local tgt="$HOME/webui" if webui_detected; then ok "webui admin dashboard already running on http://127.0.0.1:${WEBUI_PORT}" return fi if [ -d "$tgt" ] && [ -f "$tgt/install.sh" ]; then warn "The webui admin dashboard is NOT running but a checkout exists at $tgt (port $WEBUI_PORT)." else warn "The webui admin dashboard (himalsimkhada/webui) was not detected on http://127.0.0.1:${WEBUI_PORT}." fi info "The nginx dashboard is hosted by the webui facade; without it you only get the API." read_input ans " Install the webui admin dashboard now? [Y/n] " if [ "${ans:-y}" = "n" ] || [ "${ans:-y}" = "N" ]; then warn "Install it later with: curl -fsSL https://raw.githubusercontent.com/himalsimkhada/webui/main/install.sh | bash" return fi has_cmd git || die "git is required to clone the webui dashboard." mkdir -p "$HOME" if [ ! -f "$tgt/install.sh" ]; then info "Cloning $WEBUI_REPO_URL into $tgt" git clone --quiet --depth 1 "$WEBUI_REPO_URL" "$tgt" fi info "Running the webui dashboard installer (choose Docker or Manual mode)" bash "$tgt/install.sh" ok "webui admin dashboard installed. Open http://localhost:${WEBUI_PORT}" } # ── nginx + host control agent ─────────────────────────────────────────── nginx_packages() { if is_deb; then echo "nginx" elif is_rpm; then echo "nginx" elif is_arch; then echo "nginx" fi } ensure_host_nginx() { # The managed nginx must live on this machine (the docker container edits # the mounted /etc/nginx, and the agent signals its master). local ngconf="/etc/nginx/nginx.conf" if ! has_cmd nginx; then warn "nginx is not installed." read_input ans " Install nginx now? [y/N] " if [ "${ans:-n}" != "y" ] && [ "${ans:-n}" != "Y" ]; then die "nginx is required. Re-run after installing nginx." fi # shellcheck disable=SC2046 pkg_install $(nginx_packages) fi has_cmd nginx || die "nginx is required" if [ ! -f "$ngconf" ]; then die "Expected nginx config at $ngconf but it is missing." fi ok "nginx present ($ngconf)" } ensure_host_agent() { info "Installing the host control agent so the container can manage" info "the HOST nginx (PID namespaces block direct signals; see README)." sudo cp "$DIR/host_agent.py" /usr/local/bin/nginx-webui-host-agent.py sudo chmod 755 /usr/local/bin/nginx-webui-host-agent.py sudo cp "$DIR/nginx-webui-host-agent.service" /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now nginx-webui-host-agent >/dev/null 2>&1 sleep 1 if curl -fsS http://127.0.0.1:9401/status >/dev/null 2>&1; then ok "Host control agent active on :9401 (binds 0.0.0.0 for containers)" else warn "Agent installed but not answering yet. Check: sudo systemctl status nginx-webui-host-agent" fi } ensure_docker() { if ! has_cmd docker || ! docker compose version >/dev/null 2>&1; then warn "Docker with the compose plugin is required but not installed." read_input ans " Install Docker now? [y/N] " ans if [ "${ans:-n}" != "y" ] && [ "${ans:-n}" != "Y" ]; then die "Docker is required for this mode. Re-run after installing Docker." fi info "Installing Docker" if is_deb; then sudo apt-get update -qq sudo apt-get install -y -qq docker.io docker-compose-v2 elif is_rpm; then sudo dnf install -y moby-engine docker-compose-plugin 2>/dev/null \ || sudo dnf install -y moby-engine || \ warn "Could not auto-install Docker. Install it manually and re-run." elif is_arch; then sudo pacman -S --noconfirm --needed docker docker-compose 2>/dev/null \ || sudo pacman -S --noconfirm --needed docker else die "Unsupported distro for automatic Docker install. Install Docker manually." fi fi if ! docker compose version >/dev/null 2>&1; then die "Docker compose plugin is missing. Install docker-compose and re-run." fi if ! docker info >/dev/null 2>&1; then warn "Docker daemon is not reachable. Starting the docker service..." sudo systemctl enable --now docker >/dev/null 2>&1 || true sleep 2 fi docker info >/dev/null 2>&1 \ || die "Docker daemon is not reachable. Start it (sudo systemctl start docker) and re-run." ok "Docker + compose plugin available and the daemon is running" } ensure_python_tools() { if is_deb; then pkg_install python3 python3-venv elif is_rpm; then pkg_install python3 python3-pip elif is_arch; then pkg_install python python-virtualenv fi has_cmd python3 || die "python3 is required" } # ── Mode 1: Docker + host control agent ────────────────────────────────── mode_docker() { info "Mode 1: Docker + host control agent (web UI container, host nginx)" ensure_docker ensure_host_nginx ensure_host_agent ask_password write_env_file </dev/null </dev/null sudo systemctl daemon-reload sudo systemctl enable nginx-webui sudo systemctl restart nginx-webui ok "Deployed. Open http://localhost:8400" ok "Manage with: sudo systemctl status nginx-webui" offer_webui_portal } # ── Main menu / flags ──────────────────────────────────────────────────── show_menu() { echo "" echo "Select how you want to run the nginx Web UI:" echo "" echo " 1) Docker + host agent - web UI container managing nginx on THIS machine (recommended)" echo " 2) Manual - nginx and the web UI both installed directly on THIS machine" echo "" while :; do read_input choice "Enter your choice [1-2]: " case "$choice" in 1) mode_docker; return;; 2) mode_manual; return;; *) warn "Please choose 1 or 2.";; esac done } do_check() { echo "── System check ─────────────────────────────" echo "Distro : $OS_NAME ($OS_ID)" echo "Package tool : $(is_deb && echo 'apt' || (is_rpm && echo 'rpm/dnf' || (is_arch && echo 'pacman' || echo 'unknown')))" echo "nginx conf : /etc/nginx/nginx.conf" echo "" if has_cmd nginx; then echo "nginx : installed ($(nginx -v 2>&1 | sed 's/^nginx version: //'))" else echo "nginx : NOT installed" fi if curl -fsS http://127.0.0.1:9401/status >/dev/null 2>&1; then echo "host agent : active on 127.0.0.1:9401" else echo "host agent : not running" fi if has_cmd docker && docker compose version >/dev/null 2>&1; then echo "Docker+compose: available" else echo "Docker+compose: missing" fi if has_cmd python3; then echo "python3 : $(command -v python3)" else echo "python3 : missing" fi echo "" } case "${1:-}" in --check|-c) do_check; exit 0;; --help|-h) sed -n '1,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0;; "") if [ "$(id -u)" -eq 0 ]; then warn "Running as root. Prefer running as a normal sudo user on some distros." fi show_menu echo "" echo "Done!" ;; *) echo "Unknown option: $1 (use --help)"; exit 1;; esac