netgescon-day0/scripts/install/ubuntu2404-laravel-base.sh

630 lines
20 KiB
Bash
Executable File

#!/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 <version> PHP major.minor to install (default: 8.3)
--node-major <version> Node.js major release (default: 20)
--projects-dir <path> 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> 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 <<EOF
memory_limit=512M
upload_max_filesize=64M
post_max_size=64M
max_execution_time=300
max_input_vars=3000
opcache.enable=1
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
realpath_cache_size=4096K
realpath_cache_ttl=600
EOF
run_root cp "/etc/php/${PHP_VERSION}/fpm/conf.d/99-laravel-base.ini" "/etc/php/${PHP_VERSION}/cli/conf.d/99-laravel-base.ini"
fi
TMP_COMPOSER_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_COMPOSER_DIR"' EXIT
if [[ $INSTALL_COMPOSER -eq 1 ]]; then
info "Installing Composer"
curl -fsSL https://getcomposer.org/installer -o "$TMP_COMPOSER_DIR/composer-setup.php"
php "$TMP_COMPOSER_DIR/composer-setup.php" --install-dir="$TMP_COMPOSER_DIR" --filename=composer >/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" <<EOF
Laravel Base Setup Summary
Date: $(date -Is)
Host: $(hostname)
Ubuntu: ${VERSION:-Ubuntu 24.04}
PHP: $SUMMARY_PHP
Composer: $SUMMARY_COMPOSER
Node: $SUMMARY_NODE
NPM: $SUMMARY_NPM
MySQL: $SUMMARY_MYSQL
Nginx: $SUMMARY_NGINX
Redis: $SUMMARY_REDIS
Projects dir: $LARAVEL_PROJECTS_DIR
Workstation user: $WORKSTATION_USER
XFCE installed: $INSTALL_XFCE
VS Code installed: $INSTALL_VSCODE
XRDP installed: $INSTALL_XRDP
Firewall configured: $CONFIGURE_FIREWALL
Installed components: ${INSTALLED_COMPONENTS[*]:-none}
Skipped components: ${SKIPPED_COMPONENTS[*]:-none}
EOF
run_root install -o "$WORKSTATION_USER" -g "$WORKSTATION_USER" -m 0644 "$TMP_COMPOSER_DIR/summary.txt" "$SUMMARY_FILE"
info "Base machine ready"
printf '\nInstalled components:\n'
for component in "${INSTALLED_COMPONENTS[@]}"; do
printf ' - %s\n' "$component"
done
printf '\nSkipped components:\n'
for component in "${SKIPPED_COMPONENTS[@]}"; do
printf ' - %s\n' "$component"
done
printf '\nNext steps:\n'
printf ' 1. Copy or clone your Laravel project under %s\n' "$LARAVEL_PROJECTS_DIR"
printf ' 2. Configure Nginx site and database for the specific project\n'
printf ' 3. For NetGescon, run scripts/install/netgescon-node-bootstrap.sh\n'