#!/bin/bash # This script installs wg-ui from the git repo # and sets up the appropriate system process # manager (systemd or whichever) for the # appropriate OS. The preferred directory for # assets is /var/lib/wgui/webapp set -euo pipefail WGUI_LIB_DIR="/var/lib/wgui" INSTALL_DIR="${INSTALL_DIR:-$WGUI_LIB_DIR/webapp}" SERVICE_USER="${SERVICE_USER:-}" SERVICE_GROUP="${SERVICE_GROUP:-}" SERVICE_NAME="${SERVICE_NAME:-wgui}" REPO_URL="${REPO_URL:-}" BRANCH="${BRANCH:-main}" BUN_INSTALL_DIR="${BUN_INSTALL_DIR:-/opt/bun}" BUN_BIN="/usr/local/bin/bun" UPDATE=false DEV_MODE=false if [ "${NODE_ENV:-}" = "development" ]; then DEV_MODE=true log "NODE_ENV=development — skipping system service installation (daemon is assumed to already be running)" fi log() { echo "==> $*"; } fail() { echo "error: $*" >&2; exit 1; } require_root() { if [ "$(id -u)" -ne 0 ]; then if ! command -v sudo >/dev/null 2>&1; then fail "this script must be run as root, and sudo is not installed" fi log "not running as root — re-executing with sudo ..." sudo -v || fail "current user does not have sudo privileges — run this script as root or grant the current user sudo access" exec sudo -E "$0" "$@" fi } resolve_service_user() { if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && id -u "$SUDO_USER" >/dev/null 2>&1; then echo "$SUDO_USER" else echo "root" fi } require_root SERVICE_USER="${SERVICE_USER:-$(resolve_service_user)}" SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}" log "running the wg-ui service as $SERVICE_USER (group: $SERVICE_GROUP)" if [ -z "$REPO_URL" ]; then fail "REPO_URL is not set — pass the git URL of the wg-ui repo, e.g. REPO_URL=https://git.example.com/org/wireguard-ui.git $0" fi detect_distro() { if [ -f /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release echo "$ID" else echo "unknown" fi } detect_init_system() { if command -v systemctl >/dev/null 2>&1; then echo "systemd" elif [ -x /sbin/openrc-run ] || [ -f /etc/alpine-release ]; then echo "openrc" else echo "unknown" fi } DISTRO="$(detect_distro)" INIT_SYSTEM="$(detect_init_system)" log "detected distro: $DISTRO, init system: $INIT_SYSTEM" install_deps() { case "$DISTRO" in debian | ubuntu | linuxmint | raspbian) export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get install -y git curl ca-certificates openssl ;; fedora | rhel | centos | rocky | almalinux) dnf install -y git curl ca-certificates openssl ;; arch | manjaro | endeavouros) pacman -Syu --noconfirm --needed git curl ca-certificates openssl ;; alpine) apk add --no-cache bash curl ca-certificates-bundle git openssl ;; *) fail "unsupported distro: $DISTRO" ;; esac } install_bun() { if [ ! -x "$BUN_BIN" ]; then log "installing bun to $BUN_INSTALL_DIR ..." mkdir -p "$BUN_INSTALL_DIR" curl -fsSL https://bun.sh/install | BUN_INSTALL="$BUN_INSTALL_DIR" bash if [ ! -x "$BUN_INSTALL_DIR/bun" ]; then fail "bun binary not found at $BUN_INSTALL_DIR/bun after install" fi ln -sf "$BUN_INSTALL_DIR/bun" "$BUN_BIN" fi log "bun: $($BUN_BIN --version)" } setup_service_user() { mkdir -p "$WGUI_LIB_DIR/iptables" "$WGUI_LIB_DIR/keys" "$WGUI_LIB_DIR/clients" chown -R "$SERVICE_USER:$SERVICE_GROUP" "$WGUI_LIB_DIR" } clone_or_update() { mkdir -p "$(dirname "$INSTALL_DIR")" if [ -d "$INSTALL_DIR/.git" ]; then UPDATE=true log "updating existing install at $INSTALL_DIR ..." git -C "$INSTALL_DIR" fetch --depth 1 origin "$BRANCH" local db_backup="$INSTALL_DIR/db/.wgui-pre-update.db" if [ -f "$INSTALL_DIR/db/wgui" ]; then cp -a "$INSTALL_DIR/db/wgui" "$db_backup" fi git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH" if [ -f "$db_backup" ]; then mv -f "$db_backup" "$INSTALL_DIR/db/wgui" fi else log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..." git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR" fi chown -R "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR" } run_as_service_user() { local cmd cmd="cd $(printf '%q' "$INSTALL_DIR") && export PATH=/usr/local/bin:/usr/bin:/bin && $(printf '%q ' "$@")" if command -v runuser >/dev/null 2>&1; then runuser -u "$SERVICE_USER" -- bash -c "$cmd" else su -s /bin/bash -c "$cmd" "$SERVICE_USER" fi } install_dependencies() { log "installing app dependencies with bun ..." run_as_service_user "$BUN_BIN" install } ensure_env_file() { if [ ! -f "$INSTALL_DIR/.env" ]; then log "generating $INSTALL_DIR/.env with fresh encryption secrets ..." { echo "NODE_ENV=${NODE_ENV:-production}" echo "ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '\n')" echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')" echo "DATA_DIR=$INSTALL_DIR/.data" } > "$INSTALL_DIR/.env" chown "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR/.env" chmod 600 "$INSTALL_DIR/.env" fi } setup_wireguard() { local setup_script="$INSTALL_DIR/src/scripts/setup-wireguard.sh" if [ -x "$setup_script" ] && [ "${SKIP_WIREGUARD_SETUP:-0}" != "1" ]; then log "running $(basename "$setup_script") to install wireguard on this host ..." "$setup_script" fi } grant_runtime_access() { log "granting $SERVICE_USER access to /etc/wireguard ..." mkdir -p /etc/wireguard chown "root:$SERVICE_GROUP" /etc/wireguard chmod 770 /etc/wireguard } install_systemd_unit() { local unit="/etc/systemd/system/$SERVICE_NAME.service" log "writing systemd unit $unit ..." cat > "$unit" < "$init_script" </dev/null | grep -oE '[0-9]+' | head -1 || echo "10752" } install_deps install_bun setup_service_user clone_or_update install_dependencies ensure_env_file setup_wireguard grant_runtime_access if [ "$DEV_MODE" = false ]; then case "$INIT_SYSTEM" in systemd) install_systemd_unit ;; openrc) install_openrc_unit ;; *) log "no supported init system found — start manually with: su -s /bin/bash $SERVICE_USER -c 'cd $INSTALL_DIR && NODE_ENV=production $BUN_BIN src/server.ts' (add the line above to your boot scripts)" ;; esac fi PORT="$(grab_port)" log "wg-ui install complete." log "webapp: $INSTALL_DIR" log "runtime: $WGUI_LIB_DIR (keys, iptables, client configs)" if [ "$DEV_MODE" = true ]; then log "process: development mode — no system service installed" else log "process: managed by $INIT_SYSTEM as $SERVICE_NAME" fi log "open http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):$PORT in your browser"