Deploying Laravel on Your Own VPS: A Ground-Up DIY Guide -- presentation version
Jan 11 / 8 min read
Language Mismatch Disclaimer: Please be aware that the language of this article may not match the language settings of your browser or device.
Do you want to read articles in English instead ?
This is the presentation version of this post that is more detailed.
Intro
- Babacar
- Sr. Software Engineer
- x years of experience
- currently Solutions Architect
- Avid learner, curious mind
- Building Traxelio - an IOT project react native / Laravel
Intro
- Babacar
- stay active: basketball, swimming, outdoor activity
Audience check
Topics
- Deploying Laravel on Your Own VPS: A Ground-Up DIY Guide
- Building a cross platform app with Expo from sketches to app/play store release
- My Journey Through Leadership, Solutions Architecture, and Productivity Hacks
- Invest, Create, Own: A Practical Guide to Building Wealth and Independence
Outline
- PHP and Laravel
- Existing tools
- Why this talk
- Our focus today
- Pros of building yourself
- Cons of building yourself
- Deploying
- Deploying
- Basic build
- Requirements
- Mysql
- PHP
- Composer
- Node
- Nginx
- Secure traffic using HTTPS
- Queue workers
- Talk Objectives
- Conclusion
PHP and Laravel
- PHP app on internet: 75% in 2024
- laravel app 35% within PHP
Existing tools
- laravel forge
- laravel vapor
- laravel cloud
- CI / CD
- and many more
Why this talk
- not universally understood
- consummer society
- implement nothing and sell you everything
- part of a bigger series about DIY
- experience sharing
Our focus today
- deployment aspect
- no tools, just basics
- show you how to do it
Pros of building yourself
- these basics skills are transposable
- gain ability to solve complex problem
- no vendor locking
- better privacy: required for certain enterprise projects
- if you like it, maybe someone else will like it so you can sell it
Cons of building yourself
- can be the worst
- you don't know what you are doing
- not because you can, means you should build it
- trade money to gain time and expertise by delegating to other reliable services
Deploying
- webserver: nginx
- database: mysql
- PHP
- composer
- bundler: node/yarn
Deploying
- workers: scheduler, cron jobs
- firewall
- monitoring: file log, kibana, cloudwatch
- search: elastic search, algolia
- caching: redis, memcache
- other third party services
Basic build
recipe on an ubuntu server
- mysql
- PHP
- composer
- node
- nginx
- supervisor
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
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
nvm exec $version npm i yarn -g
Nginx
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
Nginx - Secure traffic using HTTPS
domain=$1
email=${2:-"[email protected]"}
sudo apt-get install -y python3-certbot-nginx
certbot certonly --nginx --rsa-key-size 4096 --email $email -d $domain -d www.$domain
Nginx - Secure traffic using HTTPS
# 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
Nginx - Secure traffic using HTTPS
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;
}
Nginx - Secure traffic using HTTPS
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;
Nginx - Secure traffic using HTTPS
server {
# ...
# 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
Nginx - Secure traffic using HTTPS
server {
# ...
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;
location = /favicon.ico
} { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# ...
}
Nginx - Secure traffic using HTTPS
server {
# ...
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;
}
# ...
Nginx - Secure traffic using HTTPS
server {
# ...
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;
}
# ...
Nginx - Secure traffic using HTTPS
server {
# ...
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;
# ...
}
EOF
Nginx - Secure traffic using HTTPS
# 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
Nginx - Secure traffic using HTTPS
cat >> /etc/PHP/$PHP_version/cli/PHP.ini << EOF
post_max_size=128M
upload_max_filesize=128M
max_upload_file=50
EOF
Nginx - 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 workers
#! /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
Supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start all
Talk Objectives
- learn the basics
- not the best way to deploy, not the only way
- knowledge is a journey not a road
- not a single solution, adapt for your use case
- basics are translatable to any new tools
No 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 use case.
No Conclusion - deployment strategies
- ansible for script automation and orchestration (ie ansible)
- aws code deploy
- CI / CD pipeline from github
No Conclusion - security
- servers
- in app
No Conclusion - monitoring
- uptime checker / status page
- you maybe don't need a microservice
- performance tips
No Conclusion - scale
- horizontal
- queues and workers
- database
- cache
- web / load balancing
Thank you for your attention
- https://bcd.dev
- https://twitter.com/babacarcissedia
- https://bcd.dev/post/deploy-laravel-on-your-own-vps-server-a-diy-guide-131
Foot notes
Markdown to PDF using Marp
npx @marp-team/marp-cli@latest presentation.md --pdf --allow-local-files