curl -fsSL https://gist.githubusercontent.com/mrizwan47/9eeeb3e07e50ff7dcbfd7f6079cf9981/raw/561eddf636af5d5b26fd84e9af7a4100dc398d68/lemp-install.sh | sudo bash
Last active
May 2, 2026 13:02
-
-
Save mrizwan47/9eeeb3e07e50ff7dcbfd7f6079cf9981 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Ultimate LEMP Stack Installer for Ubuntu | |
| # Based on: https://riz.codes/install-lemp-and-phpmyadmin-ubuntu/ | |
| # Author: Rizwan (https://riz.codes) | |
| # | |
| # Installs: PHP 8.3 (FPM), NGINX, MySQL, PHPMyAdmin, SSL (Let's Encrypt), | |
| # Composer, Node + NPM (via NVM) | |
| # | |
| # Usage: | |
| # curl -fsSL https://gist.githubusercontent.com/.../lemp-install.sh | sudo bash | |
| # # or | |
| # wget -qO- https://gist.githubusercontent.com/.../lemp-install.sh | sudo bash | |
| # | |
| set -uo pipefail | |
| # ---------- pretty printing ---------- | |
| RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m' | |
| BLUE=$'\033[0;34m'; BOLD=$'\033[1m'; NC=$'\033[0m' | |
| info() { printf "%s[INFO]%s %s\n" "$BLUE" "$NC" "$*"; } | |
| ok() { printf "%s[ OK ]%s %s\n" "$GREEN" "$NC" "$*"; } | |
| warn() { printf "%s[WARN]%s %s\n" "$YELLOW" "$NC" "$*"; } | |
| err() { printf "%s[FAIL]%s %s\n" "$RED" "$NC" "$*" >&2; } | |
| section() { printf "\n%s%s==> %s%s\n" "$BOLD" "$BLUE" "$*" "$NC"; } | |
| die() { err "$*"; exit 1; } | |
| # Force interactive prompts to read from /dev/tty so the script works | |
| # correctly when piped: curl ... | sudo bash | |
| if [[ -t 0 ]]; then | |
| INPUT_FD=0 | |
| else | |
| if [[ -r /dev/tty ]]; then | |
| exec 3</dev/tty | |
| INPUT_FD=3 | |
| else | |
| die "No interactive TTY available. Please download the script and run it directly: | |
| wget <url> -O lemp-install.sh && sudo bash lemp-install.sh" | |
| fi | |
| fi | |
| ask() { | |
| # ask "Prompt" "default" -> echoes user reply (or default) | |
| local prompt=$1 default=${2:-} reply | |
| if [[ -n $default ]]; then | |
| printf "%s%s%s [%s]: " "$BOLD" "$prompt" "$NC" "$default" >/dev/tty | |
| else | |
| printf "%s%s%s: " "$BOLD" "$prompt" "$NC" >/dev/tty | |
| fi | |
| IFS= read -r -u "$INPUT_FD" reply || reply="" | |
| printf '%s' "${reply:-$default}" | |
| } | |
| confirm() { | |
| # confirm "Question" "Y" -> returns 0 for yes, 1 for no | |
| local prompt=$1 default=${2:-Y} reply hint | |
| [[ $default == [Yy] ]] && hint="[Y/n]" || hint="[y/N]" | |
| while true; do | |
| printf "%s%s%s %s: " "$BOLD" "$prompt" "$NC" "$hint" >/dev/tty | |
| IFS= read -r -u "$INPUT_FD" reply || reply="" | |
| reply=${reply:-$default} | |
| case $reply in | |
| [Yy]|[Yy][Ee][Ss]) return 0 ;; | |
| [Nn]|[Nn][Oo]) return 1 ;; | |
| *) printf "Please answer yes or no.\n" >/dev/tty ;; | |
| esac | |
| done | |
| } | |
| ask_secret() { | |
| local prompt=$1 reply | |
| printf "%s%s%s: " "$BOLD" "$prompt" "$NC" >/dev/tty | |
| IFS= read -r -s -u "$INPUT_FD" reply || reply="" | |
| printf "\n" >/dev/tty | |
| printf '%s' "$reply" | |
| } | |
| trap 'err "An error occurred on line $LINENO. Aborting."; exit 1' ERR | |
| set -E | |
| # ---------- preflight ---------- | |
| banner() { | |
| cat <<'EOF' | |
| _ ______ __ __ _____ _____ _ _ _ | |
| | | | ____| \/ | __ \ |_ _| | | | | | | |
| | | | |__ | \ / | |__) | | | _ __ ___| |_ __ _| | | ___ _ __ | |
| | | | __| | |\/| | ___/ | | | '_ \/ __| __/ _` | | |/ _ \ '__| | |
| | |____| |____| | | | | _| |_| | | \__ \ || (_| | | | __/ | | |
| |______|______|_| |_|_| |_____|_| |_|___/\__\__,_|_|_|\___|_| | |
| PHP 8.3 · NGINX · MySQL · PHPMyAdmin · SSL · Composer · Node | |
| Based on https://riz.codes | |
| EOF | |
| } | |
| require_root() { | |
| if [[ $EUID -ne 0 ]]; then | |
| if command -v sudo >/dev/null 2>&1; then | |
| warn "Re-running with sudo..." | |
| exec sudo -E bash "$0" "$@" | |
| else | |
| die "Please run this script as root." | |
| fi | |
| fi | |
| } | |
| detect_ubuntu() { | |
| [[ -f /etc/os-release ]] || die "Cannot detect OS (no /etc/os-release)." | |
| # shellcheck disable=SC1091 | |
| . /etc/os-release | |
| if [[ ${ID:-} != "ubuntu" ]]; then | |
| warn "This script is designed for Ubuntu. Detected: ${PRETTY_NAME:-unknown}" | |
| confirm "Continue anyway?" "N" || exit 0 | |
| fi | |
| UBUNTU_VERSION=${VERSION_ID:-0} | |
| UBUNTU_MAJOR=${UBUNTU_VERSION%%.*} | |
| info "Detected: ${PRETTY_NAME:-Ubuntu $UBUNTU_VERSION}" | |
| } | |
| # ---------- gather inputs upfront ---------- | |
| collect_inputs() { | |
| section "Configuration" | |
| echo "We'll ask a few questions now so the install can run unattended." | |
| echo | |
| DOMAIN=$(ask "Primary domain (e.g. example.com)" "") | |
| while [[ -z $DOMAIN || ! $DOMAIN =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; do | |
| warn "Please enter a valid domain (e.g. example.com)." | |
| DOMAIN=$(ask "Primary domain" "") | |
| done | |
| INCLUDE_WWW="n" | |
| if confirm "Also include www.$DOMAIN in the NGINX server_name?" "Y"; then | |
| INCLUDE_WWW="y" | |
| fi | |
| WEBROOT=$(ask "Web root directory" "/var/www/html") | |
| INSTALL_PHPMYADMIN="n" | |
| if confirm "Install PHPMyAdmin?" "Y"; then INSTALL_PHPMYADMIN="y"; fi | |
| INSTALL_SSL="n" | |
| SSL_EMAIL="" | |
| if confirm "Install free SSL via Let's Encrypt (Certbot)?" "Y"; then | |
| INSTALL_SSL="y" | |
| SSL_EMAIL=$(ask "Email for Let's Encrypt notifications" "") | |
| while [[ -z $SSL_EMAIL || ! $SSL_EMAIL =~ ^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$ ]]; do | |
| warn "Please enter a valid email." | |
| SSL_EMAIL=$(ask "Email for Let's Encrypt" "") | |
| done | |
| fi | |
| INSTALL_COMPOSER="n" | |
| if confirm "Install Composer?" "Y"; then INSTALL_COMPOSER="y"; fi | |
| INSTALL_NVM="n" | |
| if confirm "Install Node + NPM via NVM?" "Y"; then INSTALL_NVM="y"; fi | |
| ENABLE_BASIC_AUTH="n" | |
| BASIC_AUTH_USER="" | |
| BASIC_AUTH_PASS="" | |
| if confirm "Is this a staging/test server (enable HTTP Basic Auth)?" "N"; then | |
| ENABLE_BASIC_AUTH="y" | |
| BASIC_AUTH_USER=$(ask "Basic Auth username" "staging") | |
| while :; do | |
| BASIC_AUTH_PASS=$(ask_secret "Basic Auth password (min 6 chars)") | |
| if [[ ${#BASIC_AUTH_PASS} -lt 6 ]]; then | |
| warn "Password too short." | |
| continue | |
| fi | |
| local p2; p2=$(ask_secret "Confirm password") | |
| [[ $BASIC_AUTH_PASS == "$p2" ]] && break | |
| warn "Passwords didn't match." | |
| done | |
| fi | |
| RESTRICT_SSH="n" | |
| SSH_IP="" | |
| if confirm "Restrict SSH (port 22) to a specific IP?" "N"; then | |
| RESTRICT_SSH="y" | |
| SSH_IP=$(ask "Your IP address (e.g. 131.5.13.44)" "") | |
| while [[ -z $SSH_IP || ! $SSH_IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; do | |
| warn "Please enter a valid IPv4 address." | |
| SSH_IP=$(ask "Your IP address" "") | |
| done | |
| fi | |
| RUN_SECURE_MYSQL="n" | |
| if confirm "Run mysql_secure_installation interactively at the end?" "Y"; then | |
| RUN_SECURE_MYSQL="y" | |
| fi | |
| section "Summary" | |
| cat <<EOF | |
| Domain: $DOMAIN$( [[ $INCLUDE_WWW == y ]] && echo " + www.$DOMAIN" ) | |
| Web root: $WEBROOT | |
| PHPMyAdmin: $INSTALL_PHPMYADMIN | |
| SSL (Let's Enc.): $INSTALL_SSL$( [[ $INSTALL_SSL == y ]] && echo " (notifications: $SSL_EMAIL)" ) | |
| Composer: $INSTALL_COMPOSER | |
| Node + NPM (NVM): $INSTALL_NVM | |
| Basic Auth: $ENABLE_BASIC_AUTH$( [[ $ENABLE_BASIC_AUTH == y ]] && echo " (user: $BASIC_AUTH_USER)" ) | |
| SSH lock-down: $RESTRICT_SSH$( [[ $RESTRICT_SSH == y ]] && echo " (allow only: $SSH_IP)" ) | |
| mysql_secure_install: $RUN_SECURE_MYSQL | |
| Ubuntu: $UBUNTU_VERSION | |
| EOF | |
| echo | |
| confirm "Proceed with installation?" "Y" || { warn "Aborted by user."; exit 0; } | |
| } | |
| # ---------- helpers ---------- | |
| apt_get() { | |
| DEBIAN_FRONTEND=noninteractive apt-get \ | |
| -o Dpkg::Options::="--force-confdef" \ | |
| -o Dpkg::Options::="--force-confold" \ | |
| -y "$@" | |
| } | |
| # ---------- step: prerequisites ---------- | |
| step_prereqs() { | |
| section "1/9 · Updating package lists" | |
| apt_get update | |
| ok "apt update done." | |
| section "2/9 · Upgrading installed packages" | |
| if (( UBUNTU_MAJOR >= 24 )); then | |
| info "Ubuntu $UBUNTU_VERSION ships with PHP 8.3 in its default repos — skipping upgrade & PPA per the guide." | |
| else | |
| apt_get upgrade | |
| ok "apt upgrade done." | |
| info "Adding Ondřej's PHP PPA for PHP 8.3 (Ubuntu $UBUNTU_VERSION needs this)..." | |
| apt_get install ca-certificates apt-transport-https software-properties-common | |
| # add-apt-repository would normally prompt twice; -y skips that | |
| add-apt-repository -y ppa:ondrej/php | |
| apt_get update | |
| ok "Ondřej's PHP PPA added." | |
| fi | |
| } | |
| # ---------- step: PHP 8.3 ---------- | |
| step_php() { | |
| section "3/9 · Installing PHP 8.3 (FPM) + essential extensions" | |
| apt_get install php8.3-fpm | |
| apt_get install php-mbstring php-zip php-gd php-json php-curl php-intl unzip curl npm | |
| systemctl enable --now php8.3-fpm | |
| php -v | head -n1 | sed 's/^/ /' | |
| ok "PHP 8.3 + FPM installed." | |
| } | |
| # ---------- step: NGINX ---------- | |
| step_nginx() { | |
| section "4/9 · Installing NGINX" | |
| apt_get install nginx | |
| systemctl enable --now nginx | |
| ok "NGINX installed and running." | |
| } | |
| # ---------- step: UFW ---------- | |
| step_firewall() { | |
| section "5/9 · Configuring firewall (UFW)" | |
| if ! command -v ufw >/dev/null 2>&1; then | |
| apt_get install ufw | |
| fi | |
| ufw allow 80/tcp >/dev/null | |
| ufw allow 443/tcp >/dev/null | |
| if [[ $RESTRICT_SSH == "y" ]]; then | |
| info "Restricting SSH to $SSH_IP only..." | |
| ufw allow from "$SSH_IP" to any port 22 proto tcp >/dev/null | |
| else | |
| ufw allow 22/tcp >/dev/null | |
| fi | |
| # --force avoids the "may disrupt SSH" interactive prompt | |
| ufw --force enable >/dev/null | |
| ok "UFW enabled. Ports 80, 443, 22 allowed." | |
| } | |
| # ---------- step: MySQL + PHPMyAdmin ---------- | |
| step_mysql_pma() { | |
| section "6/9 · Installing MySQL Server" | |
| apt_get install mysql-server | |
| systemctl enable --now mysql | |
| ok "MySQL server installed." | |
| if [[ $INSTALL_PHPMYADMIN == "y" ]]; then | |
| info "Installing PHPMyAdmin (BEFORE mysql_secure_installation, as per the guide — avoids password-policy conflicts)..." | |
| # Pre-seed debconf so the install is fully unattended: | |
| # - no webserver auto-config (we use NGINX) | |
| # - dbconfig-common: yes | |
| # - blank app password (PHPMyAdmin auto-generates one) | |
| debconf-set-selections <<EOF | |
| phpmyadmin phpmyadmin/dbconfig-install boolean true | |
| phpmyadmin phpmyadmin/app-password-confirm password | |
| phpmyadmin phpmyadmin/mysql/admin-pass password | |
| phpmyadmin phpmyadmin/mysql/app-pass password | |
| phpmyadmin phpmyadmin/reconfigure-webserver multiselect | |
| EOF | |
| apt_get install phpmyadmin | |
| # Symlink PHPMyAdmin into the web root | |
| mkdir -p "$WEBROOT" | |
| if [[ ! -e "$WEBROOT/phpmyadmin" ]]; then | |
| ln -s /usr/share/phpmyadmin "$WEBROOT/phpmyadmin" | |
| ok "Symlinked /usr/share/phpmyadmin → $WEBROOT/phpmyadmin" | |
| else | |
| warn "$WEBROOT/phpmyadmin already exists — skipping symlink." | |
| fi | |
| else | |
| info "Skipping PHPMyAdmin per your choice." | |
| fi | |
| } | |
| # ---------- step: NGINX site config ---------- | |
| step_nginx_site() { | |
| section "7/9 · Creating NGINX site for $DOMAIN" | |
| # Remove default site as the guide instructs | |
| rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default | |
| local conf="/etc/nginx/sites-available/$DOMAIN" | |
| local server_name="$DOMAIN" | |
| [[ $INCLUDE_WWW == "y" ]] && server_name="$DOMAIN www.$DOMAIN" | |
| mkdir -p "$WEBROOT" | |
| # Basic Auth (optional) | |
| local auth_block="" | |
| if [[ $ENABLE_BASIC_AUTH == "y" ]]; then | |
| if ! command -v openssl >/dev/null 2>&1; then | |
| apt_get install openssl | |
| fi | |
| local hash | |
| hash=$(openssl passwd -6 "$BASIC_AUTH_PASS") | |
| printf '%s:%s\n' "$BASIC_AUTH_USER" "$hash" > /etc/nginx/.htpasswd | |
| chmod 640 /etc/nginx/.htpasswd | |
| chown root:www-data /etc/nginx/.htpasswd | |
| auth_block=$' auth_basic "Restricted Content";\n auth_basic_user_file /etc/nginx/.htpasswd;\n' | |
| ok "Basic Auth file written to /etc/nginx/.htpasswd" | |
| fi | |
| cat > "$conf" <<EOF | |
| server { | |
| listen 80; | |
| server_name $server_name; | |
| root $WEBROOT; | |
| index index.php index.html index.htm; | |
| location / { | |
| try_files \$uri \$uri/ /index.php?\$query_string; | |
| } | |
| location ~ \.php\$ { | |
| include snippets/fastcgi-php.conf; | |
| fastcgi_pass unix:/var/run/php/php8.3-fpm.sock; | |
| } | |
| location ~ /\. { | |
| deny all; | |
| } | |
| ${auth_block} | |
| } | |
| EOF | |
| ok "Wrote $conf" | |
| ln -sfn "$conf" "/etc/nginx/sites-enabled/$DOMAIN" | |
| info "Testing NGINX configuration..." | |
| if nginx -t 2>&1 | sed 's/^/ /'; then | |
| systemctl reload nginx | |
| ok "NGINX reloaded." | |
| else | |
| die "NGINX config test failed. Inspect $conf and run: nginx -t" | |
| fi | |
| } | |
| # ---------- step: SSL via Certbot ---------- | |
| step_ssl() { | |
| [[ $INSTALL_SSL != "y" ]] && { info "Skipping SSL."; return; } | |
| section "8/9 · Installing Let's Encrypt SSL via Certbot" | |
| apt_get install certbot python3-certbot-nginx | |
| local domain_args=( -d "$DOMAIN" ) | |
| [[ $INCLUDE_WWW == "y" ]] && domain_args+=( -d "www.$DOMAIN" ) | |
| warn "Certbot needs $DOMAIN to resolve to THIS server's public IP via an A record." | |
| if ! confirm "Is your DNS pointing here? Continue with Certbot?" "Y"; then | |
| warn "Skipping SSL — re-run later with: certbot --nginx ${domain_args[*]}" | |
| return | |
| fi | |
| if certbot --nginx --non-interactive --agree-tos \ | |
| --email "$SSL_EMAIL" --redirect "${domain_args[@]}"; then | |
| ok "SSL certificate installed. Auto-renewal is handled by certbot.timer." | |
| else | |
| warn "Certbot failed. You can re-run interactively: certbot --nginx" | |
| fi | |
| } | |
| # ---------- step: Composer ---------- | |
| step_composer() { | |
| [[ $INSTALL_COMPOSER != "y" ]] && { info "Skipping Composer."; return; } | |
| section "9a · Installing Composer" | |
| pushd /tmp >/dev/null || die "Cannot cd to /tmp" | |
| curl -sS https://getcomposer.org/installer -o composer-setup.php | |
| local expected actual | |
| expected=$(curl -sS https://composer.github.io/installer.sig) | |
| actual=$(php -r "echo hash_file('SHA384', 'composer-setup.php');") | |
| if [[ $expected != "$actual" ]]; then | |
| rm -f composer-setup.php | |
| popd >/dev/null || true | |
| die "Composer installer checksum mismatch — refusing to run." | |
| fi | |
| ok "Composer installer verified." | |
| php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer | |
| rm -f composer-setup.php | |
| popd >/dev/null || true | |
| composer --version | sed 's/^/ /' | |
| ok "Composer installed at /usr/local/bin/composer" | |
| } | |
| # ---------- step: NVM + Node ---------- | |
| step_nvm() { | |
| [[ $INSTALL_NVM != "y" ]] && { info "Skipping NVM/Node."; return; } | |
| section "9b · Installing NVM + latest stable Node" | |
| # Install NVM for the invoking user (the one who ran sudo) — not root, | |
| # since NVM is per-user. Falls back to root if no SUDO_USER. | |
| local target_user=${SUDO_USER:-root} | |
| local target_home | |
| target_home=$(getent passwd "$target_user" | cut -d: -f6) | |
| [[ -z $target_home ]] && target_home=$HOME | |
| info "Installing NVM into $target_home (.nvm) for user '$target_user'..." | |
| sudo -u "$target_user" -H bash <<EOF | |
| set -e | |
| export HOME="$target_home" | |
| curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash | |
| export NVM_DIR="\$HOME/.nvm" | |
| [ -s "\$NVM_DIR/nvm.sh" ] && \. "\$NVM_DIR/nvm.sh" | |
| nvm install stable | |
| nvm alias default stable | |
| node -v | |
| npm -v | |
| EOF | |
| ok "Node + NPM installed for $target_user." | |
| info "If 'nvm' isn't found in new shells, ensure these lines are in ~/.bashrc:" | |
| cat <<'EOF' | |
| export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")" | |
| [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" | |
| EOF | |
| } | |
| # ---------- step: mysql_secure_installation ---------- | |
| step_secure_mysql() { | |
| [[ $RUN_SECURE_MYSQL != "y" ]] && { | |
| warn "Remember to run 'sudo mysql_secure_installation' later!" | |
| return | |
| } | |
| section "Securing MySQL installation" | |
| info "Recommended answers (per the guide):" | |
| cat <<'EOF' | |
| - VALIDATE PASSWORD component? Y, level 2 (strong) | |
| - Remove anonymous users? Y | |
| - Disallow root login remotely? Y | |
| - Remove test database? Y | |
| - Reload privilege tables? Y | |
| EOF | |
| echo | |
| # mysql_secure_installation is interactive — hand the user a real TTY. | |
| if [[ -r /dev/tty ]]; then | |
| mysql_secure_installation </dev/tty >/dev/tty 2>&1 || \ | |
| warn "mysql_secure_installation didn't complete cleanly — re-run it manually." | |
| else | |
| warn "No TTY for interactive MySQL hardening. Run: sudo mysql_secure_installation" | |
| fi | |
| } | |
| # ---------- summary ---------- | |
| final_summary() { | |
| section "All done!" | |
| local proto="http" | |
| [[ $INSTALL_SSL == "y" ]] && proto="https" | |
| cat <<EOF | |
| ${GREEN}Site:${NC} $proto://$DOMAIN | |
| ${GREEN}Web root:${NC} $WEBROOT | |
| EOF | |
| [[ $INSTALL_PHPMYADMIN == "y" ]] && \ | |
| echo " ${GREEN}PHPMyAdmin:${NC} $proto://$DOMAIN/phpmyadmin" | |
| cat <<EOF | |
| ${BOLD}Useful commands:${NC} | |
| sudo nginx -t && sudo systemctl reload nginx | |
| sudo systemctl status php8.3-fpm | |
| sudo systemctl status mysql | |
| sudo tail -f /var/log/nginx/error.log | |
| Article: https://riz.codes/install-lemp-and-phpmyadmin-ubuntu/ | |
| EOF | |
| ok "Happy coding!" | |
| } | |
| # ---------- main ---------- | |
| main() { | |
| banner | |
| require_root "$@" | |
| detect_ubuntu | |
| collect_inputs | |
| step_prereqs | |
| step_php | |
| step_nginx | |
| step_firewall | |
| step_mysql_pma | |
| step_nginx_site | |
| step_ssl | |
| step_composer | |
| step_nvm | |
| step_secure_mysql | |
| final_summary | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment