Skip to content

Instantly share code, notes, and snippets.

@mrizwan47
Last active May 2, 2026 13:02
Show Gist options
  • Select an option

  • Save mrizwan47/9eeeb3e07e50ff7dcbfd7f6079cf9981 to your computer and use it in GitHub Desktop.

Select an option

Save mrizwan47/9eeeb3e07e50ff7dcbfd7f6079cf9981 to your computer and use it in GitHub Desktop.

curl -fsSL https://gist.githubusercontent.com/mrizwan47/9eeeb3e07e50ff7dcbfd7f6079cf9981/raw/561eddf636af5d5b26fd84e9af7a4100dc398d68/lemp-install.sh | sudo bash

#!/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