Half of the problems people run into with Laravel on a live server have nothing to do with application code — they come from the PHP-FPM / Nginx / file permissions stack. A blank white screen instead of your site, a 502 Bad Gateway right after deployment, permission denied errors in storage — these are classic issues almost everyone hits when moving Laravel from a local Sail environment to a real VPS. Over the past several years I’ve deployed Laravel on dozens of servers, and this article walks through a working sequence of steps for Laravel 13 on Ubuntu 24.04 with Nginx, including the exact spots where people usually lose hours debugging.
Laravel 13 was released in March 2026 and requires PHP 8.3 as a minimum. The good news: Ubuntu 24.04 LTS ships with PHP 8.3 out of the box in its repositories, so in most cases you won’t need to pull in a third-party PPA just to get the right version.
What You’ll Need Before Starting
- A server running Ubuntu 24.04 (a fresh VPS or a clean install) with SSH access and sudo privileges.
- A domain or test subdomain, if you plan to configure HTTPS.
- A database — the examples here use MySQL 8, though PostgreSQL works just as well.
- 15–20 minutes of free time and some patience for file permissions — they’re usually what ruins the mood at the very last step.
Step 1: Update the System and Install Base Packages
Always start with a system update — it eliminates a good chunk of odd package-version conflicts down the line:
bash
sudo apt update && sudo apt upgrade -y sudo apt install -y curl unzip git software-properties-common
Don’t skip apt upgrade. Fresh images from VPS providers often ship with outdated ca-certificates packages, which can later cause curl or Composer to fail with SSL errors when fetching dependencies.
Step 2: Install PHP 8.3 and the Required Extensions
Laravel 13 needs PHP 8.3 or higher. On Ubuntu 24.04 it’s already in the main repository, so the ondrej/php PPA isn’t strictly required — but if you want guaranteed access to future PHP 8.4 minor releases, it’s still worth adding:
bash
sudo add-apt-repository ppa:ondrej/php -y sudo apt update
Install PHP-FPM along with the extensions Laravel needs to either run at all or avoid misbehaving on specific features:
bash
sudo apt install -y php8.3-fpm php8.3-cli php8.3-common \ php8.3-mysql php8.3-pgsql php8.3-mbstring php8.3-xml \ php8.3-curl php8.3-zip php8.3-bcmath php8.3-gd php8.3-intl php8.3-redis
Pitfall #1: a missing php8.3-mbstring or php8.3-xml won’t show up as an installation error — it’ll show up during composer install, with a vague message about a missing extension. It’s simpler to install the full set up front than to guess later what’s missing.
Verify the version:
bash
php -v
Make sure the output shows 8.3.x specifically, not an older system php-cli version — on servers with a history of upgrades, multiple PHP versions sometimes coexist, and update-alternatives can point to the wrong one.
Step 3: Install Composer
Composer is PHP’s dependency manager, and there’s no building Laravel without it:
bash
curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer composer --version
Step 4: Install Laravel 13
There are two workable approaches: the global Laravel installer or Composer directly. The second is more reliable in CI/CD scenarios, so it’s the one I typically use on servers:
bash
composer create-project laravel/laravel example-app "13.*" cd example-app
If you’re cloning an existing project repository instead, run composer install --no-dev --optimize-autoloader in place of create-project. The --no-dev flag is critical in production — it excludes development-only packages like Pest or Laravel Telescope, which have no business being in a production build.
Set up the environment:
bash
cp .env.example .env php artisan key:generate
Edit .env to set your database parameters, APP_URL, and — most importantly for production — APP_ENV=production and APP_DEBUG=false. Leaving APP_DEBUG enabled on a live server isn’t a minor oversight: any unhandled error will show visitors a full stack trace, including server paths, package versions, and sometimes the contents of .env.
Step 5: File Permissions — The Source of Most Problems
Nginx and PHP-FPM typically run as the www-data user. The storage and bootstrap/cache directories need write access, or Laravel won’t be able to write logs, cache files, and compiled Blade templates:
bash
sudo chown -R $USER:www-data /var/www/example-app
sudo find /var/www/example-app -type f -exec chmod 664 {} \;
sudo find /var/www/example-app -type d -exec chmod 775 {} \;
sudo chmod -R ug+rwx storage bootstrap/cache
Pitfall #2: the quick-and-dirty fix of running chmod -R 777 storage does technically work, but it’s a security hole — any process on the server gains write access to those directories. The correct approach is group ownership by www-data with 775 permissions, as shown above.
Step 6: Install and Configure Nginx
bash
sudo apt install -y nginx
Create a configuration file for the site:
bash
sudo nano /etc/nginx/sites-available/example-app
Here’s a base Nginx configuration for Laravel, tested across a number of projects:
nginx
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example-app/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 ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Three details people most often overlook:
rootpoints to thepublicfolder, not the project root. This is the single most common reason a visitor sees a file listing instead of the site — or worse, gets direct access to the.envsource file.- The PHP-FPM socket path must match the actual PHP version in use. If you have multiple versions installed,
php8.3-fpm.sockmight not match the version active in the CLI — check withls /run/php/. - The
location ~ /\.(?!well-known).*block blocks access to dotfiles, including.envand.git. Without it,.envcan theoretically be downloaded directly via URL if the configuration is misconfigured elsewhere — this is a real vulnerability that automated scanners actively look for.
Enable the configuration:
bash
sudo ln -s /etc/nginx/sites-available/example-app /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
Running nginx -t before reload is not optional — it checks the syntax and saves you from taking down a working site over a typo in the config.
Step 7: Configure the Database and Run Migrations
bash
sudo mysql -e "CREATE DATABASE laravel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" sudo mysql -e "CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'strong_password';" sudo mysql -e "GRANT ALL PRIVILEGES ON laravel_db.* TO 'laravel_user'@'localhost';"
Enter these credentials in .env, then run the migrations:
bash
php artisan migrate --force
The --force flag is necessary because in a production environment Laravel asks for confirmation before running migrations by default — without the flag, the command will simply hang inside an interactive prompt during your deploy script.
Step 8: Cache Configuration and Optimize for Production
bash
php artisan config:cache php artisan route:cache php artisan view:cache composer install --optimize-autoloader --no-dev
Pitfall #3, the most annoying one: if you’ve run config:cache and then change a variable in .env and simply reload the site — the change won’t take effect. Laravel will read the cached config, not the .env file. After any edit to .env in production, always run php artisan config:clear and re-run config:cache afterward. This detail breaks deploy scripts more often than you’d expect.
Step 9: HTTPS via Let’s Encrypt
bash
sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com
Certbot will automatically add the port-443 block and set up the redirect from HTTP. Verify certificate auto-renewal:
bash
sudo certbot renew --dry-run
Verifying the Result
Open the site in your browser. If you see a 502 Bad Gateway error, it’s almost always an incorrect PHP-FPM socket path in the Nginx config, or the php8.3-fpm service isn’t running (sudo systemctl status php8.3-fpm). If you see a blank white page with no visible error, temporarily set APP_DEBUG=true, check the actual error in your browser or in storage/logs/laravel.log, and then make sure to set APP_DEBUG=false again afterward.
FAQ
What PHP version does Laravel 13 require? Laravel 13 requires PHP 8.3 at minimum. Ubuntu 24.04 includes PHP 8.3 in its standard repositories, so an additional PPA isn’t required for a basic install.
Can I use Apache instead of Nginx for Laravel 13? Yes, Laravel isn’t tied to a specific web server. That said, Nginx as a reverse proxy in front of PHP-FPM generally performs better under high load thanks to its asynchronous connection-handling model.
Why does the site return a 502 Bad Gateway after deployment? The most common cause is an incorrect path to the PHP-FPM unix socket in the Nginx configuration, or the php8.3-fpm service being stopped. Check systemctl status php8.3-fpm and the socket path under /run/php/.
Should I use chmod 777 for the storage folder? No, that’s an unsafe practice. The correct approach is to make www-data the group owner and set permissions to 775, as described in Step 5.
What if changes to .env don’t take effect after editing it? If config:cache was run previously, Laravel reads the cached configuration instead of .env directly. Run php artisan config:clear, then config:cache again if needed.