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