VPS Deployment with Nginx

This guide covers deploying the backend (PHP-FPM) and frontend (static SPA) on separate subdomains using Nginx, with SSL via Let's Encrypt.

Prerequisites

  • Ubuntu 22.04+ VPS with root access
  • PHP 8.2+ with required extensions (mbstring, xml, curl, pgsql/mysql, redis, zip)
  • Composer
  • Node.js 18+ and npm
  • Nginx
  • PostgreSQL or MySQL
  • Redis
  • Certbot (for Let's Encrypt SSL)

Backend: Nginx Configuration

Create /etc/nginx/sites-available/api.yourdomain.com:

server {
    listen 80;
    server_name api.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

    root /var/www/backend/public;
    index index.php;

    charset utf-8;
    client_max_body_size 20M;

    # Laravel pretty URLs
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny dotfiles
    location ~ /\.(?!well-known) {
        deny all;
    }
}

Frontend: Nginx Configuration

Create /etc/nginx/sites-available/app.yourdomain.com:

server {
    listen 80;
    server_name app.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name app.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/app.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.yourdomain.com/privkey.pem;

    root /var/www/frontend/dist;
    index index.html;

    # SPA: route all paths to index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Enable Sites

sudo ln -s /etc/nginx/sites-available/api.yourdomain.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/app.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

SSL with Let's Encrypt

# Install certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificates
sudo certbot --nginx -d api.yourdomain.com
sudo certbot --nginx -d app.yourdomain.com

# Auto-renewal is configured automatically
# Verify with:
sudo certbot renew --dry-run

Supervisor for Queue Workers

Create /etc/supervisor/conf.d/saaskitfy-worker.conf:

[program:saaskitfy-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/backend/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/supervisor/saaskitfy-worker.log
stopwaitsecs=3600
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start saaskitfy-worker:*

File Permissions

sudo chown -R www-data:www-data /var/www/backend/storage
sudo chown -R www-data:www-data /var/www/backend/bootstrap/cache
sudo chmod -R 775 /var/www/backend/storage
sudo chmod -R 775 /var/www/backend/bootstrap/cache

Deploy Script Example

#!/bin/bash
cd /var/www/backend

git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
sudo supervisorctl restart saaskitfy-worker:*

cd /var/www/frontend
git pull origin main
npm install
npm run build