When SSH asks whether you want to continue connecting, type:
Update the package index and installed packages:
sudo apt update
sudo apt upgrade -y
Install basic tools used later in the deployment:
sudo apt install -y curl unzip git software-properties-common ca-certificates
Verify the server is running Ubuntu 24.04:
Expected output includes:
Description: Ubuntu 24.04 LTS

Step 2 — Install Nginx and PHP-FPM
Install Nginx, PHP 8.3-FPM, Composer, and the PHP extensions Laravel needs on Ubuntu 24.04.
sudo apt install -y nginx \
php8.3-fpm php8.3-cli php8.3-common \
php8.3-curl php8.3-mbstring php8.3-xml \
php8.3-bcmath php8.3-zip php8.3-sqlite3 \
php8.3-intl composer
Enable Nginx and PHP-FPM:
sudo systemctl enable nginx
sudo systemctl enable php8.3-fpm
sudo systemctl start nginx
sudo systemctl start php8.3-fpm
Verify both services are running:
sudo systemctl status nginx --no-pager
sudo systemctl status php8.3-fpm --no-pager
Expected output includes:
Check the installed versions:
clear
nginx -v
php -v
composer --version
Expected output includes Nginx, PHP 8.3, and Composer.

Verify Laravel's required PHP extensions are loaded:
php -m | grep -Ei 'ctype|curl|dom|fileinfo|filter|hash|mbstring|openssl|pcre|pdo|session|tokenizer|xml|bcmath'
Expected output includes these modules:
bcmath
ctype
curl
dom
fileinfo
filter
hash
mbstring
openssl
pcre
PDO
session
tokenizer
xml
Open SSH, HTTP, and HTTPS before sending public traffic to the server.
Allow SSH first:
Allow Nginx web traffic:
sudo ufw allow 'Nginx Full'
Enable UFW:
Verify the firewall rules:
Expected output:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
Nginx Full ALLOW Anywhere
Open the server IP in your browser:
Expected result: the default Nginx welcome page loads.
Step 4 — Create the Laravel application
Create a fresh Laravel application in /var/www/laravel. For a real project, replace the composer create-project command with your Git clone and run the same deployment steps after it.
Create the application directory:
sudo mkdir -p /var/www/laravel
sudo chown -R "$USER":www-data /var/www/laravel
Move into the directory:
Create a new Laravel project:
composer create-project laravel/laravel .
Verify Laravel is installed:
Expected output:
This tutorial was verified with:
Laravel Framework 13.17.0
Confirm the public entry point exists:
ls -la public/index.php artisan .env
Expected output includes:
artisan
.env
public/index.php
Set production environment values, create the SQLite database file, and give Nginx/PHP-FPM write access only where Laravel needs it.
Move into the app directory:
Set production environment values. For HTTP testing by IP, use your server IP:
sed -i 's/^APP_ENV=.*/APP_ENV=production/' .env
sed -i 's/^APP_DEBUG=.*/APP_DEBUG=false/' .env
sed -i 's#^APP_URL=.*#APP_URL=http://your_server_ip#' .env
Use SQLite for this first deployment:
sed -i 's/^DB_CONNECTION=.*/DB_CONNECTION=sqlite/' .env
sed -i '/^DB_HOST=/d;/^DB_PORT=/d;/^DB_DATABASE=/d;/^DB_USERNAME=/d;/^DB_PASSWORD=/d' .env
touch database/database.sqlite
Run the default migrations:
php artisan migrate --force
Expected output includes either migrations running or:
Set Laravel permissions without using chmod 777:
sudo chown -R "$USER":www-data /var/www/laravel
sudo chmod -R ug+rwX storage bootstrap/cache database
sudo find storage bootstrap/cache database -type d -exec chmod g+s {} \;
Clear and rebuild Laravel caches:
php artisan optimize:clear
php artisan optimize
Verify the environment:
clear
grep -E '^APP_ENV=|^APP_DEBUG=|^APP_URL=|^DB_CONNECTION=' .env
Expected output:
APP_ENV=production
APP_DEBUG=false
APP_URL=http://your_server_ip
DB_CONNECTION=sqlite
Configure Nginx to serve Laravel from the public directory and pass PHP requests to PHP-FPM.

Laravel must be served from the public directory. Do not point Nginx to /var/www/laravel, because the project root contains private files such as .env, config files, storage, logs, and application code.
Create the Nginx server block. For HTTP testing by IP, set APP_HOST to your VM public IP:
APP_HOST="your_server_ip"
APP_DIR="/var/www/laravel"
sudo tee /etc/nginx/sites-available/laravel > /dev/null <<NGINX
server {
listen 80;
listen [::]:80;
server_name ${APP_HOST};
root ${APP_DIR}/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index 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_page 404 /index.php;
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
NGINX
Disable the default Nginx site and enable the Laravel site:
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -sfn /etc/nginx/sites-available/laravel /etc/nginx/sites-enabled/laravel
Test the Nginx configuration:
Expected output:
syntax is ok
test is successful
Reload Nginx:
sudo systemctl reload nginx
Verify Nginx is serving the Laravel site over HTTP:
curl -I http://your_server_ip
Expected output includes:
HTTP/1.1 200 OK
Server: nginx
Open this URL in your browser:
Expected result: the Laravel welcome page loads through Nginx and PHP-FPM.

Step 7 — Enable HTTPS when a domain is ready
HTTPS requires a real domain or subdomain pointed to your VM. Let's Encrypt and Certbot cannot issue a normal browser-trusted certificate for only a raw IP address.
If you do not have a domain yet, skip this step for now and continue to Step 8. The Laravel deployment will still work over HTTP for testing. For production, return to this step after creating a DNS record.
Create this DNS record at your DNS provider:
| Type | Name | Value |
|---|
| A | laravel | your_server_ip |
That should produce a domain like:
After DNS resolves, update Laravel's APP_URL:
cd /var/www/laravel
APP_HOST="laravel.example.com"
sed -i "s#^APP_URL=.*#APP_URL=https://${APP_HOST}#" .env
Update the Nginx server_name to the domain:
sudo sed -i "s/server_name .*/server_name ${APP_HOST};/" /etc/nginx/sites-available/laravel
Test and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Install Certbot:
sudo apt install -y certbot python3-certbot-nginx
Request the certificate:
sudo certbot --nginx -d "$APP_HOST" --redirect
Follow the Certbot prompts for email, terms, and redirect behavior.
Verify HTTPS:
curl -I https://laravel.example.com
Expected output includes:
Verify automatic renewal:
sudo certbot renew --dry-run
Expected output includes:
Congratulations, all simulated renewals succeeded
<!-- Add this screenshot after a domain-backed HTTPS test:
File name: deploy-laravel-ubuntu-24-04-nginx-05-laravel-https-page.webp
Alt text: Laravel application loading over HTTPS with a valid SSL certificate in the browser
Placement: End of Step 7 — Enable HTTPS when a domain is ready
-->
Step 8 — Optimize Laravel for production
Install production Composer dependencies, cache Laravel configuration, and verify the production environment.
Move into the app directory:
Install production dependencies and optimize the autoloader:
composer install --no-dev --optimize-autoloader
Cache Laravel's production files:
Verify the environment:
php artisan about --only=environment
Expected output includes:
Environment
Application Name
Laravel Version
PHP Version
Environment
Debug Mode
Confirm debug mode is off:
grep -E '^APP_ENV=|^APP_DEBUG=|^APP_URL=' .env
Expected output for HTTP testing:
APP_ENV=production
APP_DEBUG=false
APP_URL=http://your_server_ip
Expected output after the HTTPS step:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://laravel.example.com
Step 9 — Verify the full Laravel deployment
Verify the browser endpoint, PHP-FPM handoff, Nginx logs, and Laravel logs before considering the deployment complete.
For HTTP testing, set:
APP_TEST_URL="http://your_server_ip"
If you completed HTTPS, use:
APP_TEST_URL="https://laravel.example.com"
Check the response headers:
Expected output for HTTP testing:
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Expected output after HTTPS:
Check that the Laravel page loads:
curl -s "$APP_TEST_URL" | grep -i "Laravel" | head
Expected output includes:
Check Nginx errors:
sudo tail -n 30 /var/log/nginx/error.log
Expected result: no new PHP-FPM, permission, or upstream errors. A line such as client closed keepalive connection is not a deployment failure.
Check Laravel logs:
sudo test -f /var/www/laravel/storage/logs/laravel.log \
&& sudo tail -n 30 /var/www/laravel/storage/logs/laravel.log \
|| echo "No Laravel log file yet."
Expected result: either no log file yet or no new production errors.

At this point, Laravel is verified through Nginx and PHP-FPM. If you used only the server IP, the deployment is verified over HTTP. If you completed the domain-backed Certbot step, the deployment is verified over HTTPS.
Cleanup
Use this section only if you want to remove the Laravel deployment from the VM.
⚠️ Warning: The following commands delete the Laravel application directory, Nginx site, and TLS certificate. Back up application code and data first.
If you issued a certificate, delete it first:
sudo certbot delete --cert-name laravel.example.com
Remove the Nginx site:
sudo rm -f /etc/nginx/sites-enabled/laravel
sudo rm -f /etc/nginx/sites-available/laravel
sudo nginx -t
sudo systemctl reload nginx
Delete the Laravel application:
sudo rm -rf /var/www/laravel
Remove public web firewall rules if this VM will no longer serve websites:
sudo ufw delete allow 'Nginx Full'
sudo ufw status
Troubleshooting
Nginx returns 403 or 404
Cause: Nginx is not pointing to Laravel's public directory, or the server block is not enabled.
Fix:
grep -R "root" /etc/nginx/sites-enabled/laravel
ls -la /var/www/laravel/public/index.php
sudo nginx -t
sudo systemctl reload nginx
The Nginx root must be:
Nginx returns 502 Bad Gateway
Cause: PHP-FPM is stopped, or the Nginx fastcgi_pass socket path is wrong.
Fix:
sudo systemctl status php8.3-fpm --no-pager
ls -la /run/php/php8.3-fpm.sock
grep fastcgi_pass /etc/nginx/sites-enabled/laravel
The server block should use:
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
Restart PHP-FPM and reload Nginx:
sudo systemctl restart php8.3-fpm
sudo systemctl reload nginx
Laravel returns 500 Server Error
Cause: Missing app key, wrong permissions, failed migration, or an invalid .env value.
Fix:
cd /var/www/laravel
php artisan key:generate --force
php artisan migrate --force
sudo chmod -R ug+rwX storage bootstrap/cache database
php artisan optimize:clear
php artisan optimize
Check the Laravel log:
sudo tail -n 50 /var/www/laravel/storage/logs/laravel.log
Certbot fails domain validation
Cause: The domain does not point to the VM public IP, or ports 80 and 443 are blocked.
Fix:
dig +short laravel.example.com
curl -I http://laravel.example.com
sudo ufw status
The domain must resolve to your Raff VM public IPv4 address, and UFW must allow Nginx Full.
The site works by IP but HTTPS is not available
Cause: HTTPS needs a domain. A raw IP address can load the Laravel app over HTTP, but Certbot needs a domain name for a normal browser-trusted certificate.
Fix: Create an A record pointing a subdomain to the VM IP, update APP_URL, update Nginx server_name, and run the Certbot commands in Step 7.
Uploaded files are not publicly visible
Cause: Laravel's public storage symlink has not been created.
Fix:
cd /var/www/laravel
php artisan storage:link
ls -la public/storage
Expected output shows public/storage pointing to storage/app/public.
Conclusion
You now have Laravel running on an Ubuntu 24.04 VM with Nginx, PHP-FPM, Composer, production environment values, correct writable directories, and terminal verification. In this test run, the application was verified over HTTP using the VM public IP. For production HTTPS, point a domain to the VM and complete the Certbot step.
If you have not deployed your Raff VM yet, you can launch one from the Raff VM product page.
Next steps:
Sources