#!/usr/bin/env bash # mqvi — One-command installer for Linux servers. # # Usage: # curl -fsSL https://raw.githubusercontent.com/akinalpfdn/Mqvi/main/deploy/install.sh | sudo bash # # Optional flags (any combination): # --domain Public hostname (e.g. demo.mqvi.net). If omitted, sslip.io # is used automatically based on the public IP. # --port Internal port mqvi-server binds to. Default: 9090. # --no-tls Skip Caddy entirely; expose mqvi-server directly on --port. # Voice/video will not work in browsers without HTTPS. # --existing-caddy Force "existing Caddy" mode even if detection fails. # Script installs mqvi-server only and prints a Caddyfile # snippet for you to wire up manually. # -y, --yes Non-interactive: accept all defaults, skip prompts. # # What this does: # 1. Creates /opt/mqvi and a dedicated 'mqvi' system user # 2. Downloads the latest mqvi-server binary from GitHub releases # 3. Downloads the LiveKit SFU binary # 4. Generates .env with random JWT_SECRET and ENCRYPTION_KEY # 5. Writes livekit.yaml with random API credentials # 6. Installs systemd units for both services # 7. (TLS modes) Installs/configures Caddy for automatic Let's Encrypt # 8. Opens firewall ports (UFW / firewalld if present) # 9. Starts everything # # Re-running is safe: existing .env and livekit.yaml are preserved. set -euo pipefail REPO="akinalpfdn/Mqvi" INSTALL_DIR="/opt/mqvi" DATA_DIR="${INSTALL_DIR}/data" SERVICE_USER="mqvi" LIVEKIT_VERSION="v1.9.12" DEFAULT_PORT="9090" # ─── Flag defaults ─────────────────────────────────────────────────────────── DOMAIN="" MQVI_PORT="" NO_TLS=0 EXISTING_CADDY_FLAG=0 NON_INTERACTIVE=0 log() { printf '\033[1;34m[mqvi]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[mqvi]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[mqvi]\033[0m %s\n' "$*" >&2; exit 1; } # ─── Parse flags ───────────────────────────────────────────────────────────── while [ $# -gt 0 ]; do case "$1" in --domain) DOMAIN="${2:-}"; shift 2 ;; --domain=*) DOMAIN="${1#*=}"; shift ;; --port) MQVI_PORT="${2:-}"; shift 2 ;; --port=*) MQVI_PORT="${1#*=}"; shift ;; --no-tls) NO_TLS=1; shift ;; --existing-caddy) EXISTING_CADDY_FLAG=1; shift ;; -y|--yes) NON_INTERACTIVE=1; shift ;; -h|--help) sed -n '2,20p' "$0" | sed 's/^# \?//' exit 0 ;; *) die "Unknown flag: $1 (use --help)" ;; esac done [ "$(id -u)" -eq 0 ] || die "This script must be run as root. Try: sudo bash install.sh" # ─── Detect architecture ───────────────────────────────────────────────────── case "$(uname -m)" in x86_64|amd64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; *) die "Unsupported architecture: $(uname -m). mqvi supports x86_64 and arm64." ;; esac log "Detected architecture: ${ARCH}" # ─── Required tools ────────────────────────────────────────────────────────── for tool in curl tar openssl systemctl; do command -v "$tool" >/dev/null 2>&1 || die "Missing required tool: $tool" done # ─── Detect package manager (for Caddy install) ────────────────────────────── PKG="" if command -v apt-get >/dev/null 2>&1; then PKG="apt" elif command -v dnf >/dev/null 2>&1; then PKG="dnf" elif command -v yum >/dev/null 2>&1; then PKG="yum" fi set_clamav_config_value() { # $1 = file, $2 = key, $3 = value local file="$1" local key="$2" local value="$3" if [ ! -f "$file" ]; then return 1 fi if grep -Eq "^[#[:space:]]*${key}[[:space:]]+" "$file"; then sed -i -E "s|^[#[:space:]]*${key}[[:space:]]+.*|${key} ${value}|" "$file" else printf '\n%s %s\n' "$key" "$value" >> "$file" fi } configure_clamav_tcp() { local configured=0 for conf in /etc/clamav/clamd.conf /etc/clamd.d/scan.conf /etc/clamd.conf; do if [ -f "$conf" ]; then set_clamav_config_value "$conf" TCPSocket 3310 || true set_clamav_config_value "$conf" TCPAddr 127.0.0.1 || true configured=1 fi done if [ "$configured" -eq 0 ]; then warn "Could not find clamd.conf to enable TCP scanning. Set TCPSocket 3310 and TCPAddr 127.0.0.1 manually." return 1 fi return 0 } ensure_env_value() { # $1 = file, $2 = key, $3 = value local file="$1" local key="$2" local value="$3" if grep -Eq "^${key}=" "$file"; then return fi printf '%s=%s\n' "$key" "$value" >> "$file" } install_clamav() { if command -v clamd >/dev/null 2>&1 || command -v clamdscan >/dev/null 2>&1; then log "ClamAV already installed." configure_clamav_tcp || return 1 systemctl restart clamav-daemon >/dev/null 2>&1 || true systemctl restart clamd@scan >/dev/null 2>&1 || true return 0 fi local mem_kb mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null || echo 0)" if [ "${mem_kb:-0}" -gt 0 ] && [ "$mem_kb" -lt 1500000 ]; then warn "This server has less than 1.5GB RAM. ClamAV may be too heavy; uploads will still work if the scanner is unavailable." fi log "Installing ClamAV scanner..." case "$PKG" in apt) export DEBIAN_FRONTEND=noninteractive if ! apt-get update -qq; then warn "Could not update apt metadata for ClamAV."; return 1; fi if ! apt-get install -y clamav-daemon clamav-freshclam >/dev/null; then warn "Could not install ClamAV packages."; return 1; fi configure_clamav_tcp || return 1 systemctl enable clamav-freshclam >/dev/null 2>&1 || true systemctl restart clamav-freshclam >/dev/null 2>&1 || true systemctl enable clamav-daemon >/dev/null 2>&1 || true systemctl restart clamav-daemon >/dev/null 2>&1 || true ;; dnf|yum) if ! "$PKG" install -y clamav clamd clamav-update >/dev/null; then warn "Could not install ClamAV packages."; return 1; fi freshclam >/dev/null 2>&1 || true configure_clamav_tcp || return 1 systemctl enable clamd@scan >/dev/null 2>&1 || true systemctl restart clamd@scan >/dev/null 2>&1 || true systemctl enable clamav-freshclam >/dev/null 2>&1 || true systemctl restart clamav-freshclam >/dev/null 2>&1 || true ;; *) warn "Cannot auto-install ClamAV on this distro. Set up clamd manually or set MQVI_ANTIVIRUS_ENABLED=false." return 1 ;; esac } # ─── Detect existing Caddy ─────────────────────────────────────────────────── CADDY_PRESENT=0 if [ "$EXISTING_CADDY_FLAG" -eq 1 ]; then CADDY_PRESENT=1 elif command -v caddy >/dev/null 2>&1; then CADDY_PRESENT=1 fi # ─── Determine public IP (for sslip.io fallback) ───────────────────────────── PUBLIC_IP="$(curl -fsSL --max-time 3 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}')" [ -n "$PUBLIC_IP" ] || warn "Could not determine public IP." # ─── Interactive prompts (only if TTY + not -y) ────────────────────────────── prompt() { # $1 = question, $2 = default, $3 = var to set local answer if [ "$NON_INTERACTIVE" -eq 1 ] || [ ! -t 0 ]; then eval "$3=\"\${$3:-$2}\"" return fi if [ -n "${!3:-}" ]; then return; fi read -r -p "$1 [$2]: " answer }" log " Port: ${MQVI_PORT} (internal)" # In TLS modes mqvi binds to localhost — Caddy proxies to it. # In plain mode mqvi binds to 0.0.0.0 so it's reachable directly. if [ "$MODE" = "plain" ]; then BIND_HOST="0.0.0.0" else BIND_HOST="127.0.0.1" fi # ─── Create service user ───────────────────────────────────────────────────── if ! id "$SERVICE_USER" >/dev/null 2>&1; then log "Creating system user '${SERVICE_USER}'..." useradd --system --home "$INSTALL_DIR" --shell /usr/sbin/nologin "$SERVICE_USER" fi mkdir -p "$INSTALL_DIR" "$DATA_DIR/uploads" # ─── Download mqvi-server binary ───────────────────────────────────────────── MQVI_URL="https://github.com/${REPO}/releases/latest/download/mqvi-server-linux-${ARCH}" log "Downloading mqvi-server (linux-${ARCH})..." curl -fsSL --retry 3 -o "${INSTALL_DIR}/mqvi-server" "$MQVI_URL" \ || die "Failed to download mqvi-server from ${MQVI_URL}" chmod +x "${INSTALL_DIR}/mqvi-server" # ─── Download LiveKit binary ───────────────────────────────────────────────── if [ ! -x "${INSTALL_DIR}/livekit-server" ]; then LK_URL="https://github.com/livekit/livekit/releases/download/${LIVEKIT_VERSION}/livekit_${LIVEKIT_VERSION#v}_linux_${ARCH}.tar.gz" log "Downloading LiveKit ${LIVEKIT_VERSION}..." curl -fsSL --retry 3 "$LK_URL" | tar xz -C "$INSTALL_DIR" livekit-server \ || die "Failed to download LiveKit from ${LK_URL}" chmod +x "${INSTALL_DIR}/livekit-server" fi AV_ENABLED=true AV_SYSTEMD_UNITS="" if ! install_clamav; then AV_ENABLED=false warn "Antivirus disabled in generated .env. Set MQVI_ANTIVIRUS_ENABLED=true after installing and starting ClamAV manually." else AV_SYSTEMD_UNITS=" clamav-daemon.service clamd@scan.service" fi # ─── Generate livekit.yaml (if absent) ─────────────────────────────────────── LK_API_KEY="" LK_API_SECRET="" if [ ! -f "${INSTALL_DIR}/livekit.yaml" ]; then log "Generating livekit.yaml with random credentials..." LK_API_KEY="APIkey$(openssl rand -hex 6)" LK_API_SECRET="$(openssl rand -hex 32)" cat > "${INSTALL_DIR}/livekit.yaml" < "${INSTALL_DIR}/.env" <> "${INSTALL_DIR}/.env" fi if [ -n "$LK_API_KEY" ]; then cat >> "${INSTALL_DIR}/.env" < /etc/systemd/system/mqvi-livekit.service < /etc/systemd/system/mqvi-server.service </dev/null # ─── coturn (TURN relay for P2P calls) ─────────────────────────────────────── # Reuse the single source of truth (deploy/coturn-setup.sh) instead of # duplicating the turnserver.conf. Best-effort: 1-on-1 P2P calls still connect # without it (just no relay fallback), so a failure must not abort the install. log "Setting up coturn (P2P call TURN relay)..." # coturn-setup.sh reads TURN_SECRET from .env (single source of truth) — self-host # users don't hand-edit .env, so seed one here if absent (preserved on re-runs). ensure_env_value "${INSTALL_DIR}/.env" "TURN_SECRET" "$(openssl rand -hex 32)" if curl -fsSL --retry 3 "https://raw.githubusercontent.com/${REPO}/main/deploy/coturn-setup.sh" -o /tmp/mqvi-coturn-setup.sh; then MQVI_ENV="${INSTALL_DIR}/.env" bash /tmp/mqvi-coturn-setup.sh \ || warn "coturn setup failed — P2P relay disabled. Re-run deploy/coturn-setup.sh later." rm -f /tmp/mqvi-coturn-setup.sh chown "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}/.env" 2>/dev/null || true else warn "Could not download coturn-setup.sh — P2P relay not configured." fi # ─── Caddy install + config (TLS modes only, when not existing) ────────────── install_caddy() { command -v caddy >/dev/null 2>&1 && return 0 log "Installing Caddy..." case "$PKG" in apt) export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg ca-certificates >/dev/null curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key \ | gpg --dearmor --yes -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \ > /etc/apt/sources.list.d/caddy-stable.list apt-get update -qq apt-get install -y caddy >/dev/null ;; dnf|yum) "$PKG" install -y "dnf-command(copr)" >/dev/null 2>&1 || true "$PKG" copr enable -y @caddy/caddy >/dev/null 2>&1 || true "$PKG" install -y caddy >/dev/null ;; *) die "Cannot auto-install Caddy on this distro. Install Caddy manually, then re-run with --existing-caddy." ;; esac } write_caddyfile() { # Single-site Caddyfile owned by us. If you have other sites, use --existing-caddy instead. # The "managed by mqvi" marker lets start.sh safely sync max_size from .env. local size_bytes size_bytes="$(grep -E '^UPLOAD_MAX_SIZE=' "${INSTALL_DIR:-.}/.env" 2>/dev/null | tail -n 1 | cut -d= -f2-)" [ -z "$size_bytes" ] && size_bytes="104857600" cat > /etc/caddy/Caddyfile </dev/null 2>&1 || true } CADDY_SNIPPET="" case "$MODE" in new-caddy-domain|new-caddy-sslip) install_caddy write_caddyfile ;; existing) CADDY_SNIPPET=$(cat </dev/null 2>&1 && ufw status | grep -q "Status: active"; then log "Opening UFW ports..." for p in "${protos_tcp[@]}"; do ufw allow "${p}"/tcp >/dev/null; done ufw allow 7882/udp >/dev/null ufw allow "${protos_udp_range}"/udp >/dev/null elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then log "Opening firewalld ports..." for p in "${protos_tcp[@]}"; do firewall-cmd --permanent --add-port="${p}"/tcp >/dev/null; done firewall-cmd --permanent --add-port=7882/udp >/dev/null firewall-cmd --permanent --add-port="${protos_udp_range//:/-}"/udp >/dev/null firewall-cmd --reload >/dev/null else warn "No supported firewall detected. Open these ports manually:" warn " TCP: ${protos_tcp[*]}, 7882" warn " UDP: 7882, ${protos_udp_range}" fi } open_ports # ─── Start services ────────────────────────────────────────────────────────── log "Starting services..." systemctl restart mqvi-livekit.service sleep 1 systemctl restart mqvi-server.service sleep 2 if [ "$MODE" = "new-caddy-domain" ] || [ "$MODE" = "new-caddy-sslip" ]; then systemctl restart caddy sleep 2 fi if ! systemctl is-active --quiet mqvi-server.service; then warn "mqvi-server did not start. Check logs: journalctl -u mqvi-server -n 50" exit 1 fi # ─── Final output ──────────────────────────────────────────────────────────── PUBLIC_URL="" case "$MODE" in new-caddy-domain|new-caddy-sslip|existing) PUBLIC_URL="https://${DOMAIN}" ;; plain) PUBLIC_URL="http://${PUBLIC_IP:-}:${MQVI_PORT}" ;; esac echo echo "╔════════════════════════════════════════════════════════════╗" echo "║ mqvi is installed and running. ║" echo "╠════════════════════════════════════════════════════════════╣" printf "║ Web UI: %-47s║\n" "${PUBLIC_URL}" printf "║ Install: %-47s║\n" "${INSTALL_DIR}" printf "║ Data: %-47s║\n" "${DATA_DIR}" echo "║" echo "║ Logs: journalctl -u mqvi-server -f" echo "║ Restart: systemctl restart mqvi-server" echo "║ Stop: systemctl stop mqvi-server mqvi-livekit" echo "║" echo "║ The first user to register becomes the server owner." echo "╚════════════════════════════════════════════════════════════╝" if [ "$MODE" = "existing" ]; then echo echo "Caddy was detected on this system. mqvi-server is bound to 127.0.0.1:${MQVI_PORT}." echo "Add this block to your Caddyfile and reload Caddy:" echo echo " ${CADDY_SNIPPET//$'\n'/$'\n '}" echo echo "Then: sudo systemctl reload caddy" fi if [ "$MODE" = "plain" ]; then echo warn "Running over HTTP. Browsers will block microphone/camera/screen-share." warn "Re-run with --domain for automatic HTTPS via Let's Encrypt." fi