pelican-installer/install.sh
Pelican Installer Bot cbc8ee39ff v2.1.0: all /installer options as CLI flags, Wings reverse proxy, Redis, admin user
NEW — Every /installer wizard field is now a CLI flag:
  --cache-driver (file|redis)
  --redis-host, --redis-port, --redis-user, --redis-password
  --session-driver (file|database|cookie|redis)
  --queue-driver (database|redis|sync)
  --app-name
  --admin-email, --admin-user, --admin-pass (skip web installer entirely)

NEW — Redis auto-install:
  When cache/session/queue uses redis + host is localhost → installs redis-server
  Configures redis password if provided
  Installs php-redis extension alongside other PHP extensions

NEW — Wings reverse proxy for all 3 web servers:
  --wings-domain, --wings-port, --wings-proxy, --wings-ssl
  Nginx: full config with websocket upgrade for console/file manager
  Apache: mod_proxy + mod_proxy_wstunnel + RewriteEngine for WS
  Caddy: reverse_proxy with flush_interval -1 for websockets
  SSL certs obtained for both panel + wings domains in one pass
  Post-install instructions tell user to set api.ssl.enabled: false

NEW — Admin user auto-creation:
  If --admin-email/user/pass are all set, creates user via artisan tinker
  Sets APP_INSTALLED=true so /installer redirects to dashboard
  Shows admin credentials in green box

IMPROVED — Interactive menus now ask for all options:
  App name, cache driver, session driver, queue driver
  Redis connection details when redis is selected
  Wings proxy setup during wings install
  Admin user creation (optional, can still use web installer)

Health checks now verify: CACHE_STORE, SESSION_DRIVER, QUEUE_CONNECTION in .env
Health checks now verify: Redis running (when redis driver selected)
2026-05-17 12:24:58 +00:00

2402 lines
94 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
#############################################################################################
# #
# Pelican Installer — The best way to install Pelican Panel & Wings #
# #
# Copyright (c) 2026, JarvisCLI #
# https://github.com/everestmcarthur/pelican-installer #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License v3.0 #
# #
# Features: #
# • Full CLI flags for automated/unattended installs #
# • Every /installer wizard field available as a flag #
# • Supports Nginx, Apache, and Caddy (auto-SSL) web servers #
# • Full reverse proxy for Wings daemon on all 3 web servers #
# • Supports SQLite, MySQL, MariaDB, and PostgreSQL databases #
# • Redis caching/session/queue support with auto-install #
# • Automatic SSL via Let's Encrypt or Caddy #
# • Creates admin user automatically (skip web installer entirely) #
# • Writes ALL config to .env — panel works immediately after install #
# • Runs database migrations & seeders automatically #
# • Post-install health checks & HTTP verification #
# • Queue worker systemd service + cron auto-setup #
# • Comprehensive logging to /var/log/pelican-installer.log #
# • Update & Uninstall commands #
# • ARM64 support #
# • Single script — no external dependencies #
# #
#############################################################################################
# ═══════════════════════════════════════════════════════════════════════════════
# Version & Defaults
# ═══════════════════════════════════════════════════════════════════════════════
INSTALLER_VERSION="2.1.0"
PELICAN_PANEL_URL="https://github.com/pelican-dev/panel/releases/latest/download/panel.tar.gz"
PELICAN_WINGS_BASE="https://github.com/pelican-dev/wings/releases/latest/download"
# ── Configurable variables (override via flags or environment) ──
# Paths
PELICAN_DIR="${PELICAN_DIR:-/var/www/pelican}"
WINGS_DIR="${WINGS_DIR:-/etc/pelican}"
WINGS_BIN="${WINGS_BIN:-/usr/local/bin/wings}"
LOG_FILE="${LOG_FILE:-/var/log/pelican-installer.log}"
# Panel basics
FQDN="${FQDN:-}"
WEBSERVER="${WEBSERVER:-}" # nginx | apache | caddy
APP_NAME="${APP_NAME:-Pelican}"
APP_URL="${APP_URL:-}" # auto-set from FQDN if empty
TIMEZONE="${TIMEZONE:-UTC}"
CONFIGURE_SSL="${CONFIGURE_SSL:-}" # true | false
EMAIL="${EMAIL:-}"
# Admin user (skip web installer entirely)
ADMIN_EMAIL="${ADMIN_EMAIL:-}"
ADMIN_USER="${ADMIN_USER:-}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
# Database
DB_CHOICE="${DB_CHOICE:-}" # sqlite | mysql | mariadb | postgres
DB_HOST="${DB_HOST:-127.0.0.1}"
DB_PORT="${DB_PORT:-}" # auto-set based on DB_CHOICE if empty
DB_NAME="${DB_NAME:-pelican}"
DB_USER="${DB_USER:-pelican}"
DB_PASSWORD="${DB_PASSWORD:-}" # auto-generated if empty
# Cache
CACHE_DRIVER="${CACHE_DRIVER:-}" # file | redis
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
REDIS_PORT="${REDIS_PORT:-6379}"
REDIS_USER="${REDIS_USER:-}"
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
# Session
SESSION_DRIVER="${SESSION_DRIVER:-}" # file | database | cookie | redis
# Queue
QUEUE_DRIVER="${QUEUE_DRIVER:-}" # database | redis | sync
# Wings
INSTALL_PANEL="${INSTALL_PANEL:-}"
INSTALL_WINGS_FLAG="${INSTALL_WINGS_FLAG:-}"
WINGS_DOMAIN="${WINGS_DOMAIN:-}" # FQDN for Wings reverse proxy (e.g., node.example.com)
WINGS_PORT="${WINGS_PORT:-8080}" # Wings daemon port (default 8080)
WINGS_SSL="${WINGS_SSL:-}" # true | false — SSL for Wings reverse proxy
WINGS_PROXY="${WINGS_PROXY:-}" # true | false — set up reverse proxy for Wings
# General
ASSUME_YES="${ASSUME_YES:-false}"
ACTION="${ACTION:-}" # install | update | uninstall
# ═══════════════════════════════════════════════════════════════════════════════
# Colors & Formatting
# ═══════════════════════════════════════════════════════════════════════════════
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; MAGENTA='\033[0;35m'; CYAN='\033[0;36m'
WHITE='\033[1;37m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' MAGENTA='' CYAN='' WHITE='' BOLD='' DIM='' RESET=''
fi
CURRENT_STEP=0
TOTAL_STEPS=0
# ═══════════════════════════════════════════════════════════════════════════════
# Logging & Output
# ═══════════════════════════════════════════════════════════════════════════════
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
print_header() {
clear 2>/dev/null || true
echo ""
echo -e "${CYAN}${BOLD}"
echo " ╔═══════════════════════════════════════════════════════════╗"
echo " ║ ║"
echo " ║ 🐦 Pelican Panel & Wings Installer 🐦 ║"
echo " ║ ║"
echo " ║ v${INSTALLER_VERSION} ║"
echo " ╚═══════════════════════════════════════════════════════════╝"
echo -e "${RESET}"
echo ""
}
info() { echo -e " ${BLUE}ℹ${RESET} $*"; log "INFO: $*"; }
success() { echo -e " ${GREEN}✓${RESET} $*"; log "OK: $*"; }
warn() { echo -e " ${YELLOW}⚠${RESET} $*"; log "WARN: $*"; }
error() { echo -e " ${RED}✗${RESET} $*"; log "ERROR: $*"; }
fatal() {
echo ""
echo -e " ${RED}${BOLD}FATAL:${RESET} ${RED}$*${RESET}"
log "FATAL: $*"
echo -e " ${DIM}Check ${LOG_FILE} for details${RESET}"
echo ""
exit 1
}
step() {
CURRENT_STEP=$((CURRENT_STEP + 1))
echo ""
if [[ $TOTAL_STEPS -gt 0 ]]; then
echo -e " ${MAGENTA}${BOLD}[${CURRENT_STEP}/${TOTAL_STEPS}] $*${RESET}"
else
echo -e " ${MAGENTA}${BOLD}▸ $*${RESET}"
fi
echo -e " ${DIM}$(printf '%.0s─' {1..55})${RESET}"
log "STEP ${CURRENT_STEP}: $*"
}
ask() { echo -e -n " ${CYAN}?${RESET} $* "; }
confirm() {
local prompt="$1"
local default="${2:-Y}"
if [[ "$ASSUME_YES" == true ]]; then
return 0
fi
ask "${prompt}"
read -r answer
answer="${answer:-$default}"
[[ "$answer" =~ ^[Yy]$ ]]
}
spinner() {
local pid=$1
local msg=$2
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r ${CYAN}%s${RESET} %s" "${spin:i++%${#spin}:1}" "$msg"
sleep 0.1
done
printf "\r ${CYAN} ${RESET} %s\n" "$msg"
}
run_cmd() {
local msg="$1"
shift
"$@" >> "$LOG_FILE" 2>&1 &
local pid=$!
spinner $pid "$msg"
wait $pid
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
error "$msg — FAILED (exit code $exit_code)"
error "Check ${LOG_FILE} for details"
return $exit_code
fi
return 0
}
# ═══════════════════════════════════════════════════════════════════════════════
# CLI Argument Parsing
# ═══════════════════════════════════════════════════════════════════════════════
show_help() {
echo ""
echo " Pelican Installer v${INSTALLER_VERSION}"
echo ""
echo " Usage: $0 [ACTION] [OPTIONS]"
echo ""
echo " Actions:"
echo " install Install Panel and/or Wings (default)"
echo " update Update Panel and/or Wings"
echo " uninstall Uninstall Panel and/or Wings"
echo ""
echo " Components:"
echo " --panel Install/update Panel"
echo " --wings Install/update Wings"
echo " --both Install/update Panel + Wings"
echo ""
echo " Panel — General:"
echo " --domain FQDN Panel domain name (e.g., panel.example.com)"
echo " --app-name NAME Application name (default: Pelican)"
echo " --webserver WS Web server: nginx, apache, caddy"
echo " --ssl Enable SSL via Let's Encrypt"
echo " --no-ssl Disable SSL"
echo " --email EMAIL Email for Let's Encrypt"
echo " --timezone TZ Timezone (default: UTC)"
echo ""
echo " Panel — Admin User (skip web installer):"
echo " --admin-email EMAIL Admin account email"
echo " --admin-user USER Admin account username"
echo " --admin-pass PASS Admin account password"
echo ""
echo " Panel — Database:"
echo " --database DB Database: sqlite, mysql, mariadb, postgres"
echo " --db-host HOST Database host (default: 127.0.0.1)"
echo " --db-port PORT Database port (auto-detected from db type)"
echo " --db-name NAME Database name (default: pelican)"
echo " --db-user USER Database user (default: pelican)"
echo " --db-password PASS Database password (auto-generated if empty)"
echo ""
echo " Panel — Cache:"
echo " --cache-driver DRV Cache driver: file, redis (default: file)"
echo " --redis-host HOST Redis host (default: 127.0.0.1)"
echo " --redis-port PORT Redis port (default: 6379)"
echo " --redis-user USER Redis username (optional, Redis 6+ ACL)"
echo " --redis-password PW Redis password (optional)"
echo ""
echo " Panel — Session:"
echo " --session-driver DRV Session driver: file, database, cookie, redis"
echo ""
echo " Panel — Queue:"
echo " --queue-driver DRV Queue driver: database, redis, sync"
echo ""
echo " Wings:"
echo " --wings-domain FQDN Wings node domain (e.g., node.example.com)"
echo " --wings-port PORT Wings daemon port (default: 8080)"
echo " --wings-proxy Set up web server reverse proxy for Wings"
echo " --wings-ssl Enable SSL for Wings reverse proxy"
echo ""
echo " General:"
echo " --yes, -y Skip all confirmation prompts"
echo " --help, -h Show this help"
echo ""
echo " Examples:"
echo ""
echo " # Interactive install"
echo " sudo bash install.sh"
echo ""
echo " # Fully automated panel install (skip web installer entirely)"
echo " sudo bash install.sh install --panel --domain panel.example.com \\"
echo " --webserver nginx --database postgres --ssl --email you@example.com \\"
echo " --cache-driver redis --session-driver redis --queue-driver redis \\"
echo " --admin-email admin@example.com --admin-user admin --admin-pass S3cure! \\"
echo " --timezone America/New_York -y"
echo ""
echo " # Panel + Wings with Wings reverse proxy"
echo " sudo bash install.sh install --both --domain panel.example.com \\"
echo " --wings-domain node.example.com --wings-proxy --wings-ssl \\"
echo " --webserver nginx --database postgres --ssl --email you@example.com -y"
echo ""
echo " # Wings only with Caddy reverse proxy"
echo " sudo bash install.sh install --wings --wings-domain node.example.com \\"
echo " --wings-proxy --webserver caddy -y"
echo ""
echo " # Update everything"
echo " sudo bash install.sh update --both -y"
echo ""
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
install|update|uninstall)
ACTION="$1" ;;
--panel)
INSTALL_PANEL=true ;;
--wings)
INSTALL_WINGS_FLAG=true ;;
--both)
INSTALL_PANEL=true; INSTALL_WINGS_FLAG=true ;;
--domain|--fqdn)
FQDN="$2"; shift ;;
--app-name)
APP_NAME="$2"; shift ;;
--webserver|--ws)
WEBSERVER="$2"; shift ;;
--database|--db)
DB_CHOICE="$2"; shift ;;
--db-host)
DB_HOST="$2"; shift ;;
--db-port)
DB_PORT="$2"; shift ;;
--db-name)
DB_NAME="$2"; shift ;;
--db-user)
DB_USER="$2"; shift ;;
--db-password|--db-pass)
DB_PASSWORD="$2"; shift ;;
--ssl)
CONFIGURE_SSL=true ;;
--no-ssl)
CONFIGURE_SSL=false ;;
--email)
EMAIL="$2"; shift ;;
--timezone|--tz)
TIMEZONE="$2"; shift ;;
--admin-email)
ADMIN_EMAIL="$2"; shift ;;
--admin-user|--admin-username)
ADMIN_USER="$2"; shift ;;
--admin-pass|--admin-password)
ADMIN_PASSWORD="$2"; shift ;;
--cache-driver|--cache)
CACHE_DRIVER="$2"; shift ;;
--redis-host)
REDIS_HOST="$2"; shift ;;
--redis-port)
REDIS_PORT="$2"; shift ;;
--redis-user|--redis-username)
REDIS_USER="$2"; shift ;;
--redis-password|--redis-pass)
REDIS_PASSWORD="$2"; shift ;;
--session-driver|--session)
SESSION_DRIVER="$2"; shift ;;
--queue-driver|--queue)
QUEUE_DRIVER="$2"; shift ;;
--wings-domain)
WINGS_DOMAIN="$2"; shift ;;
--wings-port)
WINGS_PORT="$2"; shift ;;
--wings-proxy)
WINGS_PROXY=true ;;
--wings-ssl)
WINGS_SSL=true ;;
--yes|-y)
ASSUME_YES=true ;;
--help|-h)
show_help ;;
*)
warn "Unknown option: $1" ;;
esac
shift
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# OS Detection
# ═══════════════════════════════════════════════════════════════════════════════
detect_os() {
step "Detecting system environment"
if [[ ! -f /etc/os-release ]]; then
fatal "Cannot detect operating system — /etc/os-release not found."
fi
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
OS_NAME="$PRETTY_NAME"
case "$OS_ID" in
ubuntu|debian)
OS_FAMILY="debian"; PKG_MANAGER="apt" ;;
almalinux|rocky|centos|rhel)
OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;;
*)
if command -v apt &>/dev/null; then
OS_FAMILY="debian"; PKG_MANAGER="apt"
warn "Unknown OS '$OS_ID' — detected apt, proceeding as Debian-like"
elif command -v dnf &>/dev/null; then
OS_FAMILY="rhel"; PKG_MANAGER="dnf"
warn "Unknown OS '$OS_ID' — detected dnf, proceeding as RHEL-like"
else
fatal "Unsupported OS: $OS_ID (no apt or dnf found)"
fi
;;
esac
ARCH=$(uname -m)
case "$ARCH" in
x86_64) WINGS_ARCH="amd64" ;;
aarch64|arm64) WINGS_ARCH="arm64" ;;
*) fatal "Unsupported architecture: $ARCH" ;;
esac
success "OS: ${BOLD}$OS_NAME${RESET} ($OS_FAMILY/$PKG_MANAGER)"
success "Architecture: ${BOLD}$ARCH${RESET} → Wings binary: $WINGS_ARCH"
# Detect web server user (updated later when WEBSERVER is chosen)
update_webserver_user
}
update_webserver_user() {
if [[ "$OS_FAMILY" == "rhel" ]]; then
case "${WEBSERVER:-nginx}" in
nginx) WEBSERVER_USER="nginx" ;;
apache) WEBSERVER_USER="apache" ;;
caddy) WEBSERVER_USER="caddy" ;;
*) WEBSERVER_USER="nginx" ;;
esac
else
WEBSERVER_USER="www-data"
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# Preflight Checks
# ═══════════════════════════════════════════════════════════════════════════════
preflight_checks() {
step "Running preflight checks"
# Root check
if [[ $EUID -ne 0 ]]; then
fatal "This script must be run as root. Use: sudo bash $0"
fi
success "Running as root"
# Essential tools
for cmd in curl tar openssl; do
if ! command -v "$cmd" &>/dev/null; then
info "Installing $cmd..."
$PKG_MANAGER install -y "$cmd" >> "$LOG_FILE" 2>&1 || fatal "Failed to install $cmd"
fi
success "$cmd available"
done
# Virtualization check (for Wings)
if command -v systemd-detect-virt &>/dev/null; then
local virt
virt=$(systemd-detect-virt 2>/dev/null || echo "none")
if [[ "$virt" == "openvz" || "$virt" == "lxc" ]]; then
warn "Detected ${BOLD}$virt${RESET} — Docker/Wings may not work without nested virt"
else
success "Virtualization: $virt"
fi
fi
# Memory
local total_mem
total_mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo "0")
if [[ $total_mem -lt 1024 ]]; then
warn "Low memory: ${total_mem}MB (1GB+ recommended)"
else
success "Memory: ${total_mem}MB"
fi
# Disk
local free_disk
free_disk=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
if [[ $free_disk -lt 5 ]]; then
warn "Low disk: ${free_disk}GB free (10GB+ recommended)"
else
success "Disk: ${free_disk}GB free"
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# PHP Installation
# ═══════════════════════════════════════════════════════════════════════════════
detect_php_version() {
for ver in 8.4 8.3 8.2; do
if [[ "$OS_FAMILY" == "debian" ]]; then
if apt-cache show "php${ver}-fpm" &>/dev/null 2>&1; then
PHP_VERSION="$ver"; return
fi
else
if dnf list "php" 2>/dev/null | grep -q "$ver"; then
PHP_VERSION="$ver"; return
fi
fi
done
PHP_VERSION="8.3"
}
install_php() {
step "Installing PHP $PHP_VERSION & extensions"
if [[ "$OS_FAMILY" == "debian" ]]; then
# Add PHP repo
if ! command -v add-apt-repository &>/dev/null; then
apt install -y software-properties-common >> "$LOG_FILE" 2>&1
fi
if [[ "$OS_ID" == "ubuntu" ]]; then
add-apt-repository -y ppa:ondrej/php >> "$LOG_FILE" 2>&1 || true
elif [[ "$OS_ID" == "debian" ]]; then
apt install -y lsb-release apt-transport-https ca-certificates >> "$LOG_FILE" 2>&1
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb >> "$LOG_FILE" 2>&1
dpkg -i /tmp/debsuryorg-archive-keyring.deb >> "$LOG_FILE" 2>&1
echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" \
> /etc/apt/sources.list.d/sury-php.list
fi
run_cmd "Updating package lists" apt update
local pkgs=(
"php${PHP_VERSION}" "php${PHP_VERSION}-fpm" "php${PHP_VERSION}-common"
"php${PHP_VERSION}-cli" "php${PHP_VERSION}-gd" "php${PHP_VERSION}-mysql"
"php${PHP_VERSION}-pgsql" "php${PHP_VERSION}-mbstring" "php${PHP_VERSION}-bcmath"
"php${PHP_VERSION}-xml" "php${PHP_VERSION}-curl" "php${PHP_VERSION}-zip"
"php${PHP_VERSION}-intl" "php${PHP_VERSION}-sqlite3" "php${PHP_VERSION}-tokenizer"
"php${PHP_VERSION}-fileinfo" "php${PHP_VERSION}-dom" "php${PHP_VERSION}-redis"
)
run_cmd "Installing PHP ${PHP_VERSION} and extensions" apt install -y "${pkgs[@]}"
elif [[ "$OS_FAMILY" == "rhel" ]]; then
dnf install -y epel-release >> "$LOG_FILE" 2>&1
dnf install -y "https://rpms.remirepo.net/enterprise/remi-release-$(rpm -E %rhel).rpm" >> "$LOG_FILE" 2>&1 || true
dnf module reset php -y >> "$LOG_FILE" 2>&1 || true
dnf module enable "php:remi-${PHP_VERSION}" -y >> "$LOG_FILE" 2>&1 || true
run_cmd "Installing PHP ${PHP_VERSION} and extensions" dnf install -y \
php php-fpm php-common php-cli php-gd php-mysqlnd php-pgsql \
php-mbstring php-bcmath php-xml php-curl php-zip php-intl php-pdo \
php-tokenizer php-redis
fi
# Configure PHP-FPM pool
local fpm_conf=""
if [[ "$OS_FAMILY" == "debian" ]]; then
fpm_conf="/etc/php/${PHP_VERSION}/fpm/pool.d/www.conf"
else
fpm_conf="/etc/php-fpm.d/www.conf"
fi
if [[ -f "$fpm_conf" ]]; then
sed -i "s/^user = .*/user = ${WEBSERVER_USER}/" "$fpm_conf"
sed -i "s/^group = .*/group = ${WEBSERVER_USER}/" "$fpm_conf"
sed -i "s/^listen.owner = .*/listen.owner = ${WEBSERVER_USER}/" "$fpm_conf"
sed -i "s/^listen.group = .*/listen.group = ${WEBSERVER_USER}/" "$fpm_conf"
success "PHP-FPM pool configured for ${WEBSERVER_USER}"
fi
systemctl enable --now "php${PHP_VERSION}-fpm" >> "$LOG_FILE" 2>&1 || \
systemctl enable --now php-fpm >> "$LOG_FILE" 2>&1 || true
success "PHP-FPM started and enabled"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Redis Installation
# ═══════════════════════════════════════════════════════════════════════════════
needs_redis() {
[[ "$CACHE_DRIVER" == "redis" || "$SESSION_DRIVER" == "redis" || "$QUEUE_DRIVER" == "redis" ]]
}
install_redis() {
if ! needs_redis; then
return
fi
# Only install if using localhost and redis isn't already running
if [[ "$REDIS_HOST" == "127.0.0.1" || "$REDIS_HOST" == "localhost" || "$REDIS_HOST" == "::1" ]]; then
if command -v redis-server &>/dev/null && systemctl is-active --quiet redis-server 2>/dev/null; then
success "Redis is already installed and running"
else
step "Installing Redis"
if [[ "$OS_FAMILY" == "debian" ]]; then
run_cmd "Installing Redis server" apt install -y redis-server
else
run_cmd "Installing Redis server" dnf install -y redis
fi
# Configure Redis password if set
if [[ -n "$REDIS_PASSWORD" ]]; then
local redis_conf="/etc/redis/redis.conf"
[[ ! -f "$redis_conf" ]] && redis_conf="/etc/redis.conf"
if [[ -f "$redis_conf" ]]; then
sed -i "s/^# requirepass .*/requirepass ${REDIS_PASSWORD}/" "$redis_conf"
sed -i "s/^requirepass .*/requirepass ${REDIS_PASSWORD}/" "$redis_conf"
success "Redis password configured"
fi
fi
systemctl enable --now redis-server >> "$LOG_FILE" 2>&1 || \
systemctl enable --now redis >> "$LOG_FILE" 2>&1
success "Redis installed and running"
fi
else
info "Redis host is ${REDIS_HOST} — skipping local Redis install"
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# Database Installation
# ═══════════════════════════════════════════════════════════════════════════════
generate_db_password() {
if [[ -z "$DB_PASSWORD" && "$DB_CHOICE" != "sqlite" ]]; then
DB_PASSWORD=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
log "Generated DB password: $DB_PASSWORD"
fi
}
auto_set_db_port() {
if [[ -z "$DB_PORT" ]]; then
case "$DB_CHOICE" in
mysql|mariadb) DB_PORT="3306" ;;
postgres) DB_PORT="5432" ;;
sqlite) DB_PORT="" ;;
esac
fi
}
install_database() {
if [[ "$DB_CHOICE" == "sqlite" ]]; then
info "Using SQLite — no database server needed"
return
fi
step "Installing ${DB_CHOICE^} database server"
case "$DB_CHOICE" in
mysql)
if [[ "$OS_FAMILY" == "debian" ]]; then
run_cmd "Installing MySQL" apt install -y mysql-server mysql-client
else
run_cmd "Installing MySQL" dnf install -y mysql-server
fi
systemctl enable --now mysql >> "$LOG_FILE" 2>&1 || systemctl enable --now mysqld >> "$LOG_FILE" 2>&1
success "MySQL installed and running"
;;
mariadb)
if [[ "$OS_FAMILY" == "debian" ]]; then
run_cmd "Installing MariaDB" apt install -y mariadb-server mariadb-client
else
run_cmd "Installing MariaDB" dnf install -y mariadb-server
fi
systemctl enable --now mariadb >> "$LOG_FILE" 2>&1
success "MariaDB installed and running"
;;
postgres)
if [[ "$OS_FAMILY" == "debian" ]]; then
run_cmd "Installing PostgreSQL" apt install -y postgresql postgresql-client
else
run_cmd "Installing PostgreSQL" dnf install -y postgresql-server postgresql
postgresql-setup --initdb >> "$LOG_FILE" 2>&1 || true
fi
systemctl enable --now postgresql >> "$LOG_FILE" 2>&1
success "PostgreSQL installed and running"
;;
esac
setup_database
}
setup_database() {
step "Creating database & user"
generate_db_password
auto_set_db_port
case "$DB_CHOICE" in
mysql|mariadb)
mysql -u root <<EOSQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`;
CREATE USER IF NOT EXISTS '${DB_USER}'@'${DB_HOST}' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'${DB_HOST}' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EOSQL
if [[ $? -ne 0 ]]; then
fatal "Failed to create MySQL/MariaDB database. Check ${LOG_FILE}"
fi
success "Database '${DB_NAME}' created with user '${DB_USER}'"
;;
postgres)
# Create user and database
sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" >> "$LOG_FILE" 2>&1 || {
sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" >> "$LOG_FILE" 2>&1
}
sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" >> "$LOG_FILE" 2>&1 || {
sudo -u postgres psql -c "ALTER DATABASE ${DB_NAME} OWNER TO ${DB_USER};" >> "$LOG_FILE" 2>&1
}
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" >> "$LOG_FILE" 2>&1
# CRITICAL: Configure pg_hba.conf for password auth over TCP
local pg_hba
pg_hba=$(sudo -u postgres psql -t -c "SHOW hba_file;" 2>/dev/null | xargs)
if [[ -n "$pg_hba" && -f "$pg_hba" ]]; then
if ! grep -q "${DB_USER}" "$pg_hba" 2>/dev/null; then
sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} 127.0.0.1/32 md5" "$pg_hba"
sed -i "/^# IPv6 local connections:/a host ${DB_NAME} ${DB_USER} ::1/128 md5" "$pg_hba"
success "pg_hba.conf configured for password authentication"
systemctl reload postgresql >> "$LOG_FILE" 2>&1
fi
else
for hba_path in /etc/postgresql/*/main/pg_hba.conf /var/lib/pgsql/data/pg_hba.conf; do
if [[ -f "$hba_path" ]]; then
if ! grep -q "${DB_USER}" "$hba_path"; then
sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} 127.0.0.1/32 md5" "$hba_path"
sed -i "/^# IPv6 local connections:/a host ${DB_NAME} ${DB_USER} ::1/128 md5" "$hba_path"
fi
break
fi
done
systemctl reload postgresql >> "$LOG_FILE" 2>&1
success "pg_hba.conf configured for password authentication"
fi
success "PostgreSQL database '${DB_NAME}' created with user '${DB_USER}'"
;;
esac
# Show credentials in a visible box
echo ""
echo -e " ${GREEN}${BOLD}┌─────────────────────────────────────────────┐${RESET}"
echo -e " ${GREEN}${BOLD}│ DATABASE CREDENTIALS — SAVE THESE! │${RESET}"
echo -e " ${GREEN}${BOLD}├─────────────────────────────────────────────┤${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Host: ${WHITE}${DB_HOST}${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Port: ${WHITE}${DB_PORT}${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Database: ${WHITE}${DB_NAME}${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} User: ${WHITE}${DB_USER}${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Password: ${WHITE}${DB_PASSWORD}${RESET}"
echo -e " ${GREEN}${BOLD}└─────────────────────────────────────────────┘${RESET}"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════════
# Web Server Installation
# ═══════════════════════════════════════════════════════════════════════════════
get_fpm_socket() {
if [[ "$OS_FAMILY" == "rhel" ]]; then
echo "/run/php-fpm/www.sock"
else
echo "/run/php/php${PHP_VERSION}-fpm.sock"
fi
}
install_webserver() {
step "Installing ${WEBSERVER^} web server"
case "$WEBSERVER" in
nginx) install_nginx ;;
apache) install_apache ;;
caddy) install_caddy ;;
esac
}
# ── Nginx ────────────────────────────────────────────────────────────────────
install_nginx() {
if [[ "$OS_FAMILY" == "debian" ]]; then
run_cmd "Installing Nginx" apt install -y nginx
else
run_cmd "Installing Nginx" dnf install -y nginx
fi
rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled 2>/dev/null || true
# Ensure sites-enabled is included on RHEL
if [[ "$OS_FAMILY" == "rhel" ]] && ! grep -q "sites-enabled" /etc/nginx/nginx.conf 2>/dev/null; then
sed -i '/http {/a \ include /etc/nginx/sites-enabled/*.conf;' /etc/nginx/nginx.conf
fi
local fpm_sock
fpm_sock=$(get_fpm_socket)
if [[ "$INSTALL_PANEL" == true ]]; then
write_nginx_panel_config "$fpm_sock"
ln -sf /etc/nginx/sites-available/pelican.conf /etc/nginx/sites-enabled/pelican.conf
success "Nginx panel vhost configured for ${FQDN}"
fi
if [[ "$WINGS_PROXY" == true && -n "$WINGS_DOMAIN" ]]; then
write_nginx_wings_config
ln -sf /etc/nginx/sites-available/wings.conf /etc/nginx/sites-enabled/wings.conf
success "Nginx wings reverse proxy configured for ${WINGS_DOMAIN}"
fi
nginx -t >> "$LOG_FILE" 2>&1 || warn "Nginx config test failed — check ${LOG_FILE}"
systemctl enable --now nginx >> "$LOG_FILE" 2>&1
systemctl restart nginx >> "$LOG_FILE" 2>&1
success "Nginx configured and running"
}
write_nginx_panel_config() {
local fpm_sock="$1"
if [[ "$CONFIGURE_SSL" == true ]]; then
cat > /etc/nginx/sites-available/pelican.conf <<NGINXCFG
server_tokens off;
server {
listen 80;
server_name ${FQDN};
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name ${FQDN};
root ${PELICAN_DIR}/public;
index index.php;
access_log /var/log/nginx/pelican.access.log;
error_log /var/log/nginx/pelican.error.log error;
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
ssl_certificate /etc/letsencrypt/live/${FQDN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${FQDN}/privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
ssl_prefer_server_ciphers on;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
add_header X-Frame-Options DENY;
add_header Referrer-Policy same-origin;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
fastcgi_split_path_info ^(.+\.php)(/.+)\$;
fastcgi_pass unix:${fpm_sock};
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}
NGINXCFG
else
cat > /etc/nginx/sites-available/pelican.conf <<NGINXCFG
server_tokens off;
server {
listen 80;
server_name ${FQDN};
root ${PELICAN_DIR}/public;
index index.php;
access_log /var/log/nginx/pelican.access.log;
error_log /var/log/nginx/pelican.error.log error;
client_max_body_size 100m;
client_body_timeout 120s;
sendfile off;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header Content-Security-Policy "frame-ancestors 'self'";
add_header X-Frame-Options DENY;
add_header Referrer-Policy same-origin;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
fastcgi_split_path_info ^(.+\.php)(/.+)\$;
fastcgi_pass unix:${fpm_sock};
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M";
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors off;
fastcgi_buffer_size 16k;
fastcgi_buffers 4 16k;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
}
location ~ /\.ht {
deny all;
}
}
NGINXCFG
fi
}
write_nginx_wings_config() {
if [[ "$WINGS_SSL" == true ]]; then
cat > /etc/nginx/sites-available/wings.conf <<WINGSCFG
server {
listen 80;
server_name ${WINGS_DOMAIN};
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name ${WINGS_DOMAIN};
ssl_certificate /etc/letsencrypt/live/${WINGS_DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${WINGS_DOMAIN}/privkey.pem;
ssl_session_cache shared:WingsSSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305";
ssl_prefer_server_ciphers on;
# Websocket support (console, file manager, etc.)
location ~ /api/servers/(?<server>[a-f0-9-]+)/ws\$ {
proxy_pass http://127.0.0.1:${WINGS_PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
location / {
proxy_pass http://127.0.0.1:${WINGS_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 0;
}
}
WINGSCFG
else
cat > /etc/nginx/sites-available/wings.conf <<WINGSCFG
server {
listen 80;
server_name ${WINGS_DOMAIN};
# Websocket support (console, file manager, etc.)
location ~ /api/servers/(?<server>[a-f0-9-]+)/ws\$ {
proxy_pass http://127.0.0.1:${WINGS_PORT};
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
location / {
proxy_pass http://127.0.0.1:${WINGS_PORT};
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 0;
}
}
WINGSCFG
fi
}
# ── Apache ───────────────────────────────────────────────────────────────────
install_apache() {
if [[ "$OS_FAMILY" == "debian" ]]; then
run_cmd "Installing Apache" apt install -y apache2 "libapache2-mod-php${PHP_VERSION}"
a2enmod rewrite ssl headers proxy proxy_fcgi proxy_http proxy_wstunnel >> "$LOG_FILE" 2>&1 || true
a2dissite 000-default >> "$LOG_FILE" 2>&1 || true
else
run_cmd "Installing Apache" dnf install -y httpd mod_ssl
fi
local fpm_sock
fpm_sock=$(get_fpm_socket)
local conf_dir="/etc/apache2/sites-available"
[[ "$OS_FAMILY" == "rhel" ]] && conf_dir="/etc/httpd/conf.d"
if [[ "$INSTALL_PANEL" == true ]]; then
write_apache_panel_config "$conf_dir" "$fpm_sock"
if [[ "$OS_FAMILY" == "debian" ]]; then
a2ensite pelican >> "$LOG_FILE" 2>&1
fi
success "Apache panel vhost configured for ${FQDN}"
fi
if [[ "$WINGS_PROXY" == true && -n "$WINGS_DOMAIN" ]]; then
write_apache_wings_config "$conf_dir"
if [[ "$OS_FAMILY" == "debian" ]]; then
a2ensite wings >> "$LOG_FILE" 2>&1
fi
success "Apache wings reverse proxy configured for ${WINGS_DOMAIN}"
fi
if [[ "$OS_FAMILY" == "debian" ]]; then
systemctl enable --now apache2 >> "$LOG_FILE" 2>&1
systemctl restart apache2 >> "$LOG_FILE" 2>&1
else
systemctl enable --now httpd >> "$LOG_FILE" 2>&1
systemctl restart httpd >> "$LOG_FILE" 2>&1
fi
success "Apache configured and running"
}
write_apache_panel_config() {
local conf_dir="$1"
local fpm_sock="$2"
if [[ "$CONFIGURE_SSL" == true ]]; then
cat > "${conf_dir}/pelican.conf" <<APACHECFG
<VirtualHost *:80>
ServerName ${FQDN}
Redirect permanent / https://${FQDN}/
</VirtualHost>
<VirtualHost *:443>
ServerName ${FQDN}
DocumentRoot ${PELICAN_DIR}/public
AllowEncodedSlashes On
php_value upload_max_filesize 100M
php_value post_max_size 100M
<Directory "${PELICAN_DIR}/public">
Require all granted
AllowOverride all
</Directory>
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${FQDN}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${FQDN}/privkey.pem
</VirtualHost>
APACHECFG
else
cat > "${conf_dir}/pelican.conf" <<APACHECFG
<VirtualHost *:80>
ServerName ${FQDN}
DocumentRoot ${PELICAN_DIR}/public
AllowEncodedSlashes On
php_value upload_max_filesize 100M
php_value post_max_size 100M
<Directory "${PELICAN_DIR}/public">
Require all granted
AllowOverride all
</Directory>
</VirtualHost>
APACHECFG
fi
}
write_apache_wings_config() {
local conf_dir="$1"
if [[ "$WINGS_SSL" == true ]]; then
cat > "${conf_dir}/wings.conf" <<WINGSCFG
<VirtualHost *:80>
ServerName ${WINGS_DOMAIN}
Redirect permanent / https://${WINGS_DOMAIN}/
</VirtualHost>
<VirtualHost *:443>
ServerName ${WINGS_DOMAIN}
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${WINGS_DOMAIN}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${WINGS_DOMAIN}/privkey.pem
SSLProxyEngine On
ProxyPreserveHost On
# Websocket support
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/(.*) ws://127.0.0.1:${WINGS_PORT}/\$1 [P,L]
ProxyPass / http://127.0.0.1:${WINGS_PORT}/
ProxyPassReverse / http://127.0.0.1:${WINGS_PORT}/
RequestHeader set X-Forwarded-Proto "https"
</VirtualHost>
WINGSCFG
else
cat > "${conf_dir}/wings.conf" <<WINGSCFG
<VirtualHost *:80>
ServerName ${WINGS_DOMAIN}
ProxyPreserveHost On
# Websocket support
RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/(.*) ws://127.0.0.1:${WINGS_PORT}/\$1 [P,L]
ProxyPass / http://127.0.0.1:${WINGS_PORT}/
ProxyPassReverse / http://127.0.0.1:${WINGS_PORT}/
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>
WINGSCFG
fi
}
# ── Caddy ────────────────────────────────────────────────────────────────────
install_caddy() {
if [[ "$OS_FAMILY" == "debian" ]]; then
apt install -y debian-keyring debian-archive-keyring apt-transport-https >> "$LOG_FILE" 2>&1
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 2>/dev/null
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
tee /etc/apt/sources.list.d/caddy-stable.list >> "$LOG_FILE" 2>&1
apt update >> "$LOG_FILE" 2>&1
run_cmd "Installing Caddy" apt install -y caddy
else
dnf install -y 'dnf-command(copr)' >> "$LOG_FILE" 2>&1
dnf copr enable @caddy/caddy -y >> "$LOG_FILE" 2>&1
run_cmd "Installing Caddy" dnf install -y caddy
fi
local fpm_sock
fpm_sock=$(get_fpm_socket)
# Build Caddyfile with panel + optional wings
{
if [[ "$INSTALL_PANEL" == true ]]; then
cat <<CADDYPANEL
${FQDN} {
root * ${PELICAN_DIR}/public
file_server
php_fastcgi unix/${fpm_sock} {
root ${PELICAN_DIR}/public
index index.php
}
header {
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
X-Robots-Tag none
Content-Security-Policy "frame-ancestors 'self'"
X-Frame-Options DENY
Referrer-Policy same-origin
-Server
}
encode gzip
log {
output file /var/log/caddy/pelican.log
}
}
CADDYPANEL
fi
if [[ "$WINGS_PROXY" == true && -n "$WINGS_DOMAIN" ]]; then
cat <<CADDYWINGS
${WINGS_DOMAIN} {
reverse_proxy 127.0.0.1:${WINGS_PORT} {
# Flush for websockets (console, file manager)
flush_interval -1
# Unlimited upload size for server files
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
# Websocket handling is automatic in Caddy
log {
output file /var/log/caddy/wings.log
}
}
CADDYWINGS
fi
} > /etc/caddy/Caddyfile
mkdir -p /var/log/caddy
systemctl enable --now caddy >> "$LOG_FILE" 2>&1
systemctl restart caddy >> "$LOG_FILE" 2>&1
[[ "$INSTALL_PANEL" == true ]] && success "Caddy panel vhost configured for ${FQDN} (auto-SSL)"
[[ "$WINGS_PROXY" == true && -n "$WINGS_DOMAIN" ]] && success "Caddy wings reverse proxy configured for ${WINGS_DOMAIN} (auto-SSL)"
success "Caddy configured and running"
}
# ═══════════════════════════════════════════════════════════════════════════════
# SSL (Let's Encrypt)
# ═══════════════════════════════════════════════════════════════════════════════
install_ssl() {
if [[ "$CONFIGURE_SSL" != true && "$WINGS_SSL" != true ]]; then
return
fi
if [[ "$WEBSERVER" == "caddy" ]]; then
info "Caddy handles SSL automatically — skipping certbot"
return
fi
step "Obtaining SSL certificate(s) via Let's Encrypt"
if [[ "$OS_FAMILY" == "debian" ]]; then
apt install -y certbot >> "$LOG_FILE" 2>&1
[[ "$WEBSERVER" == "nginx" ]] && apt install -y python3-certbot-nginx >> "$LOG_FILE" 2>&1
[[ "$WEBSERVER" == "apache" ]] && apt install -y python3-certbot-apache >> "$LOG_FILE" 2>&1
else
dnf install -y certbot >> "$LOG_FILE" 2>&1
[[ "$WEBSERVER" == "nginx" ]] && dnf install -y python3-certbot-nginx >> "$LOG_FILE" 2>&1
[[ "$WEBSERVER" == "apache" ]] && dnf install -y python3-certbot-apache >> "$LOG_FILE" 2>&1
fi
local plugin="--standalone"
[[ "$WEBSERVER" == "nginx" ]] && plugin="--nginx"
[[ "$WEBSERVER" == "apache" ]] && plugin="--apache"
# Collect all domains that need certs
local domains=()
[[ "$CONFIGURE_SSL" == true && -n "$FQDN" ]] && domains+=("$FQDN")
[[ "$WINGS_SSL" == true && -n "$WINGS_DOMAIN" && "$WINGS_DOMAIN" != "$FQDN" ]] && domains+=("$WINGS_DOMAIN")
for domain in "${domains[@]}"; do
info "Requesting certificate for ${domain}..."
certbot certonly $plugin -d "$domain" --non-interactive --agree-tos --email "$EMAIL" >> "$LOG_FILE" 2>&1 || {
warn "Plugin-based cert for $domain failed, trying standalone..."
systemctl stop nginx 2>/dev/null; systemctl stop apache2 2>/dev/null; systemctl stop httpd 2>/dev/null
certbot certonly --standalone -d "$domain" --non-interactive --agree-tos --email "$EMAIL" >> "$LOG_FILE" 2>&1 || \
error "Failed to obtain SSL certificate for $domain — is the domain pointed to this server?"
systemctl start nginx 2>/dev/null || systemctl start apache2 2>/dev/null || systemctl start httpd 2>/dev/null
}
success "SSL certificate obtained for ${domain}"
done
# Auto-renewal cron
local restart_svc="$WEBSERVER"
[[ "$WEBSERVER" == "apache" && "$OS_FAMILY" == "debian" ]] && restart_svc="apache2"
[[ "$WEBSERVER" == "apache" && "$OS_FAMILY" == "rhel" ]] && restart_svc="httpd"
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --deploy-hook \"systemctl restart ${restart_svc}\"") | sort -u | crontab -
success "Auto-renewal cron configured"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Pelican Panel Installation
# ═══════════════════════════════════════════════════════════════════════════════
install_panel() {
step "Downloading Pelican Panel"
mkdir -p "$PELICAN_DIR"
cd "$PELICAN_DIR"
run_cmd "Downloading and extracting Pelican Panel" bash -c "curl -sSL '$PELICAN_PANEL_URL' | tar -xzv >> '$LOG_FILE' 2>&1"
success "Panel files downloaded to ${PELICAN_DIR}"
step "Installing Composer & PHP dependencies"
if ! command -v composer &>/dev/null; then
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer >> "$LOG_FILE" 2>&1
success "Composer installed"
fi
run_cmd "Installing PHP dependencies (this takes ~1 minute)" \
env COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader --working-dir="$PELICAN_DIR"
success "Composer dependencies installed"
step "Configuring Pelican Panel"
configure_env_file
run_database_setup
set_permissions
create_admin_user
setup_queue_worker
setup_cron_job
# Show APP_KEY warning
local app_key
app_key=$(grep "^APP_KEY=" "${PELICAN_DIR}/.env" 2>/dev/null | cut -d= -f2-)
if [[ -n "$app_key" ]]; then
echo ""
echo -e " ${RED}${BOLD}┌─────────────────────────────────────────────────────────┐${RESET}"
echo -e " ${RED}${BOLD}│ ⚠ ENCRYPTION KEY — BACK THIS UP! LOSING IT = DATA LOSS│${RESET}"
echo -e " ${RED}${BOLD}├─────────────────────────────────────────────────────────┤${RESET}"
echo -e " ${RED}${BOLD}│${RESET} APP_KEY=${WHITE}${app_key}${RESET}"
echo -e " ${RED}${BOLD}└─────────────────────────────────────────────────────────┘${RESET}"
echo ""
fi
}
configure_env_file() {
info "Writing .env configuration..."
# Determine APP_URL
if [[ -z "$APP_URL" ]]; then
if [[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]]; then
APP_URL="https://${FQDN}"
else
APP_URL="http://${FQDN}"
fi
fi
# Determine DB connection string for Laravel
local laravel_db_connection
case "$DB_CHOICE" in
mysql) laravel_db_connection="mysql" ;;
mariadb) laravel_db_connection="mariadb" ;;
postgres) laravel_db_connection="pgsql" ;;
sqlite) laravel_db_connection="sqlite" ;;
esac
# Set defaults for session/cache/queue if not set
CACHE_DRIVER="${CACHE_DRIVER:-file}"
SESSION_DRIVER="${SESSION_DRIVER:-file}"
QUEUE_DRIVER="${QUEUE_DRIVER:-database}"
# Copy example .env if needed
if [[ ! -f "${PELICAN_DIR}/.env" && -f "${PELICAN_DIR}/.env.example" ]]; then
cp "${PELICAN_DIR}/.env.example" "${PELICAN_DIR}/.env"
fi
# Generate APP_KEY
cd "$PELICAN_DIR"
php artisan key:generate --force --no-interaction >> "$LOG_FILE" 2>&1
success "Application key generated"
# Helper to set a value in .env
set_env() {
local key="$1"
local value="$2"
if grep -q "^${key}=" "${PELICAN_DIR}/.env" 2>/dev/null; then
# Escape special chars in value for sed
local escaped_value
escaped_value=$(printf '%s' "$value" | sed 's/[&/\]/\\&/g')
sed -i "s|^${key}=.*|${key}=${escaped_value}|" "${PELICAN_DIR}/.env"
else
echo "${key}=${value}" >> "${PELICAN_DIR}/.env"
fi
}
# ── Core settings ──
set_env "APP_NAME" "$APP_NAME"
set_env "APP_URL" "$APP_URL"
set_env "APP_TIMEZONE" "$TIMEZONE"
set_env "APP_ENV" "production"
set_env "APP_DEBUG" "false"
# ── Database settings ──
set_env "DB_CONNECTION" "$laravel_db_connection"
if [[ "$DB_CHOICE" == "sqlite" ]]; then
set_env "DB_DATABASE" "${PELICAN_DIR}/database/database.sqlite"
touch "${PELICAN_DIR}/database/database.sqlite"
else
set_env "DB_HOST" "$DB_HOST"
set_env "DB_PORT" "$DB_PORT"
set_env "DB_DATABASE" "$DB_NAME"
set_env "DB_USERNAME" "$DB_USER"
set_env "DB_PASSWORD" "$DB_PASSWORD"
fi
# ── Cache settings ──
set_env "CACHE_STORE" "$CACHE_DRIVER"
if [[ "$CACHE_DRIVER" == "redis" ]]; then
set_env "REDIS_HOST" "$REDIS_HOST"
set_env "REDIS_PORT" "$REDIS_PORT"
[[ -n "$REDIS_USER" ]] && set_env "REDIS_USERNAME" "$REDIS_USER"
[[ -n "$REDIS_PASSWORD" ]] && set_env "REDIS_PASSWORD" "$REDIS_PASSWORD"
fi
# ── Session settings ──
set_env "SESSION_DRIVER" "$SESSION_DRIVER"
local session_secure="false"
[[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]] && session_secure="true"
set_env "SESSION_SECURE_COOKIE" "$session_secure"
# ── Queue settings ──
set_env "QUEUE_CONNECTION" "$QUEUE_DRIVER"
success ".env configured with all panel settings"
log "ENV: APP_URL=$APP_URL DB=$laravel_db_connection CACHE=$CACHE_DRIVER SESSION=$SESSION_DRIVER QUEUE=$QUEUE_DRIVER"
}
run_database_setup() {
info "Running database migrations..."
cd "$PELICAN_DIR"
php artisan migrate --seed --force >> "$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then
error "Database migration failed — check ${LOG_FILE}"
error "You can retry manually: cd ${PELICAN_DIR} && php artisan migrate --seed --force"
else
success "Database migrations and seeders completed"
fi
}
set_permissions() {
info "Setting file permissions..."
chown -R "${WEBSERVER_USER}:${WEBSERVER_USER}" "$PELICAN_DIR"
chmod -R 775 "${PELICAN_DIR}/storage" "${PELICAN_DIR}/bootstrap/cache"
# Ensure storage subdirectories exist
mkdir -p "${PELICAN_DIR}/storage/framework/"{cache/data,sessions,views}
mkdir -p "${PELICAN_DIR}/storage/logs"
chown -R "${WEBSERVER_USER}:${WEBSERVER_USER}" "${PELICAN_DIR}/storage"
success "Permissions set (owner: ${WEBSERVER_USER}, storage: 775)"
}
create_admin_user() {
if [[ -z "$ADMIN_EMAIL" || -z "$ADMIN_USER" || -z "$ADMIN_PASSWORD" ]]; then
info "Admin user not specified — you'll create one via the web installer at /installer"
return
fi
step "Creating admin user"
cd "$PELICAN_DIR"
# Use artisan tinker to create the admin user
php artisan tinker --execute="
\$user = new \App\Models\User();
\$user->email = '${ADMIN_EMAIL}';
\$user->username = '${ADMIN_USER}';
\$user->password = bcrypt('${ADMIN_PASSWORD}');
\$user->root_admin = true;
\$user->save();
echo 'Admin user created: ${ADMIN_EMAIL}';
" >> "$LOG_FILE" 2>&1
if [[ $? -eq 0 ]]; then
success "Admin user created: ${ADMIN_EMAIL} (${ADMIN_USER})"
# Mark as installed so /installer redirects to login
set_env "APP_INSTALLED" "true"
success "Panel marked as installed — /installer will redirect to dashboard"
echo ""
echo -e " ${GREEN}${BOLD}┌─────────────────────────────────────────────────┐${RESET}"
echo -e " ${GREEN}${BOLD}│ ADMIN CREDENTIALS │${RESET}"
echo -e " ${GREEN}${BOLD}├─────────────────────────────────────────────────┤${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Email: ${WHITE}${ADMIN_EMAIL}${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Username: ${WHITE}${ADMIN_USER}${RESET}"
echo -e " ${GREEN}${BOLD}│${RESET} Password: ${WHITE}${ADMIN_PASSWORD}${RESET}"
echo -e " ${GREEN}${BOLD}└─────────────────────────────────────────────────┘${RESET}"
echo ""
else
warn "Failed to create admin user — you can create one via the web installer at /installer"
fi
}
setup_queue_worker() {
info "Creating queue worker service..."
cat > /etc/systemd/system/pelican-queue.service <<QSVC
# Pelican Queue Worker — auto-installed by pelican-installer
[Unit]
Description=Pelican Queue Worker
After=network.target
[Service]
User=${WEBSERVER_USER}
Group=${WEBSERVER_USER}
Restart=always
ExecStart=/usr/bin/php ${PELICAN_DIR}/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
QSVC
systemctl daemon-reload
systemctl enable --now pelican-queue >> "$LOG_FILE" 2>&1
success "Queue worker service created and running"
}
setup_cron_job() {
info "Setting up scheduled task cron..."
local cron_line="* * * * * php ${PELICAN_DIR}/artisan schedule:run >> /dev/null 2>&1"
(crontab -u "${WEBSERVER_USER}" -l 2>/dev/null | grep -v "schedule:run"; echo "$cron_line") | crontab -u "${WEBSERVER_USER}" -
success "Cron job configured (runs every minute)"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Wings Installation
# ═══════════════════════════════════════════════════════════════════════════════
install_wings() {
step "Installing Docker"
if command -v docker &>/dev/null; then
success "Docker already installed"
else
run_cmd "Installing Docker CE (this may take a minute)" bash -c "curl -sSL https://get.docker.com/ | CHANNEL=stable sh >> '$LOG_FILE' 2>&1"
systemctl enable --now docker >> "$LOG_FILE" 2>&1
success "Docker CE installed and running"
fi
step "Installing Wings daemon"
mkdir -p "$WINGS_DIR" /var/run/wings
local wings_url="${PELICAN_WINGS_BASE}/wings_linux_${WINGS_ARCH}"
run_cmd "Downloading Wings binary (${WINGS_ARCH})" curl -sSL -o "$WINGS_BIN" "$wings_url"
chmod u+x "$WINGS_BIN"
success "Wings binary installed to ${WINGS_BIN}"
step "Setting up Wings systemd service"
cat > /etc/systemd/system/wings.service <<WSVC
[Unit]
Description=Wings Daemon
After=docker.service
Requires=docker.service
PartOf=docker.service
[Service]
User=root
WorkingDirectory=${WINGS_DIR}
LimitNOFILE=4096
PIDFile=/var/run/wings/daemon.pid
ExecStart=${WINGS_BIN}
Restart=on-failure
StartLimitInterval=180
StartLimitBurst=30
RestartSec=5s
[Install]
WantedBy=multi-user.target
WSVC
systemctl daemon-reload
systemctl enable wings >> "$LOG_FILE" 2>&1
success "Wings systemd service created and enabled"
# Write Wings config.yml template if proxy is set up
if [[ "$WINGS_PROXY" == true ]]; then
info "Wings reverse proxy is configured — set api.ssl.enabled: false in config.yml"
info "The reverse proxy terminates SSL, Wings listens on HTTP"
fi
echo ""
echo -e " ${YELLOW}${BOLD}Wings requires configuration from Panel:${RESET}"
echo -e " ${DIM}1. Go to Panel admin → Nodes → Create New${RESET}"
echo -e " ${DIM}2. Copy the configuration YAML from the Configuration tab${RESET}"
echo -e " ${DIM}3. Paste it into: ${WHITE}${WINGS_DIR}/config.yml${RESET}"
if [[ "$WINGS_PROXY" == true ]]; then
echo -e " ${DIM}4. In config.yml, set ${WHITE}api.ssl.enabled: false${RESET}"
echo -e " ${DIM}5. Then start Wings: ${WHITE}sudo systemctl start wings${RESET}"
else
echo -e " ${DIM}4. Then start Wings: ${WHITE}sudo systemctl start wings${RESET}"
fi
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════════
# Uninstall
# ═══════════════════════════════════════════════════════════════════════════════
uninstall() {
echo ""
echo -e " ${RED}${BOLD}⚠ Uninstall Pelican${RESET}"
echo ""
echo -e " ${CYAN}[1]${RESET} Uninstall Panel"
echo -e " ${CYAN}[2]${RESET} Uninstall Wings"
echo -e " ${CYAN}[3]${RESET} Uninstall Both"
echo -e " ${CYAN}[4]${RESET} Cancel"
echo ""
ask "Select option [1-4]:"
read -r choice
case "$choice" in
1) uninstall_panel ;;
2) uninstall_wings ;;
3) uninstall_panel; uninstall_wings ;;
4|*) info "Cancelled."; exit 0 ;;
esac
}
uninstall_panel() {
if ! confirm "Remove all panel files in ${PELICAN_DIR}? (y/N):" "N"; then
info "Aborted."; return
fi
step "Uninstalling Pelican Panel"
systemctl stop pelican-queue 2>/dev/null || true
systemctl disable pelican-queue 2>/dev/null || true
rm -f /etc/systemd/system/pelican-queue.service
rm -f /etc/nginx/sites-enabled/pelican.conf /etc/nginx/sites-available/pelican.conf 2>/dev/null || true
rm -f /etc/apache2/sites-enabled/pelican.conf /etc/apache2/sites-available/pelican.conf 2>/dev/null || true
rm -f /etc/httpd/conf.d/pelican.conf 2>/dev/null || true
rm -rf "$PELICAN_DIR"
systemctl daemon-reload
systemctl restart nginx 2>/dev/null || systemctl restart apache2 2>/dev/null || \
systemctl restart httpd 2>/dev/null || systemctl restart caddy 2>/dev/null || true
success "Pelican Panel uninstalled"
}
uninstall_wings() {
if ! confirm "Stop and remove Wings? (y/N):" "N"; then
info "Aborted."; return
fi
step "Uninstalling Wings"
systemctl stop wings 2>/dev/null || true
systemctl disable wings 2>/dev/null || true
rm -f /etc/systemd/system/wings.service
rm -f "$WINGS_BIN"
rm -f /etc/nginx/sites-enabled/wings.conf /etc/nginx/sites-available/wings.conf 2>/dev/null || true
rm -f /etc/apache2/sites-enabled/wings.conf /etc/apache2/sites-available/wings.conf 2>/dev/null || true
rm -f /etc/httpd/conf.d/wings.conf 2>/dev/null || true
systemctl daemon-reload
if confirm "Also remove Wings config (${WINGS_DIR})? (y/N):" "N"; then
rm -rf "$WINGS_DIR"
fi
success "Wings uninstalled"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Update
# ═══════════════════════════════════════════════════════════════════════════════
update_panel() {
step "Updating Pelican Panel"
[[ ! -d "$PELICAN_DIR" ]] && fatal "Panel not found at ${PELICAN_DIR}"
cd "$PELICAN_DIR"
php artisan down >> "$LOG_FILE" 2>&1 || true
info "Panel in maintenance mode"
cp .env ".env.backup.$(date +%Y%m%d%H%M%S)"
success "Backed up .env"
run_cmd "Downloading latest Pelican Panel" bash -c "curl -sSL '$PELICAN_PANEL_URL' | tar -xzv >> '$LOG_FILE' 2>&1"
run_cmd "Updating PHP dependencies" \
env COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
php artisan migrate --seed --force >> "$LOG_FILE" 2>&1
success "Database migrations applied"
php artisan optimize:clear >> "$LOG_FILE" 2>&1
success "Caches cleared"
# Re-detect webserver user
if [[ "$OS_FAMILY" == "rhel" ]]; then
if systemctl is-active --quiet nginx; then WEBSERVER_USER="nginx"
elif systemctl is-active --quiet httpd; then WEBSERVER_USER="apache"
elif systemctl is-active --quiet caddy; then WEBSERVER_USER="caddy"
else WEBSERVER_USER="nginx"; fi
else
WEBSERVER_USER="www-data"
fi
chown -R "${WEBSERVER_USER}:${WEBSERVER_USER}" "$PELICAN_DIR"
chmod -R 775 "${PELICAN_DIR}/storage" "${PELICAN_DIR}/bootstrap/cache"
systemctl restart pelican-queue >> "$LOG_FILE" 2>&1 || true
php artisan queue:restart >> "$LOG_FILE" 2>&1 || true
php artisan up >> "$LOG_FILE" 2>&1
success "Panel updated and back online!"
}
update_wings() {
step "Updating Wings"
systemctl stop wings >> "$LOG_FILE" 2>&1 || true
run_cmd "Downloading latest Wings binary" curl -sSL -o "$WINGS_BIN" "${PELICAN_WINGS_BASE}/wings_linux_${WINGS_ARCH}"
chmod u+x "$WINGS_BIN"
systemctl start wings >> "$LOG_FILE" 2>&1
success "Wings updated and restarted!"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Health Checks
# ═══════════════════════════════════════════════════════════════════════════════
health_check() {
step "Running post-install health checks"
local passed=0
local failed=0
check_pass() { success "$1"; passed=$((passed + 1)); }
check_fail() { error "$1"; failed=$((failed + 1)); }
# PHP
if php -v &>/dev/null; then
check_pass "PHP $(php -v | head -1 | awk '{print $2}') is working"
else
check_fail "PHP is not working"
fi
# Web server
if [[ "$INSTALL_PANEL" == true || "$WINGS_PROXY" == true ]]; then
local ws_active=false
for svc in nginx apache2 httpd caddy; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
check_pass "${svc^} is running"
ws_active=true
break
fi
done
[[ "$ws_active" == false ]] && check_fail "No web server is running"
fi
if [[ "$INSTALL_PANEL" == true ]]; then
# Panel files
if [[ -f "${PELICAN_DIR}/artisan" ]]; then
check_pass "Panel files present at ${PELICAN_DIR}"
else
check_fail "Panel files missing"
fi
# .env has APP_KEY
if grep -q "^APP_KEY=base64:" "${PELICAN_DIR}/.env" 2>/dev/null; then
check_pass ".env has APP_KEY"
else
check_fail ".env missing or no APP_KEY"
fi
# .env has DB_CONNECTION
if grep -q "^DB_CONNECTION=" "${PELICAN_DIR}/.env" 2>/dev/null; then
check_pass ".env has database config"
else
check_fail ".env missing database config"
fi
# .env has cache/session/queue config
for setting in CACHE_STORE SESSION_DRIVER QUEUE_CONNECTION; do
if grep -q "^${setting}=" "${PELICAN_DIR}/.env" 2>/dev/null; then
check_pass ".env has ${setting}"
else
check_fail ".env missing ${setting}"
fi
done
# Queue worker
if systemctl is-active --quiet pelican-queue; then
check_pass "Queue worker is running"
else
check_fail "Queue worker is not running"
fi
# Artisan CLI
if cd "$PELICAN_DIR" && php artisan --version >> "$LOG_FILE" 2>&1; then
check_pass "Artisan CLI works"
else
check_fail "Artisan CLI errors (check ${LOG_FILE})"
fi
# HTTP check
if [[ -n "$FQDN" ]]; then
local proto="http"
[[ "$CONFIGURE_SSL" == true || "${WEBSERVER:-}" == "caddy" ]] && proto="https"
local http_code
http_code=$(curl -sSo /dev/null -w '%{http_code}' --max-time 10 "${proto}://${FQDN}" 2>/dev/null || echo "000")
if [[ "$http_code" =~ ^(200|302|301)$ ]]; then
check_pass "Panel responds at ${proto}://${FQDN} (HTTP ${http_code})"
else
check_fail "Panel not responding at ${proto}://${FQDN} (HTTP ${http_code})"
fi
fi
# Database service
if [[ "$DB_CHOICE" != "sqlite" ]]; then
case "$DB_CHOICE" in
mysql) systemctl is-active --quiet mysql 2>/dev/null || systemctl is-active --quiet mysqld 2>/dev/null && check_pass "MySQL is running" || check_fail "MySQL not running" ;;
mariadb) systemctl is-active --quiet mariadb && check_pass "MariaDB is running" || check_fail "MariaDB not running" ;;
postgres) systemctl is-active --quiet postgresql && check_pass "PostgreSQL is running" || check_fail "PostgreSQL not running" ;;
esac
fi
# Redis (if used)
if needs_redis; then
if systemctl is-active --quiet redis-server 2>/dev/null || systemctl is-active --quiet redis 2>/dev/null; then
check_pass "Redis is running"
else
check_fail "Redis not running"
fi
fi
fi
# Wings
if [[ "$INSTALL_WINGS_FLAG" == true ]]; then
[[ -x "$WINGS_BIN" ]] && check_pass "Wings binary installed" || check_fail "Wings binary missing"
systemctl is-active --quiet docker && check_pass "Docker is running" || check_fail "Docker not running"
fi
echo ""
if [[ $failed -eq 0 ]]; then
echo -e " ${GREEN}${BOLD}✓ All ${passed} checks passed!${RESET}"
else
echo -e " ${YELLOW}${BOLD}⚠ ${passed} passed, ${failed} failed — review issues above${RESET}"
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# Interactive Menus
# ═══════════════════════════════════════════════════════════════════════════════
select_components() {
echo ""
echo -e " ${BOLD}What would you like to install?${RESET}"
echo ""
echo -e " ${CYAN}[1]${RESET} Panel only"
echo -e " ${CYAN}[2]${RESET} Wings only"
echo -e " ${CYAN}[3]${RESET} Panel + Wings ${DIM}(same machine)${RESET}"
echo ""
ask "Select [1-3]:"
read -r comp
case "${comp:-1}" in
1) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;;
2) INSTALL_PANEL=false; INSTALL_WINGS_FLAG=true ;;
3) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=true ;;
*) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;;
esac
}
collect_panel_options() {
echo ""
echo -e " ${BOLD}${WHITE}Panel Configuration${RESET}"
echo -e " ${DIM}$(printf '%.0s─' {1..40})${RESET}"
# ── App Name ──
if [[ "$ASSUME_YES" != true ]]; then
echo ""
ask "Application name [${APP_NAME}]:"
read -r input_app_name
APP_NAME="${input_app_name:-$APP_NAME}"
fi
# ── Domain ──
if [[ -z "$FQDN" ]]; then
echo ""
ask "Panel domain (e.g., panel.example.com):"
read -r FQDN
[[ -z "$FQDN" ]] && fatal "Domain is required."
fi
# Validate FQDN
if [[ ! "$FQDN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]]; then
warn "Domain '${FQDN}' looks unusual — double-check it's correct"
fi
# ── Web server ──
if [[ -z "$WEBSERVER" ]]; then
echo ""
echo -e " ${BOLD}Web server:${RESET}"
echo -e " ${CYAN}[1]${RESET} Nginx ${DIM}(recommended)${RESET}"
echo -e " ${CYAN}[2]${RESET} Apache"
echo -e " ${CYAN}[3]${RESET} Caddy ${DIM}(automatic SSL, easiest setup)${RESET}"
echo ""
ask "Select [1-3] (default: 1):"
read -r ws
case "${ws:-1}" in
1) WEBSERVER="nginx" ;;
2) WEBSERVER="apache" ;;
3) WEBSERVER="caddy" ;;
*) WEBSERVER="nginx" ;;
esac
fi
update_webserver_user
# ── Database ──
if [[ -z "$DB_CHOICE" ]]; then
echo ""
echo -e " ${BOLD}Database:${RESET}"
echo -e " ${CYAN}[1]${RESET} SQLite ${DIM}(simplest, no server needed)${RESET}"
echo -e " ${CYAN}[2]${RESET} MySQL"
echo -e " ${CYAN}[3]${RESET} MariaDB"
echo -e " ${CYAN}[4]${RESET} PostgreSQL ${DIM}(recommended for production)${RESET}"
echo ""
ask "Select [1-4] (default: 1):"
read -r db
case "${db:-1}" in
1) DB_CHOICE="sqlite" ;;
2) DB_CHOICE="mysql" ;;
3) DB_CHOICE="mariadb" ;;
4) DB_CHOICE="postgres" ;;
*) DB_CHOICE="sqlite" ;;
esac
fi
auto_set_db_port
generate_db_password
# Custom DB credentials (skip for sqlite)
if [[ "$DB_CHOICE" != "sqlite" && "$ASSUME_YES" != true ]]; then
echo ""
ask "Database name [${DB_NAME}]:"
read -r input_db_name
DB_NAME="${input_db_name:-$DB_NAME}"
ask "Database user [${DB_USER}]:"
read -r input_db_user
DB_USER="${input_db_user:-$DB_USER}"
ask "Database password [auto-generated]:"
read -r -s input_db_pass
echo ""
DB_PASSWORD="${input_db_pass:-$DB_PASSWORD}"
fi
# ── Cache ──
if [[ -z "$CACHE_DRIVER" ]]; then
echo ""
echo -e " ${BOLD}Cache driver:${RESET}"
echo -e " ${CYAN}[1]${RESET} Filesystem ${DIM}(default, no extra setup)${RESET}"
echo -e " ${CYAN}[2]${RESET} Redis ${DIM}(faster, recommended for production)${RESET}"
echo ""
ask "Select [1-2] (default: 1):"
read -r cache_choice
case "${cache_choice:-1}" in
1) CACHE_DRIVER="file" ;;
2) CACHE_DRIVER="redis" ;;
*) CACHE_DRIVER="file" ;;
esac
fi
# Redis settings (if needed by any driver)
if [[ "$CACHE_DRIVER" == "redis" && "$ASSUME_YES" != true ]]; then
echo ""
echo -e " ${BOLD}Redis configuration:${RESET}"
ask "Redis host [${REDIS_HOST}]:"
read -r input_redis_host
REDIS_HOST="${input_redis_host:-$REDIS_HOST}"
ask "Redis port [${REDIS_PORT}]:"
read -r input_redis_port
REDIS_PORT="${input_redis_port:-$REDIS_PORT}"
ask "Redis password [none]:"
read -r -s input_redis_pass
echo ""
REDIS_PASSWORD="${input_redis_pass:-$REDIS_PASSWORD}"
fi
# ── Session ──
if [[ -z "$SESSION_DRIVER" ]]; then
echo ""
echo -e " ${BOLD}Session driver:${RESET}"
echo -e " ${CYAN}[1]${RESET} Filesystem ${DIM}(default)${RESET}"
echo -e " ${CYAN}[2]${RESET} Database"
echo -e " ${CYAN}[3]${RESET} Cookie"
if [[ "$CACHE_DRIVER" == "redis" ]]; then
echo -e " ${CYAN}[4]${RESET} Redis ${DIM}(fastest)${RESET}"
fi
echo ""
local session_default=1
ask "Select [1-$([[ "$CACHE_DRIVER" == "redis" ]] && echo "4" || echo "3")] (default: 1):"
read -r session_choice
case "${session_choice:-1}" in
1) SESSION_DRIVER="file" ;;
2) SESSION_DRIVER="database" ;;
3) SESSION_DRIVER="cookie" ;;
4) [[ "$CACHE_DRIVER" == "redis" ]] && SESSION_DRIVER="redis" || SESSION_DRIVER="file" ;;
*) SESSION_DRIVER="file" ;;
esac
fi
# ── Queue ──
if [[ -z "$QUEUE_DRIVER" ]]; then
echo ""
echo -e " ${BOLD}Queue driver:${RESET}"
echo -e " ${CYAN}[1]${RESET} Database ${DIM}(default, reliable)${RESET}"
if [[ "$CACHE_DRIVER" == "redis" ]]; then
echo -e " ${CYAN}[2]${RESET} Redis ${DIM}(faster)${RESET}"
fi
echo -e " ${CYAN}[3]${RESET} Sync ${DIM}(immediate, for testing only)${RESET}"
echo ""
ask "Select [1-3] (default: 1):"
read -r queue_choice
case "${queue_choice:-1}" in
1) QUEUE_DRIVER="database" ;;
2) [[ "$CACHE_DRIVER" == "redis" ]] && QUEUE_DRIVER="redis" || QUEUE_DRIVER="database" ;;
3) QUEUE_DRIVER="sync" ;;
*) QUEUE_DRIVER="database" ;;
esac
fi
# ── SSL ──
if [[ -z "$CONFIGURE_SSL" ]]; then
if [[ "$WEBSERVER" == "caddy" ]]; then
CONFIGURE_SSL=false # Caddy handles it
info "Caddy handles SSL automatically"
else
echo ""
if confirm "Enable SSL with Let's Encrypt? (Y/n):" "Y"; then
CONFIGURE_SSL=true
if [[ -z "$EMAIL" ]]; then
ask "Email for Let's Encrypt certificates:"
read -r EMAIL
[[ -z "$EMAIL" ]] && fatal "Email is required for SSL"
fi
else
CONFIGURE_SSL=false
fi
fi
fi
# ── Admin user ──
if [[ -z "$ADMIN_EMAIL" && "$ASSUME_YES" != true ]]; then
echo ""
echo -e " ${BOLD}Admin account ${DIM}(optional — skip to use web installer)${RESET}"
ask "Admin email (leave empty to skip):"
read -r ADMIN_EMAIL
if [[ -n "$ADMIN_EMAIL" ]]; then
ask "Admin username:"
read -r ADMIN_USER
ask "Admin password:"
read -r -s ADMIN_PASSWORD
echo ""
fi
fi
# ── Timezone ──
if [[ "$ASSUME_YES" != true ]]; then
echo ""
ask "Timezone [${TIMEZONE}]:"
read -r input_tz
TIMEZONE="${input_tz:-$TIMEZONE}"
fi
}
collect_wings_options() {
if [[ "$ASSUME_YES" == true ]]; then
return
fi
echo ""
echo -e " ${BOLD}${WHITE}Wings Configuration${RESET}"
echo -e " ${DIM}$(printf '%.0s─' {1..40})${RESET}"
# Wings reverse proxy
if [[ -z "$WINGS_PROXY" ]]; then
echo ""
if confirm "Set up a reverse proxy for Wings? (Y/n):" "Y"; then
WINGS_PROXY=true
else
WINGS_PROXY=false
fi
fi
if [[ "$WINGS_PROXY" == true ]]; then
if [[ -z "$WINGS_DOMAIN" ]]; then
ask "Wings node domain (e.g., node.example.com):"
read -r WINGS_DOMAIN
[[ -z "$WINGS_DOMAIN" ]] && { warn "No Wings domain specified — skipping proxy"; WINGS_PROXY=false; return; }
fi
ask "Wings daemon port [${WINGS_PORT}]:"
read -r input_wings_port
WINGS_PORT="${input_wings_port:-$WINGS_PORT}"
if [[ -z "$WINGS_SSL" ]]; then
if [[ "$WEBSERVER" == "caddy" ]]; then
WINGS_SSL=false # Caddy handles it
info "Caddy handles Wings SSL automatically"
elif [[ "$CONFIGURE_SSL" == true ]]; then
if confirm "Enable SSL for Wings reverse proxy too? (Y/n):" "Y"; then
WINGS_SSL=true
else
WINGS_SSL=false
fi
fi
fi
# Need a webserver choice for wings-only installs
if [[ "$INSTALL_PANEL" != true && -z "$WEBSERVER" ]]; then
echo ""
echo -e " ${BOLD}Web server for Wings reverse proxy:${RESET}"
echo -e " ${CYAN}[1]${RESET} Nginx ${DIM}(recommended)${RESET}"
echo -e " ${CYAN}[2]${RESET} Apache"
echo -e " ${CYAN}[3]${RESET} Caddy ${DIM}(automatic SSL)${RESET}"
echo ""
ask "Select [1-3] (default: 1):"
read -r ws
case "${ws:-1}" in
1) WEBSERVER="nginx" ;;
2) WEBSERVER="apache" ;;
3) WEBSERVER="caddy" ;;
*) WEBSERVER="nginx" ;;
esac
fi
fi
}
show_summary() {
echo ""
echo -e " ${CYAN}${BOLD}╔═══════════════════════════════════════════╗${RESET}"
echo -e " ${CYAN}${BOLD}║ Installation Summary ║${RESET}"
echo -e " ${CYAN}${BOLD}╚═══════════════════════════════════════════╝${RESET}"
echo ""
if [[ "$INSTALL_PANEL" == true ]]; then
echo -e " ${BOLD}Panel:${RESET}"
echo -e " App name: ${GREEN}${APP_NAME}${RESET}"
echo -e " Domain: ${GREEN}${FQDN}${RESET}"
echo -e " Web server: ${GREEN}${WEBSERVER}${RESET}"
echo -e " Database: ${GREEN}${DB_CHOICE}${RESET}"
if [[ "$DB_CHOICE" != "sqlite" ]]; then
echo -e " DB name: ${GREEN}${DB_NAME}${RESET}"
echo -e " DB user: ${GREEN}${DB_USER}${RESET}"
fi
echo -e " Cache: ${GREEN}${CACHE_DRIVER}${RESET}"
echo -e " Session: ${GREEN}${SESSION_DRIVER}${RESET}"
echo -e " Queue: ${GREEN}${QUEUE_DRIVER}${RESET}"
echo -e " SSL: ${GREEN}$([[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]] && echo "Yes" || echo "No")${RESET}"
echo -e " Timezone: ${GREEN}${TIMEZONE}${RESET}"
echo -e " PHP: ${GREEN}${PHP_VERSION}${RESET}"
if [[ -n "$ADMIN_EMAIL" ]]; then
echo -e " Admin: ${GREEN}${ADMIN_EMAIL} (${ADMIN_USER})${RESET}"
else
echo -e " Admin: ${DIM}via web installer${RESET}"
fi
echo ""
fi
if [[ "$INSTALL_WINGS_FLAG" == true ]]; then
echo -e " ${BOLD}Wings:${RESET}"
echo -e " Architecture: ${GREEN}${WINGS_ARCH}${RESET}"
echo -e " Docker: ${GREEN}Will be installed${RESET}"
if [[ "$WINGS_PROXY" == true ]]; then
echo -e " Proxy: ${GREEN}${WEBSERVER} → ${WINGS_DOMAIN}:${WINGS_PORT}${RESET}"
echo -e " Proxy SSL: ${GREEN}$([[ "$WINGS_SSL" == true || "$WEBSERVER" == "caddy" ]] && echo "Yes" || echo "No")${RESET}"
else
echo -e " Proxy: ${DIM}Not configured${RESET}"
fi
echo ""
fi
if ! confirm "Proceed with installation? (Y/n):" "Y"; then
info "Installation cancelled."
exit 0
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# Completion Messages
# ═══════════════════════════════════════════════════════════════════════════════
print_complete() {
local proto="http"
[[ "$CONFIGURE_SSL" == true || "$WEBSERVER" == "caddy" ]] && proto="https"
echo ""
echo -e " ${GREEN}${BOLD}╔═══════════════════════════════════════════════════════════╗${RESET}"
echo -e " ${GREEN}${BOLD}║ ║${RESET}"
echo -e " ${GREEN}${BOLD}║ 🎉 Installation completed successfully! 🎉 ║${RESET}"
echo -e " ${GREEN}${BOLD}║ ║${RESET}"
echo -e " ${GREEN}${BOLD}╚═══════════════════════════════════════════════════════════╝${RESET}"
echo ""
if [[ "$INSTALL_PANEL" == true ]]; then
echo -e " ${BOLD}Panel — next steps:${RESET}"
if [[ -n "$ADMIN_EMAIL" ]]; then
echo -e " ${WHITE}1.${RESET} Open ${CYAN}${proto}://${FQDN}${RESET} and log in"
echo -e " Email: ${WHITE}${ADMIN_EMAIL}${RESET}"
echo -e " Username: ${WHITE}${ADMIN_USER}${RESET}"
else
echo -e " ${WHITE}1.${RESET} Open ${CYAN}${proto}://${FQDN}/installer${RESET} in your browser"
echo -e " ${WHITE}2.${RESET} Complete the web-based setup wizard"
echo -e " ${DIM}(Database, cache, session, and queue are already configured!)${RESET}"
echo -e " ${WHITE}3.${RESET} Create your admin account"
fi
echo ""
fi
if [[ "$INSTALL_WINGS_FLAG" == true ]]; then
echo -e " ${BOLD}Wings — next steps:${RESET}"
echo -e " ${WHITE}1.${RESET} Create a node in Panel admin → Nodes → Create New"
if [[ "$WINGS_PROXY" == true ]]; then
local wings_proto="http"
[[ "$WINGS_SSL" == true || "$WEBSERVER" == "caddy" ]] && wings_proto="https"
echo -e " Use FQDN: ${CYAN}${WINGS_DOMAIN}${RESET}"
echo -e " Set 'Communicate Over SSL' to ${CYAN}$([[ "$wings_proto" == "https" ]] && echo "Use SSL" || echo "No SSL")${RESET}"
echo -e " Set 'Behind Proxy' to ${CYAN}Behind Proxy${RESET}"
fi
echo -e " ${WHITE}2.${RESET} Copy the config YAML into ${CYAN}${WINGS_DIR}/config.yml${RESET}"
if [[ "$WINGS_PROXY" == true ]]; then
echo -e " ${WHITE}3.${RESET} In config.yml, ensure: ${CYAN}api.ssl.enabled: false${RESET}"
echo -e " ${DIM}(The reverse proxy terminates SSL — Wings listens on plain HTTP)${RESET}"
echo -e " ${WHITE}4.${RESET} Test: ${CYAN}sudo wings --debug${RESET}"
echo -e " ${WHITE}5.${RESET} Start: ${CYAN}sudo systemctl start wings${RESET}"
else
echo -e " ${WHITE}3.${RESET} Test: ${CYAN}sudo wings --debug${RESET}"
echo -e " ${WHITE}4.${RESET} Start: ${CYAN}sudo systemctl start wings${RESET}"
fi
echo ""
fi
echo -e " ${BOLD}Useful commands:${RESET}"
echo -e " Update: ${CYAN}sudo bash install.sh update --both${RESET}"
echo -e " Uninstall: ${CYAN}sudo bash install.sh uninstall${RESET}"
echo -e " Logs: ${CYAN}tail -f ${LOG_FILE}${RESET}"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
# Initialize log
mkdir -p "$(dirname "$LOG_FILE")"
echo "=== Pelican Installer v${INSTALLER_VERSION} — $(date) ===" >> "$LOG_FILE"
print_header
detect_os
preflight_checks
# Determine action
if [[ -z "$ACTION" ]]; then
echo ""
echo -e " ${BOLD}What would you like to do?${RESET}"
echo ""
echo -e " ${CYAN}[1]${RESET} Install Panel / Wings"
echo -e " ${CYAN}[2]${RESET} Update Panel / Wings"
echo -e " ${CYAN}[3]${RESET} Uninstall"
echo ""
ask "Select [1-3] (default: 1):"
read -r action_choice
case "${action_choice:-1}" in
1) ACTION="install" ;;
2) ACTION="update" ;;
3) ACTION="uninstall" ;;
*) ACTION="install" ;;
esac
fi
case "$ACTION" in
install)
# If components not set via flags, ask
if [[ -z "$INSTALL_PANEL" && -z "$INSTALL_WINGS_FLAG" ]]; then
select_components
fi
INSTALL_PANEL="${INSTALL_PANEL:-false}"
INSTALL_WINGS_FLAG="${INSTALL_WINGS_FLAG:-false}"
# Calculate total steps for progress indicator
TOTAL_STEPS=2 # detect + preflight (already done)
CURRENT_STEP=2
if [[ "$INSTALL_PANEL" == true ]]; then
TOTAL_STEPS=$((TOTAL_STEPS + 7)) # php, redis?, db, webserver, ssl, panel-download, panel-config
fi
if [[ "$INSTALL_WINGS_FLAG" == true ]]; then
TOTAL_STEPS=$((TOTAL_STEPS + 3)) # docker, wings-download, wings-service
if [[ "$WINGS_PROXY" == true && "$INSTALL_PANEL" != true ]]; then
TOTAL_STEPS=$((TOTAL_STEPS + 1)) # webserver for wings only
fi
fi
TOTAL_STEPS=$((TOTAL_STEPS + 1)) # health check
if [[ "$INSTALL_PANEL" == true ]]; then
detect_php_version
collect_panel_options
fi
if [[ "$INSTALL_WINGS_FLAG" == true ]]; then
collect_wings_options
fi
show_summary
if [[ "$INSTALL_PANEL" == true ]]; then
install_php
install_redis
install_database
install_webserver
install_ssl
install_panel
elif [[ "$WINGS_PROXY" == true ]]; then
# Wings-only with reverse proxy needs a web server
install_webserver
install_ssl
fi
if [[ "$INSTALL_WINGS_FLAG" == true ]]; then
install_wings
fi
health_check
print_complete
;;
update)
if [[ -z "$INSTALL_PANEL" && -z "$INSTALL_WINGS_FLAG" ]]; then
echo ""
echo -e " ${BOLD}What would you like to update?${RESET}"
echo ""
echo -e " ${CYAN}[1]${RESET} Panel"
echo -e " ${CYAN}[2]${RESET} Wings"
echo -e " ${CYAN}[3]${RESET} Both"
echo ""
ask "Select [1-3]:"
read -r upd
case "${upd:-1}" in
1) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;;
2) INSTALL_PANEL=false; INSTALL_WINGS_FLAG=true ;;
3) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=true ;;
*) INSTALL_PANEL=true; INSTALL_WINGS_FLAG=false ;;
esac
fi
detect_php_version
[[ "$INSTALL_PANEL" == true ]] && update_panel
[[ "$INSTALL_WINGS_FLAG" == true ]] && update_wings
echo ""
echo -e " ${GREEN}${BOLD}✓ Update complete!${RESET}"
echo ""
;;
uninstall)
uninstall
;;
*)
fatal "Unknown action: ${ACTION}"
;;
esac
log "Installer finished at $(date)"
}
# ─── Run ───────────────────────────────────────────────────────────────────────
main "$@"