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


Foot notes

Markdown to PDF using Marp

npx @marp-team/marp-cli@latest presentation.md --pdf --allow-local-files