Skip to content

Instantly share code, notes, and snippets.

@EmranAhmed
Forked from AminulBD/bootstrap.sh
Last active June 18, 2024 14:09
Show Gist options
  • Save EmranAhmed/a1a61b710f72317c775715fe1509e99d to your computer and use it in GitHub Desktop.
Save EmranAhmed/a1a61b710f72317c775715fe1509e99d to your computer and use it in GitHub Desktop.
Ubuntu Server LEMP Setup script

How to Install

  • Download script curl -fsSL https://gist.githubusercontent.com/EmranAhmed/a1a61b710f72317c775715fe1509e99d/raw/bootstrap.sh | bash -s
  • Make it executable chmod +x bootstrap.sh

How to use

  • ./bootstrap.sh --domain=domain.com --user=myname
#!/usr/bin/env bash
set -euo pipefail
DEBIAN_FRONTEND="noninteractive"
# Default values for parameters
DEFAULT_PHP_VERSION=8.3
DEFAULT_NODE_VERSION=20
DEFAULT_DOMAIN="example.test"
DEFAULT_USER="example"
DEFAULT_MYSQL_PASSWORD=$(openssl rand -base64 10)
NGINX_WORKER_CONNECTIONS=$(ulimit -n)
# Initialize parameters with default values
PHP_VERSION=$DEFAULT_PHP_VERSION
NODE_VERSION=$DEFAULT_NODE_VERSION
MYSQL_PASSWORD=$DEFAULT_MYSQL_PASSWORD
DOMAIN=""
CUSTOM_USER=""
# Function to display help text
show_help() {
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " --domain= Specify the domain (default: $DEFAULT_DOMAIN)"
echo " --php= Specify the php version (default: $DEFAULT_PHP_VERSION)"
echo " --node= Specify the NodeJS version (default: $DEFAULT_NODE_VERSION)"
echo " --mysql-password= Specify the MySQL Password (default: Randomly generated)"
echo " --help Display this help and exit"
}
# Parse the command line arguments
for arg in "$@"; do
case $arg in
--php=*)
PHP_VERSION="${arg#*=}"
shift # Remove --php= from processing
;;
--node=*)
NODE_VERSION="${arg#*=}"
shift # Remove --node= from processing
;;
--mysql-password=*)
MYSQL_PASSWORD="${arg#*=}"
shift # Remove --mysql-password= from processing
;;
--domain=*)
DOMAIN="${arg#*=}"
shift # Remove --domain= from processing
;;
--user=*)
CUSTOM_USER="${arg#*=}"
shift # Remove --user= from processing
;;
--help)
show_help
exit 0
;;
*)
echo "Unknown option: $arg"
show_help
exit 1
;;
esac
done
if [ -z "$DOMAIN" ]; then
read -p "Enter domain [default: $DEFAULT_DOMAIN]: " DOMAIN
DOMAIN=${DOMAIN:-$DEFAULT_DOMAIN}
fi
if [ -z "$CUSTOM_USER" ]; then
read -p "Enter username [default: $DEFAULT_USER]: " CUSTOM_USER
CUSTOM_USER=${CUSTOM_USER:-$DEFAULT_USER}
fi
# configure mysql with random password
debconf-set-selections <<< "mariadb-server mysql-server/root_password password $MYSQL_PASSWORD"
debconf-set-selections <<< "mariadb-server mysql-server/root_password_again password $MYSQL_PASSWORD"
echo $MYSQL_PASSWORD > /root/.mysql_password
# for certbot
add-apt-repository universe -yq
# install common packages
apt update -q && apt install -yq \
apt-utils software-properties-common \
rsync git git-core htop zip unzip ufw apt-transport-https \
ca-certificates gnupg git ufw zip unzip curl wget \
nginx libnginx-mod-http-cache-purge certbot python3-certbot-nginx \
redis-server supervisor mariadb-server
# ufw configiguration
ufw allow ssh
ufw allow http
ufw allow https
ufw allow redis
ufw allow mysql
ufw enable
# Add system user with custom home directory
useradd -m -d /srv/$CUSTOM_USER -s /bin/bash $CUSTOM_USER
mkdir -p /srv/$CUSTOM_USER/{apps,logs,ssl}
mkdir -p /srv/$CUSTOM_USER/apps/$DOMAIN/releases
mkdir -p /srv/$CUSTOM_USER/logs/{nginx,php,mysql,supervisor,ssl}
chown -R $CUSTOM_USER:$CUSTOM_USER /srv/$CUSTOM_USER
chmod 755 /srv/$CUSTOM_USER
# install php and extensions
add-apt-repository -y ppa:ondrej/php
apt update -q
apt install -yq \
php$PHP_VERSION-{common,cli,opcache,fpm,mysql,mbstring,xml,curl,zip,gd,imagick,bcmath,redis,intl,soap,sqlite3}
# create php-fpm pool
cat > /etc/php/$PHP_VERSION/fpm/pool.d/$CUSTOM_USER.conf <<EOF
[$CUSTOM_USER]
user = $CUSTOM_USER
group = $CUSTOM_USER
listen = /run/php/php$PHP_VERSION-fpm-$CUSTOM_USER.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
access.log = /srv/$CUSTOM_USER/logs/php/$CUSTOM_USER.access.log
slowlog = /srv/$CUSTOM_USER/logs/php/$CUSTOM_USER.slow.log
php_admin_value[error_log] = /srv/$CUSTOM_USER/logs/php/$CUSTOM_USER.error.log
EOF
systemctl restart php$PHP_VERSION-fpm
# install composer
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# install wp-cli
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar && mv wp-cli.phar /usr/local/bin/wp
# install nodejs
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
apt update -q && apt install -yq nodejs
# install yarn and pm2
npm install -g yarn pm2
# install laravel
cd /srv/$CUSTOM_USER/apps/$DOMAIN/releases
RELEASE_NAME=$(date +%s)
sudo -u $CUSTOM_USER composer create-project laravel/laravel $RELEASE_NAME
sudo -u $CUSTOM_USER ln -nfs /srv/$CUSTOM_USER/apps/$DOMAIN/releases/$RELEASE_NAME /srv/$CUSTOM_USER/apps/$DOMAIN/current
sudo -u $CUSTOM_USER mv /srv/$CUSTOM_USER/apps/$DOMAIN/releases/$RELEASE_NAME/storage /srv/$CUSTOM_USER/apps/$DOMAIN/storage
sudo -u $CUSTOM_USER mv /srv/$CUSTOM_USER/apps/$DOMAIN/releases/$RELEASE_NAME/.env /srv/$CUSTOM_USER/apps/$DOMAIN/.env
sudo -u $CUSTOM_USER ln -nfs /srv/$CUSTOM_USER/apps/$DOMAIN/.env /srv/$CUSTOM_USER/apps/$DOMAIN/releases/$RELEASE_NAME/.env
sudo -u $CUSTOM_USER ln -nfs /srv/$CUSTOM_USER/apps/$DOMAIN/storage /srv/$CUSTOM_USER/apps/$DOMAIN/releases/$RELEASE_NAME/storage
sudo -u $CUSTOM_USER ln -nfs /srv/$CUSTOM_USER/apps/$DOMAIN/storage/app/public /srv/$CUSTOM_USER/apps/$DOMAIN/releases/$RELEASE_NAME/public/storage
# create site based nginx config
cat > /etc/nginx/sites-available/$DOMAIN <<EOF
server {
listen 80;
listen [::]:80;
server_name $DOMAIN;
root /srv/$CUSTOM_USER/apps/$DOMAIN/current/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.html index.php;
charset utf-8;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# error_log /srv/$CUSTOM_USER/logs/nginx/$DOMAIN.error.log;
# access_log /srv/$CUSTOM_USER/logs/nginx/$DOMAIN.access.log;
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php$PHP_VERSION-fpm-$CUSTOM_USER.sock;
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
EOF
# remove default nginx config
rm /etc/nginx/sites-enabled/default
# enable nginx config
ln -s /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/$DOMAIN
systemctl restart nginx
# install lets encrypt
openssl dhparam -out /etc/nginx/dhparam.pem 2048
mkdir -p /opt/www/_letsencrypt
chown www-data /opt/www/_letsencrypt
certbot certonly --non-interactive --webroot -d $DOMAIN --email info@$DOMAIN -w /opt/www/_letsencrypt -n --agree-tos --force-renewal
cat > /etc/nginx/nginx.conf <<EOF
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections $NGINX_WORKER_CONNECTIONS;
multi_accept on;
}
http {
##
# Basic Settings
##
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
types_hash_bucket_size 64;
client_max_body_size 64M; # File Upload Limit
client_body_buffer_size 64M;
keepalive_timeout 60; # Fixing upstream timed out (110: Connection timed out)...
##
# MIME
##
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
##
# Diffie-Hellman parameter for DHE ciphersuites
##
ssl_dhparam /etc/nginx/dhparam.pem;
# Mozilla Intermediate configuration
# based on https://nginxconfig.io/
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;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;
##
# Logging Settings
##
access_log off;
error_log /srv/$CUSTOM_USER/logs/nginx/$DOMAIN.error.log warn;
##
# Gzip Settings
##
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
EOF
cat > /etc/nginx/sites-available/$DOMAIN <<EOF
server {
listen 80;
listen [::]:80;
server_name $DOMAIN;
# ACME-challenge
location ^~ /.well-known/acme-challenge/ {
root /opt/www/_letsencrypt;
}
location / {
return 301 https://$DOMAIN\$request_uri;
}
}
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name $DOMAIN;
root /srv/$CUSTOM_USER/apps/$DOMAIN/current/public;
index index.html index.php;
charset utf-8;
# SSL
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/$DOMAIN/chain.pem;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_buffers 16 32k;
fastcgi_buffer_size 64k;
# Config for issue: 110: Connection Time Out
fastcgi_read_timeout 600s;
fastcgi_send_timeout 600s;
fastcgi_connect_timeout 600s;
fastcgi_pass unix:/run/php/php$PHP_VERSION-fpm-$CUSTOM_USER.sock;
}
# assets, media
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
expires 7d;
access_log off;
}
# svg, fonts
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
add_header Access-Control-Allow-Origin "*";
expires 7d;
access_log off;
}
# Global restrictions configuration file.
# Designed to be included in any server {} block.
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
# . files
location ~ /\.(?!well-known) {
deny all;
}
}
EOF
systemctl restart nginx
# update php config
declare -A php_config
php_config["upload_max_filesize"]="64M"
php_config["max_input_vars"]="2000"
php_config["post_max_size"]="256M"
php_config["memory_limit"]="128M"
php_config["max_execution_time"]="60"
for key in ${!php_config[@]}; do
sed -i "s/^;\?\($key\).*/\1=${php_config[$key]}/" /etc/php/$PHP_VERSION/fpm/php.ini
done
systemctl restart php$PHP_VERSION-fpm
# install crontab
echo "* * * * * cd /srv/$CUSTOM_USER/apps/$DOMAIN/current && php artisan schedule:run >> /dev/null 2>&1" >> /var/spool/cron/crontabs/$CUSTOM_USER
chown $CUSTOM_USER:crontab /var/spool/cron/crontabs/$CUSTOM_USER
chmod 600 /var/spool/cron/crontabs/$CUSTOM_USER
systemctl restart cron
# install supervisor config
cat > /etc/supervisor/conf.d/$DOMAIN.conf <<EOF
[program:$DOMAIN]
process_name=%(program_name)s_%(process_num)02d
; if you are using horizon uncomment below line and comment queue worker line
;command=php /srv/$CUSTOM_USER/apps/$DOMAIN/current/artisan horizon
; if you are using queue worker uncomment below line and comment horizon line
command=php /srv/$CUSTOM_USER/apps/$DOMAIN/current/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=$CUSTOM_USER
numprocs=1
redirect_stderr=true
stdout_logfile=/srv/$CUSTOM_USER/logs/supervisor/$DOMAIN.log
EOF
supervisorctl reread
supervisorctl update
echo "127.0.0.1 $DOMAIN" >> /etc/hosts
echo $DOMAIN > /etc/hostname
hostname -F /etc/hostname
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment