#!/usr/bin/env bash set -Eeuo pipefail LOG_FILE="${LOG_FILE:-/tmp/ubuntu2404-laravel-base.log}" PHP_VERSION="${PHP_VERSION:-8.3}" NODE_MAJOR="${NODE_MAJOR:-20}" LARAVEL_PROJECTS_DIR="${LARAVEL_PROJECTS_DIR:-/srv/laravel-projects}" INSTALL_CORE=1 INSTALL_PHP=1 INSTALL_COMPOSER=1 INSTALL_NODE=1 INSTALL_XFCE=1 INSTALL_VSCODE=1 INSTALL_XRDP=1 CONFIGURE_FIREWALL=1 INTERACTIVE=1 declare -a INSTALLED_COMPONENTS=() declare -a SKIPPED_COMPONENTS=() if [[ -n "${SUDO_USER:-}" ]]; then WORKSTATION_USER="$SUDO_USER" else WORKSTATION_USER="${USER}" fi usage() { cat <<'EOF' Usage: ubuntu2404-laravel-base.sh [options] Options: --php-version PHP major.minor to install (default: 8.3) --node-major Node.js major release (default: 20) --projects-dir Root directory for Laravel projects (default: /srv/laravel-projects) --without-core Skip core packages (nginx, mysql, redis, supervisor, tools) --without-php Skip PHP installation and PHP tuning --without-composer Skip Composer installation --without-node Skip Node.js installation --without-xfce Skip XFCE desktop packages --without-vscode Skip Visual Studio Code installation --without-xrdp Skip XRDP installation --without-firewall Skip UFW configuration --non-interactive Do not ask questions before installation --workstation-user User that will own desktop/workspace directories --help Show this message Example: sudo bash scripts/install/ubuntu2404-laravel-base.sh --projects-dir /srv/laravel-projects EOF } log() { local level="$1" shift printf '[%s] %s\n' "$level" "$*" >&2 if [[ -n "${LOG_FILE:-}" ]]; then printf '[%s] %s\n' "$level" "$*" >> "$LOG_FILE" 2>/dev/null || true fi } info() { log INFO "$@"; } warn() { log WARN "$@"; } fail() { log ERROR "$@"; exit 1; } on_error() { local exit_code="$1" local line_no="$2" local command_text="$3" log ERROR "Installer failed at line ${line_no}: ${command_text} (exit ${exit_code})" log ERROR "See log file: ${LOG_FILE}" exit "$exit_code" } trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR tty_available() { [[ -r /dev/tty ]] } command_version() { local command_name="$1" shift || true local output='' if ! command -v "$command_name" >/dev/null 2>&1; then printf 'missing' return 0 fi if [[ $# -gt 0 ]]; then output="$({ "$@" 2>&1 || true; } | head -n 1)" else output="$({ "$command_name" --version 2>&1 || true; } | head -n 1)" fi if [[ -n "$output" ]]; then printf '%s' "$output" else printf 'installed' fi return 0 } component_state() { local value="$1" if [[ "$value" == "missing" || -z "$value" ]]; then printf 'missing' else printf 'present' fi } print_component_line() { local label="$1" local state="$2" local detail="$3" printf ' - %-18s : %-8s %s\n' "$label" "$state" "$detail" } prompt_yes_no() { local prompt_text="$1" local default_value="$2" local answer='' local suffix='[Y/n]' if [[ "$default_value" == 'n' ]]; then suffix='[y/N]' fi if ! tty_available; then [[ "$default_value" == 'y' ]] return fi while true; do printf '%s %s ' "$prompt_text" "$suffix" > /dev/tty IFS= read -r answer < /dev/tty || answer='' answer="${answer,,}" if [[ -z "$answer" ]]; then [[ "$default_value" == 'y' ]] return fi case "$answer" in y|yes) return 0 ;; n|no) return 1 ;; esac done } record_component() { local label="$1" local status="$2" if [[ "$status" == 'installed' ]]; then INSTALLED_COMPONENTS+=("$label") else SKIPPED_COMPONENTS+=("$label") fi } normalize_vscode_apt_source_conflicts() { local source_file='' local removed_any=0 while IFS= read -r source_file; do [[ -n "$source_file" ]] || continue run_root rm -f "$source_file" removed_any=1 done < <(grep -l "packages.microsoft.com/repos/code" /etc/apt/sources.list.d/*.list 2>/dev/null || true) if [[ $removed_any -eq 1 ]]; then info 'Removed conflicting VS Code apt source entries' fi } should_run_apt_work() { [[ $INSTALL_CORE -eq 1 || \ $INSTALL_PHP -eq 1 || \ $INSTALL_COMPOSER -eq 1 || \ $INSTALL_NODE -eq 1 || \ $INSTALL_VSCODE -eq 1 || \ $INSTALL_XFCE -eq 1 || \ $INSTALL_XRDP -eq 1 ]] } run_root() { if [[ ${EUID} -eq 0 ]]; then "$@" else sudo "$@" fi } run_root_env() { if [[ ${EUID} -eq 0 ]]; then env "$@" else sudo env "$@" fi } run_as_user() { local target_user="$1" shift if [[ ${EUID} -eq 0 ]]; then runuser -u "$target_user" -- "$@" else sudo -u "$target_user" "$@" fi } while [[ $# -gt 0 ]]; do case "$1" in --php-version) PHP_VERSION="$2" shift 2 ;; --node-major) NODE_MAJOR="$2" shift 2 ;; --projects-dir) LARAVEL_PROJECTS_DIR="$2" shift 2 ;; --without-core) INSTALL_CORE=0 shift ;; --without-php) INSTALL_PHP=0 shift ;; --without-composer) INSTALL_COMPOSER=0 shift ;; --without-node) INSTALL_NODE=0 shift ;; --without-xfce) INSTALL_XFCE=0 shift ;; --without-vscode) INSTALL_VSCODE=0 shift ;; --without-xrdp) INSTALL_XRDP=0 shift ;; --without-firewall) CONFIGURE_FIREWALL=0 shift ;; --non-interactive) INTERACTIVE=0 shift ;; --workstation-user) WORKSTATION_USER="$2" shift 2 ;; --help|-h) usage exit 0 ;; *) fail "Unknown option: $1" ;; esac done info "Starting Ubuntu 24.04 Laravel base installer" info "Execution user: $(id -un)" info "Log file: $LOG_FILE" if [[ ! -f /etc/os-release ]]; then fail "Cannot detect operating system" fi # shellcheck disable=SC1091 source /etc/os-release if [[ "${ID:-}" != "ubuntu" || "${VERSION_ID:-}" != "24.04" ]]; then fail "This script supports Ubuntu 24.04 only" fi if ! id -u "$WORKSTATION_USER" >/dev/null 2>&1; then fail "User not found: $WORKSTATION_USER" fi PHP_PACKAGES=( "php${PHP_VERSION}" "php${PHP_VERSION}-fpm" "php${PHP_VERSION}-cli" "php${PHP_VERSION}-common" "php${PHP_VERSION}-curl" "php${PHP_VERSION}-mbstring" "php${PHP_VERSION}-xml" "php${PHP_VERSION}-zip" "php${PHP_VERSION}-mysql" "php${PHP_VERSION}-sqlite3" "php${PHP_VERSION}-bcmath" "php${PHP_VERSION}-intl" "php${PHP_VERSION}-gd" "php${PHP_VERSION}-imagick" "php${PHP_VERSION}-redis" "php${PHP_VERSION}-opcache" ) BASE_PACKAGES=( acl apt-transport-https ca-certificates curl ffmpeg git gnupg htop imagemagick jq lsb-release net-tools nginx redis-server software-properties-common supervisor tree unzip ufw vim wget mysql-server ) show_preflight_screen() { local os_label="${PRETTY_NAME:-Ubuntu 24.04}" local php_detail local composer_detail local node_detail local npm_detail local mysql_detail local nginx_detail local redis_detail local code_detail local xfce_detail local xrdp_detail php_detail=$(command_version php php -v) composer_detail=$(command_version composer composer --version) node_detail=$(command_version node node --version) npm_detail=$(command_version npm npm --version) mysql_detail=$(command_version mysql mysql --version) nginx_detail=$(command_version nginx nginx -v) redis_detail=$(command_version redis-server redis-server --version) code_detail=$(if dpkg -s code >/dev/null 2>&1; then dpkg-query -W -f='VS Code package ${Version}' code 2>/dev/null || printf 'VS Code installed'; else printf 'missing'; fi) xfce_detail=$(if dpkg -s xfce4 >/dev/null 2>&1; then printf 'xfce4 installed'; else printf 'missing'; fi) xrdp_detail=$(if dpkg -s xrdp >/dev/null 2>&1; then printf 'xrdp installed'; else printf 'missing'; fi) printf '\n' printf '============================================================\n' printf ' Laravel Base Installer - Preflight\n' printf '============================================================\n' printf ' Machine : %s\n' "$(hostname)" printf ' System : %s\n' "$os_label" printf ' User : %s\n' "$WORKSTATION_USER" printf ' Projects dir : %s\n' "$LARAVEL_PROJECTS_DIR" printf '\n' printf 'Current status:\n' print_component_line 'PHP' "$(component_state "$php_detail")" "$php_detail" print_component_line 'Composer' "$(component_state "$composer_detail")" "$composer_detail" print_component_line 'Node.js' "$(component_state "$node_detail")" "$node_detail" print_component_line 'npm' "$(component_state "$npm_detail")" "$npm_detail" print_component_line 'MySQL' "$(component_state "$mysql_detail")" "$mysql_detail" print_component_line 'Nginx' "$(component_state "$nginx_detail")" "$nginx_detail" print_component_line 'Redis' "$(component_state "$redis_detail")" "$redis_detail" print_component_line 'VS Code' "$(component_state "$code_detail")" "$code_detail" print_component_line 'XFCE' "$(component_state "$xfce_detail")" "$xfce_detail" print_component_line 'XRDP' "$(component_state "$xrdp_detail")" "$xrdp_detail" printf '\n' printf 'Planned install set:\n' print_component_line 'Core stack' "$([[ $INSTALL_CORE -eq 1 ]] && printf enabled || printf skipped)" 'nginx, mysql, redis, supervisor, common tools' print_component_line 'PHP stack' "$([[ $INSTALL_PHP -eq 1 ]] && printf enabled || printf skipped)" "php${PHP_VERSION} and extensions" print_component_line 'Composer' "$([[ $INSTALL_COMPOSER -eq 1 ]] && printf enabled || printf skipped)" 'global composer binary' print_component_line 'Node.js' "$([[ $INSTALL_NODE -eq 1 ]] && printf enabled || printf skipped)" "Node ${NODE_MAJOR}.x" print_component_line 'VS Code' "$([[ $INSTALL_VSCODE -eq 1 ]] && printf enabled || printf skipped)" 'desktop IDE' print_component_line 'XFCE' "$([[ $INSTALL_XFCE -eq 1 ]] && printf enabled || printf skipped)" 'light desktop environment' print_component_line 'XRDP' "$([[ $INSTALL_XRDP -eq 1 ]] && printf enabled || printf skipped)" 'remote desktop service' print_component_line 'Firewall' "$([[ $CONFIGURE_FIREWALL -eq 1 ]] && printf enabled || printf skipped)" 'OpenSSH, HTTP, HTTPS, XRDP' printf '\n' } run_interactive_preflight() { if [[ $INTERACTIVE -ne 1 ]]; then return fi if ! tty_available; then warn 'No tty available: proceeding with current flags without prompts.' return fi show_preflight_screen prompt_yes_no 'Install core stack and common tools?' 'y' && INSTALL_CORE=1 || INSTALL_CORE=0 prompt_yes_no "Install PHP ${PHP_VERSION} stack?" 'y' && INSTALL_PHP=1 || INSTALL_PHP=0 prompt_yes_no 'Install Composer?' 'y' && INSTALL_COMPOSER=1 || INSTALL_COMPOSER=0 prompt_yes_no "Install Node.js ${NODE_MAJOR}.x?" 'y' && INSTALL_NODE=1 || INSTALL_NODE=0 prompt_yes_no 'Install Visual Studio Code?' "$([[ $INSTALL_VSCODE -eq 1 ]] && printf y || printf n)" && INSTALL_VSCODE=1 || INSTALL_VSCODE=0 prompt_yes_no 'Install XFCE desktop?' "$([[ $INSTALL_XFCE -eq 1 ]] && printf y || printf n)" && INSTALL_XFCE=1 || INSTALL_XFCE=0 prompt_yes_no 'Install XRDP remote desktop?' "$([[ $INSTALL_XRDP -eq 1 ]] && printf y || printf n)" && INSTALL_XRDP=1 || INSTALL_XRDP=0 prompt_yes_no 'Configure firewall rules?' "$([[ $CONFIGURE_FIREWALL -eq 1 ]] && printf y || printf n)" && CONFIGURE_FIREWALL=1 || CONFIGURE_FIREWALL=0 printf '\nSelection summary:\n' print_component_line 'Core stack' "$([[ $INSTALL_CORE -eq 1 ]] && printf install || printf skip)" '' print_component_line 'PHP stack' "$([[ $INSTALL_PHP -eq 1 ]] && printf install || printf skip)" '' print_component_line 'Composer' "$([[ $INSTALL_COMPOSER -eq 1 ]] && printf install || printf skip)" '' print_component_line 'Node.js' "$([[ $INSTALL_NODE -eq 1 ]] && printf install || printf skip)" '' print_component_line 'VS Code' "$([[ $INSTALL_VSCODE -eq 1 ]] && printf install || printf skip)" '' print_component_line 'XFCE' "$([[ $INSTALL_XFCE -eq 1 ]] && printf install || printf skip)" '' print_component_line 'XRDP' "$([[ $INSTALL_XRDP -eq 1 ]] && printf install || printf skip)" '' print_component_line 'Firewall' "$([[ $CONFIGURE_FIREWALL -eq 1 ]] && printf configure || printf skip)" '' printf '\n' if ! prompt_yes_no 'Proceed with installation?' 'y'; then fail 'Installation aborted by user' fi } run_interactive_preflight if should_run_apt_work; then normalize_vscode_apt_source_conflicts info "Updating apt metadata" run_root apt-get update run_root_env DEBIAN_FRONTEND=noninteractive apt-get -y upgrade else info "No package installation selected: skipping apt metadata refresh" fi if [[ $INSTALL_CORE -eq 1 ]]; then info "Installing base packages" run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y "${BASE_PACKAGES[@]}" record_component 'Core stack' 'installed' else record_component 'Core stack' 'skipped' fi if [[ $INSTALL_PHP -eq 1 ]]; then info "Installing PHP ${PHP_VERSION} stack" run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y "${PHP_PACKAGES[@]}" record_component "PHP ${PHP_VERSION} stack" 'installed' else record_component "PHP ${PHP_VERSION} stack" 'skipped' fi if [[ $INSTALL_PHP -eq 1 ]]; then info "Configuring PHP" run_root mkdir -p "/etc/php/${PHP_VERSION}/fpm/conf.d" "/etc/php/${PHP_VERSION}/cli/conf.d" run_root tee "/etc/php/${PHP_VERSION}/fpm/conf.d/99-laravel-base.ini" >/dev/null </dev/null run_root install -m 0755 "$TMP_COMPOSER_DIR/composer" /usr/local/bin/composer record_component 'Composer' 'installed' else record_component 'Composer' 'skipped' fi if [[ $INSTALL_NODE -eq 1 ]]; then info "Installing Node.js ${NODE_MAJOR}.x" curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | run_root bash run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs record_component "Node.js ${NODE_MAJOR}.x" 'installed' else record_component "Node.js ${NODE_MAJOR}.x" 'skipped' fi if [[ $INSTALL_VSCODE -eq 1 ]]; then info "Installing Visual Studio Code" curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > "$TMP_COMPOSER_DIR/packages.microsoft.gpg" run_root install -D -o root -g root -m 0644 "$TMP_COMPOSER_DIR/packages.microsoft.gpg" /usr/share/keyrings/packages.microsoft.gpg normalize_vscode_apt_source_conflicts echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" \ | run_root tee /etc/apt/sources.list.d/vscode.list >/dev/null run_root apt-get update run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y code record_component 'VS Code' 'installed' else record_component 'VS Code' 'skipped' fi if [[ $INSTALL_XFCE -eq 1 ]]; then info "Installing XFCE desktop" run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y xfce4 xfce4-goodies xorg dbus-x11 run_as_user "$WORKSTATION_USER" mkdir -p "/home/$WORKSTATION_USER" run_as_user "$WORKSTATION_USER" bash -lc "printf 'xfce4-session\n' > ~/.xsession" record_component 'XFCE desktop' 'installed' else record_component 'XFCE desktop' 'skipped' fi if [[ $INSTALL_XRDP -eq 1 ]]; then info "Installing XRDP" run_root_env DEBIAN_FRONTEND=noninteractive apt-get install -y xrdp run_root systemctl enable xrdp run_root systemctl restart xrdp record_component 'XRDP' 'installed' else record_component 'XRDP' 'skipped' fi info "Creating Laravel workspace directories" run_root mkdir -p "$LARAVEL_PROJECTS_DIR" "$LARAVEL_PROJECTS_DIR/shared" "$LARAVEL_PROJECTS_DIR/backups" run_root chown -R "$WORKSTATION_USER":www-data "$LARAVEL_PROJECTS_DIR" run_root chmod -R 2775 "$LARAVEL_PROJECTS_DIR" info "Configuring services" SERVICE_NAMES=() if [[ $INSTALL_PHP -eq 1 ]]; then SERVICE_NAMES+=("php${PHP_VERSION}-fpm") fi if [[ $INSTALL_CORE -eq 1 ]]; then SERVICE_NAMES+=(nginx mysql redis-server supervisor) fi if [[ ${#SERVICE_NAMES[@]} -gt 0 ]]; then run_root systemctl enable "${SERVICE_NAMES[@]}" run_root systemctl restart "${SERVICE_NAMES[@]}" fi if [[ $CONFIGURE_FIREWALL -eq 1 ]]; then info "Configuring firewall" run_root ufw allow OpenSSH run_root ufw allow 80/tcp run_root ufw allow 443/tcp if [[ $INSTALL_XRDP -eq 1 ]]; then run_root ufw allow 3389/tcp fi run_root ufw --force enable record_component 'Firewall rules' 'installed' else record_component 'Firewall rules' 'skipped' fi SUMMARY_FILE="/home/$WORKSTATION_USER/laravel-base-setup-summary.txt" info "Writing summary to $SUMMARY_FILE" SUMMARY_PHP="$(command_version php php -v)" SUMMARY_COMPOSER="$(command_version composer composer --version)" SUMMARY_NODE="$(command_version node node --version)" SUMMARY_NPM="$(command_version npm npm --version)" SUMMARY_MYSQL="$(command_version mysql mysql --version)" SUMMARY_NGINX="$(command_version nginx nginx -v)" SUMMARY_REDIS="$(command_version redis-server redis-server --version)" cat > "$TMP_COMPOSER_DIR/summary.txt" <