Originally published on bcd.dev
Before you go through the pain and challenge of configuring your own server you should perhaps consider Laravel forge. I trust they would know better how to deploy a laravel app.
For curious minded people, this is part of a bigger series Do it yourself series
Components
- webserver
- nginx
- database
- mysql
- php
- composer
- node
- npm / yarn
- scheduler
- firewall
- log
- papertrail
- search
- elastic search
- algolia
- other third party services
- redis
Basic build
recepee on an ubuntu server (22-10)
- mysql
- php
- composer
- node
- nginx
- queue
Requirements
- VPS Server
- Valid DNS record pointing to your server
Mysql
sudo apt-get install -y mysql-server
# Init project by creating a user with a dedicated database
name=$1
username=${2:-$name}
password=${3:-$name}
root_username=${4:-'root'}
root_password=${5:-''}
echo '' > tmp.sql
echo "CREATE USER $name@localhost identified by \"$password\";" >> tmp.sql
echo "CREATE DATABASE $name charset utf8 collate utf8_general_ci;" >> tmp.sql
echo "GRANT ALL PRIVILEGES ON $name.* to $name@localhost;" >> tmp.sql
mysql -u$root_username -p$root_password -e "source tmp.sql"
mysql -u$root_username -p$root_password -e "CREATE DATABASE $name charset utf8 collate utf8_general_ci;"
Php
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update
# php with some extensions
PHP_VERSION=${1:-'8.2'}
sudo apt-get install -y "php$PHP_VERSION" php$PHP_VERSION-{common,cli,fpm,zip,xml,pdo,mysql,mbstring,tokenizer,ctype,curl,common,curl,gd,intl,sqlite3,xmlrpc,xsl,soap,opcache,readline,xdebug,bcmath}
Composer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
# For simplicity purpose, we are skipping the hash check. That is a crucial step you wouldn't want to skip when downloading stuff on the internet
# Hash below matches composer version 2.1.3
# php -r "if (hash_file('sha384', 'composer-setup.php') === '756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
sudo mv composer.phar /usr/local/bin/composer
Node
You can get node on their website but I prefer getting specific version from node version manager (nvm)
version=${1:-'20'}
echo "Installing nvm + node $version"
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
source ~/.bashrc
nvm install $version
# Optional but I like yarn so here we go
nvm exec $version npm i yarn -g
Nginx
# Making sure apache is not installed to avoid conflict on port 80
sudo apt-get remove -y apache
sudo apt-get install -y nginx
rm /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
name=$1 # site.domain
webroot=${3:-"/var/www/vhosts/$name"}
mkdir -p $webroot
touch "/etc/nginx/sites-available/$name.conf"
ln -s "/etc/nginx/sites-available/$name.conf" "/etc/nginx/sites-enabled/$name.conf"
cat >> "/etc/nginx/sites-available/$name.conf" << EOF
server {
listen 80 default_server;
listen [::]:80 default_server;
root $webroot;
server_name $name www.$name;
}
EOF
Secure traffic using HTTPS
At this point we are running a web server on port 80. However everyone can see the content flowing through the networking. Welcome https.
domain=$1
email=${2:-"your@email.com"}
sudo apt-get install -y python3-certbot-nginx
# manual
# certbot certonly -a manual --rsa-key-size 4096 --email $email -d $domain -d www.$domain
# auto
## With base nginx config
certbot certonly --nginx --rsa-key-size 4096 --email $email -d $domain -d www.$domain
When the steps above succeed you can carry on with nginx config for ssl. The following will redirect all non secure connection (80) to secure connection (443)
# Usage ./laravel.sh site.domain 8.2 ~/sites
## Dependencies: letsencrypt, php$php_version, php$php_version-fpm
# sudo apt-get install -y php$php_version php$php_version-fpm
name=$1 # site.domain
user=$2
php_version=${3:-'8.2'}
root=${4:-"/var/www/vhosts/$name"}
webroot=${5:-"/var/www/vhosts/$name/public"}
touch /etc/nginx/sites-available/$name.conf
ln -s /etc/nginx/sites-available/$name.conf /etc/nginx/sites-enabled/$name.conf
mkdir -p /var/www/vhosts/$name/storage/logs
touch /var/www/vhosts/$name/storage/logs/error.log
touch /var/www/vhosts/$name/storage/logs/access.log
cat >> /etc/nginx/sites-available/$name.conf << EOF
# Force HTTPS
server {
listen 80;
listen [::]:80;
server_name $name www.$name;
return 301 https://$name\$request_uri;
}
# Force www less version'
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.$name;
ssl_certificate /etc/letsencrypt/live/$name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$name/privkey.pem;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
return 301 https://$name\$request_uri;
}
server {
server_name $name;
# SSL config
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_tokens off;
ssl_buffer_size 8k;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
ssl_ecdh_curve secp384r1;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4;
ssl_certificate /etc/letsencrypt/live/$name/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$name/privkey.pem;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# End of SSL config
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
charset utf-8;
index index.php index.html;
error_log $root/storage/logs/error.log;
access_log $root/storage/logs/access.log;
root $webroot;
error_page 404 /index.php;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
gzip_static on;
proxy_set_header Host \$server_name;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
location ~ \.php$ {
try_files \$uri /index.php =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php$php_version-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param PATH_INFO \$fastcgi_path_info;
fastcgi_buffers 8 64k;
fastcgi_buffer_size 128k;
fastcgi_connect_timeout 3000;
fastcgi_send_timeout 3000;
fastcgi_read_timeout 3000;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Define caching rules for static images
location ~* \.(jpg|jpeg|png|gif|ico)$ {
expires 30d; # adjust the caching duration as needed
add_header Cache-Control "public, max-age=2592000";
}
gzip on;
gzip_types text/plain text/css application/javascript image/*;
client_max_body_size 128m;
# For unlimited
# client_max_body_size 0;
}
EOF
# permissions
sudo find storage -type f -exec chmod 664 {} \;
sudo find storage -type d -exec chmod 775 {} \;
sudo chmod -R ug+rwx storage bootstrap/cache
sudo chgrp -R www-data storage bootstrap/cache
sudo usermod -aG $user www-data
sudo chown $user:www-data -R storage bootstrap/cache
# Check file exists
# /etc/ssl/dhparams.pem
# Generate if not
# openssl dhparam -out /etc/ssl/dhparams.pem 4096
cat >> /etc/php/$php_version/cli/php.ini << EOF
post_max_size=128M
upload_max_filesize=128M
max_upload_file=50
EOF
Auto renew certificate
# DISCLAIMER: it is safer to edit cron file using crontab dedicated command
# That being see given this is a script we likely want to have automated
/var/spool/cron/crontabs
## cron job to auto renew every 3 months for you
crontab -e
# 0 0 1 */3 * /usr/bin/certbot renew --quiet
## I saw people doing it monthly
# 0 0 1 * *
Queue
#! /bin/bash
user=${0:-$(USER)}
root_dir="/home/$user/www/"
processes=4
sudo apt get install -y supervisor
cat >> /etc/supervisor/conf.d/laravel-worker.conf << EOF
process_name=%(program_name)s_%(process_num)02d
command=php ${root_dir}/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=${user}
numprocs=${processes}
redirect_stderr=true
stdout_logfile=${root_dir}/storage/logs/worker.log
stopwaitsecs=3600
EOF
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all
Conclusion
No conclusion as this is a starting point only but gets you an operational laravel app.
Made a lot of arbitrary choices. Adapt for your usecase.
Next up
- automate script using orchestration (ie ansible)
- deploy using aws code deploy
- CI / CD pipeline from github
- tighting security with firewall policies
- monitoring
- load management / balancing
Originally published on bcd.dev
Top comments (0)