#!/usr/bin/env bash # ============================================================================= # NixPanel Installer — Ubuntu 24.04 LTS only # Usage: bash install.sh [--admin-user NAME] [--admin-pass PASS] [--admin-email EMAIL] # ============================================================================= set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' info() { echo -e "${CYAN}[INFO]${RESET} $*"; } success() { echo -e "${GREEN}[OK]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } step() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${RESET}"; } # ── Banner ──────────────────────────────────────────────────────────────────── echo -e "${BOLD}${CYAN}" cat <<'EOF' _ _ _ ____ _ | \ | (_)_ _| _ \ __ _ _ __ ___| | | \| | \ \/ / |_) / _` | '_ \ / _ \ | | |\ | |> <| __/ (_| | | | | __/ | |_| \_|_/_/\_\_| \__,_|_| |_|\___|_| Web Hosting Control Panel EOF echo -e "${RESET}" # ── Root check ──────────────────────────────────────────────────────────────── [[ $EUID -ne 0 ]] && error "Must be run as root. Try: sudo bash install.sh" # ── OS check — Ubuntu 24.04 only ───────────────────────────────────────────── step "Checking OS" if [[ ! -f /etc/os-release ]]; then error "Cannot detect OS — /etc/os-release not found" fi . /etc/os-release if [[ "${ID:-}" != "ubuntu" ]]; then error "NixPanel requires Ubuntu. Detected: ${PRETTY_NAME:-unknown}" fi UBUNTU_MAJOR=$(echo "${VERSION_ID:-0}" | cut -d. -f1) if [[ "$UBUNTU_MAJOR" -lt 24 ]]; then error "NixPanel requires Ubuntu 24.04 LTS. Detected: ${PRETTY_NAME:-unknown}" fi ARCH=$(uname -m) [[ "$ARCH" != "x86_64" && "$ARCH" != "aarch64" ]] && error "Unsupported architecture: $ARCH (x86_64 and aarch64 only)" success "Ubuntu ${VERSION_ID} on ${ARCH} — OK" # ── Argument parsing ────────────────────────────────────────────────────────── ADMIN_USER="admin" ADMIN_PASS="" ADMIN_EMAIL="" INSTALL_DIR="/opt/nixpanel" while [[ $# -gt 0 ]]; do case $1 in --admin-user) ADMIN_USER="$2"; shift 2 ;; --admin-pass) ADMIN_PASS="$2"; shift 2 ;; --admin-email) ADMIN_EMAIL="$2"; shift 2 ;; --install-dir) INSTALL_DIR="$2"; shift 2 ;; *) warn "Unknown argument: $1"; shift ;; esac done # ── Generate secrets ────────────────────────────────────────────────────────── gen_secret() { openssl rand -hex 32; } GENERATED_PASS=false if [[ -z "$ADMIN_PASS" ]]; then ADMIN_PASS=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) GENERATED_PASS=true fi if [[ -z "$ADMIN_EMAIL" ]]; then ADMIN_EMAIL="admin@$(hostname -f 2>/dev/null || echo localhost)" fi MARIADB_ROOT_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) JWT_SECRET=$(gen_secret) JWT_REFRESH_SECRET=$(gen_secret) # ── Confirm ─────────────────────────────────────────────────────────────────── echo -e "${BOLD}Installation summary:${RESET}" echo " Install directory : $INSTALL_DIR" echo " Admin username : $ADMIN_USER" echo " Admin email : $ADMIN_EMAIL" echo " Admin password : $([ "$GENERATED_PASS" = "true" ] && echo "" || echo "$ADMIN_PASS")" echo "" read -r -p "Continue? [Y/n] " CONFIRM CONFIRM="${CONFIRM:-Y}" [[ "$CONFIRM" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } # ── Logging ─────────────────────────────────────────────────────────────────── mkdir -p /var/log/nixpanel LOG_FILE="/var/log/nixpanel/install.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "======================================================" echo " NixPanel Install Log — $(date -u '+%Y-%m-%d %H:%M:%S UTC')" echo " OS: ${PRETTY_NAME} Arch: ${ARCH}" echo "======================================================" # ── Idempotent step tracker ─────────────────────────────────────────────────── STATE_DIR="/var/lib/nixpanel/install" mkdir -p "$STATE_DIR" step_done() { [[ -f "$STATE_DIR/$1.done" ]]; } mark_done() { touch "$STATE_DIR/$1.done"; } # ── Update package index ────────────────────────────────────────────────────── step "Updating package index" apt-get update -qq success "Package index updated" # ── Base dependencies ───────────────────────────────────────────────────────── step "Installing base dependencies" if ! step_done "base_deps"; then apt-get install -y -qq \ curl wget gnupg2 ca-certificates lsb-release software-properties-common \ git build-essential openssl logrotate mark_done "base_deps" fi success "Base dependencies ready" # ── Add ondrej/php PPA ──────────────────────────────────────────────────────── step "Adding PHP PPA" if ! step_done "php_ppa"; then add-apt-repository -y ppa:ondrej/php >/dev/null 2>&1 apt-get update -qq mark_done "php_ppa" fi success "PHP PPA ready" # ── Node.js 22 via NodeSource ───────────────────────────────────────────────── step "Installing Node.js 22 LTS" if ! step_done "nodejs"; then curl -fsSL https://deb.nodesource.com/setup_22.x | bash - >/dev/null 2>&1 apt-get install -y -qq nodejs mark_done "nodejs" fi success "Node.js $(node --version) ready" # ── Disable systemd-resolved stub listener (frees port 53 for PowerDNS) ───── step "Freeing port 53 for PowerDNS" if ! step_done "resolved_stub"; then # Ubuntu 24.04 runs systemd-resolved with a stub listener on 127.0.0.53:53. # PowerDNS's apt post-install restart fails if this is still active. sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf 2>/dev/null || true sed -i 's/^DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf 2>/dev/null || true systemctl restart systemd-resolved 2>/dev/null || true mark_done "resolved_stub" fi success "Port 53 freed" # ── Install all services in one pass ───────────────────────────────────────── step "Installing web stack (Apache, PHP, Exim, Dovecot, PowerDNS, MariaDB)" if ! step_done "packages"; then DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ `# Web server` \ apache2 apache2-utils \ `# PHP 8.2` \ php8.2-fpm php8.2-cli php8.2-mysql php8.2-curl php8.2-gd \ php8.2-mbstring php8.2-xml php8.2-zip php8.2-bcmath php8.2-intl \ php8.2-opcache php8.2-imagick \ `# PHP 8.3` \ php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-gd \ php8.3-mbstring php8.3-xml php8.3-zip php8.3-bcmath php8.3-intl \ php8.3-opcache php8.3-imagick \ `# DNS` \ pdns-server pdns-backend-bind \ `# Mail` \ exim4 exim4-daemon-heavy \ dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-managesieved \ spamassassin spamc \ `# Database (user data)` \ mariadb-server mariadb-client \ `# SSL` \ certbot python3-certbot-apache \ `# Security` \ fail2ban iptables iptables-persistent \ `# Utils` \ sqlite3 mark_done "packages" fi success "All packages installed" # ── Pin packages to prevent apt from overriding NixPanel-managed versions ───── step "Pinning managed packages" if ! step_done "apt_pins"; then cat > /etc/apt/preferences.d/nixpanel-pins <<'PINS' Package: apache2 apache2-* Pin: release * Pin-Priority: 1001 Package: php8.2-* php8.3-* Pin: release * Pin-Priority: 1001 Package: pdns-server pdns-backend-* Pin: release * Pin-Priority: 1001 Package: exim4 exim4-* Pin: release * Pin-Priority: 1001 Package: dovecot-* Pin: release * Pin-Priority: 1001 Package: mariadb-server mariadb-client Pin: release * Pin-Priority: 1001 PINS # Disable unattended upgrades (same as cPanel) systemctl disable --now unattended-upgrades 2>/dev/null || true mark_done "apt_pins" fi success "Package pins set" # ── Create nixpanel system user ──────────────────────────────────────────────── step "Creating nixpanel system user" if ! id nixpanel &>/dev/null; then useradd -r -s /usr/sbin/nologin -M -d "$INSTALL_DIR" nixpanel fi success "nixpanel user ready" # ── Directory structure ──────────────────────────────────────────────────────── step "Creating directory structure" mkdir -p "$INSTALL_DIR/data" mkdir -p "$INSTALL_DIR/logs" mkdir -p /etc/nixpanel mkdir -p /var/www/letsencrypt/.well-known/acme-challenge mkdir -p /etc/pdns/zones success "Directories created" # ── Configure Apache ────────────────────────────────────────────────────────── step "Configuring Apache" if ! step_done "apache"; then # Enable required modules a2enmod rewrite ssl proxy proxy_fcgi headers expires deflate 2>/dev/null || true # Disable default vhost a2dissite 000-default 2>/dev/null || true # Add panel ports to ports.conf if ! grep -q "Listen 2087" /etc/apache2/ports.conf 2>/dev/null; then cat >> /etc/apache2/ports.conf <<'PORTS' # NixPanel ports Listen 2087 Listen 2083 PORTS fi # Panel reverse proxy vhost — proxies 2087/2083 → Node.js on 127.0.0.1:4000 cat > /etc/apache2/sites-available/nixpanel-panels.conf < ServerName nixserver ProxyPreserveHost On ProxyPass /api/ http://127.0.0.1:4000/api/ retry=0 ProxyPassReverse /api/ http://127.0.0.1:4000/api/ # WebSocket for live stats RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteRule /ws/(.*) ws://127.0.0.1:4000/ws/\$1 [P,L] DocumentRoot ${INSTALL_DIR}/nixserver/dist Options -Indexes AllowOverride None Require all granted FallbackResource /index.html ErrorLog /var/log/apache2/nixserver-error.log CustomLog /var/log/apache2/nixserver-access.log combined ServerName nixclient ProxyPreserveHost On ProxyPass /api/ http://127.0.0.1:4000/api/ retry=0 ProxyPassReverse /api/ http://127.0.0.1:4000/api/ RewriteEngine On RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteRule /ws/(.*) ws://127.0.0.1:4000/ws/\$1 [P,L] DocumentRoot ${INSTALL_DIR}/nixclient/dist Options -Indexes AllowOverride None Require all granted FallbackResource /index.html ErrorLog /var/log/apache2/nixclient-error.log CustomLog /var/log/apache2/nixclient-access.log combined APACHE a2ensite nixpanel-panels systemctl enable apache2 systemctl restart apache2 # Certbot deploy hook to reload Apache on certificate renewal mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/nixpanel-reload-apache.sh <<'HOOK' #!/bin/bash systemctl reload apache2 HOOK chmod +x /etc/letsencrypt/renewal-hooks/deploy/nixpanel-reload-apache.sh mark_done "apache" fi success "Apache configured" # ── Configure PowerDNS ──────────────────────────────────────────────────────── step "Configuring PowerDNS" if ! step_done "pdns"; then # Stop any running instance first systemctl stop pdns 2>/dev/null || true # Create empty bindbackend config touch /etc/pdns/bindbackend.conf chmod 640 /etc/pdns/bindbackend.conf # Write PowerDNS config using bind backend cat > /etc/pdns/pdns.conf <<'PDNS' # NixPanel-managed PowerDNS configuration launch=bind bind-config=/etc/pdns/bindbackend.conf bind-check-interval=60 local-address=0.0.0.0 local-port=53 allow-recursion=0.0.0.0/0 log-dns-queries=no loglevel=3 # Security setuid=pdns setgid=pdns PDNS systemctl enable pdns systemctl start pdns mark_done "pdns" fi success "PowerDNS configured" # ── Configure Exim4 ─────────────────────────────────────────────────────────── step "Configuring Exim4" if ! step_done "exim"; then # Create virtual mail flat files (NixPanel manages these) touch /etc/exim4/virtual_domains touch /etc/exim4/virtual_mailboxes touch /etc/exim4/virtual_aliases chmod 640 /etc/exim4/virtual_domains /etc/exim4/virtual_mailboxes /etc/exim4/virtual_aliases # Set Exim to internet site mode via debconf debconf-set-selections <<'DEBCONF' exim4-config exim4/dc_eximconfig_configtype select internet site exim4-config exim4/dc_other_hostnames string localhost exim4-config exim4/dc_local_interfaces string exim4-config exim4/dc_readhost string exim4-config exim4/dc_relay_domains string exim4-config exim4/dc_minimaldns boolean false exim4-config exim4/dc_relay_nets string exim4-config exim4/dc_smarthost string exim4-config exim4/hide_mailname boolean true exim4-config exim4/mailname-file string /etc/mailname DEBCONF dpkg-reconfigure -f noninteractive exim4-config 2>/dev/null || true systemctl enable exim4 systemctl restart exim4 mark_done "exim" fi success "Exim4 configured" # ── Configure Dovecot ───────────────────────────────────────────────────────── step "Configuring Dovecot" if ! step_done "dovecot"; then # Create passwd file for virtual mailboxes touch /etc/dovecot/passwd chmod 640 /etc/dovecot/passwd chown root:dovecot /etc/dovecot/passwd # Configure Dovecot for passwd-file authentication cat > /etc/dovecot/conf.d/10-auth.conf <<'DOVEAUTH' disable_plaintext_auth = yes auth_mechanisms = plain login !include auth-passwdfile.conf.ext DOVEAUTH cat > /etc/dovecot/conf.d/auth-passwdfile.conf.ext <<'DOVEPF' passdb { driver = passwd-file args = scheme=SHA512-CRYPT /etc/dovecot/passwd } userdb { driver = passwd-file args = /etc/dovecot/passwd default_fields = uid=vmail gid=vmail home=/home/%d/%n } DOVEPF cat > /etc/dovecot/conf.d/10-mail.conf <<'DOVEMAIL' mail_location = maildir:/home/%d/%n/Maildir mail_privileged_group = mail DOVEMAIL systemctl enable dovecot systemctl restart dovecot mark_done "dovecot" fi success "Dovecot configured" # ── Configure SpamAssassin ──────────────────────────────────────────────────── step "Configuring SpamAssassin" if ! step_done "spamassassin"; then # Ubuntu 24.04: service unit is 'spamd', not 'spamassassin' systemctl enable spamd systemctl start spamd mark_done "spamassassin" fi success "SpamAssassin configured" # ── Configure MariaDB ───────────────────────────────────────────────────────── step "Configuring MariaDB" if ! step_done "mariadb"; then systemctl enable --now mariadb # Secure MariaDB (remove anonymous users, test db, set root password) mariadb -u root < /etc/nixpanel/mariadb.conf < "$INSTALL_DIR/server/.env" < /etc/systemd/system/nixpanel.service < /etc/fail2ban/jail.d/nixpanel.conf <<'F2B' [sshd] enabled = true maxretry = 5 bantime = 3600 findtime = 600 [apache-auth] enabled = true maxretry = 10 bantime = 600 [exim] enabled = true [dovecot] enabled = true F2B systemctl enable --now fail2ban mark_done "fail2ban" fi success "Fail2ban configured" # ── logrotate ───────────────────────────────────────────────────────────────── step "Configuring log rotation" cat > /etc/logrotate.d/nixpanel <<'LR' /var/log/nixpanel/*.log /opt/nixpanel/logs/*.log { daily rotate 14 compress delaycompress missingok notifempty sharedscripts postrotate systemctl reload nixpanel > /dev/null 2>&1 || true endscript } LR success "Log rotation configured" # ── Health check ────────────────────────────────────────────────────────────── step "Waiting for NixPanel to start" MAX_WAIT=30 WAITED=0 until curl -sf http://127.0.0.1:4000/api/health >/dev/null 2>&1; do sleep 2 WAITED=$((WAITED + 2)) if [[ $WAITED -ge $MAX_WAIT ]]; then warn "Health check timed out. Check: journalctl -u nixpanel -n 50" break fi done if curl -sf http://127.0.0.1:4000/api/health >/dev/null 2>&1; then success "NixPanel API is responding" fi # ── Save credentials ────────────────────────────────────────────────────────── CRED_FILE="/root/.nixpanel_credentials" cat > "$CRED_FILE" <