
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
A fresh Ubuntu VPS is a blank box — no web server, no PHP, no idea your Laravel app exists. The gap between "I have a server" and "my site is live over HTTPS" is where most first deploys stall: wrong document root, a 502 from a mismatched PHP-FPM socket, or a storage/ directory Laravel cannot write to. This guide closes that gap. To deploy Laravel on an Ubuntu VPS with Nginx, you provision the box, install PHP 8.4-FPM, MySQL and Composer, configure one Nginx server block, fix permissions, add free SSL, and run artisan optimize — in that order. If you would rather hand this off, my DevOps and cloud deployment services cover it end to end.
public/ directory, pass PHP to the FPM socket, set www-data ownership on storage/ and bootstrap/cache/, obtain a Let's Encrypt certificate, then run migrations and php artisan optimize.public/index.php.What do you need before you deploy Laravel on an Ubuntu VPS?
Five things make the rest of this walkthrough smooth and repeatable:
- An Ubuntu 24.04 LTS VPS with a public IP and root or
sudoaccess. 1 GB RAM is enough for a small app; 2 GB is comfortable once you run queues and a cache. - A domain name with an
Arecord pointing at the server's IP. SSL issuance later depends on DNS already resolving. - SSH access as a non-root sudo user. Deploying as
rootis a habit worth breaking on day one. - Your Laravel repository reachable over SSH or HTTPS (GitHub, GitLab, or Bitbucket), with
composer.lockcommitted. - A matching PHP version in mind — this guide targets PHP 8.4, the current stable release that Laravel 11 and 12 fully support.
How do you install Nginx, PHP 8.4-FPM, MySQL and Composer on Ubuntu?
Start by updating the package index and installing the web stack. Ubuntu 24.04 ships PHP 8.3 in its default repositories, so add Ondřej Surý's well-maintained PPA to get PHP 8.4:
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx mysql-server git unzip curl
sudo add-apt-repository -y ppa:ondrej/php
sudo apt update
sudo apt install -y php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring \
php8.4-xml php8.4-bcmath php8.4-curl php8.4-zip php8.4-intl php8.4-gd Those extensions cover what a typical Laravel app needs: mbstring, xml, bcmath, curl, zip, and intl are effectively required, and gd handles image work. Next, install Composer globally:
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
composer --version Now secure MySQL and create a dedicated database and user for the app. Never let your application connect as root:
sudo mysql_secure_installation
sudo mysql -e "CREATE DATABASE laravel_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -e "CREATE USER 'laravel'@'localhost' IDENTIFIED BY 'a-strong-password-here';"
sudo mysql -e "GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel'@'localhost';"
sudo mysql -e "FLUSH PRIVILEGES;" Confirm the FPM service is running with systemctl status php8.4-fpm and note the socket path it exposes — /run/php/php8.4-fpm.sock — because the Nginx server block points straight at it.
How do you clone and configure the Laravel app on the server?
Deploy the code into /var/www, install production dependencies, and build the environment file. Cloning as your sudo user (not root) keeps ownership sane before the final permission step:
cd /var/www
sudo git clone https://github.com/you/your-laravel-app.git example.com
cd example.com
composer install --no-dev --optimize-autoloader
cp .env.example .env
php artisan key:generate Open .env and set the environment to production, disable debug, and plug in the database credentials you just created. Getting APP_DEBUG=false right matters — leaving it on in production leaks stack traces and configuration to visitors:
APP_NAME="Your App"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel
DB_PASSWORD=a-strong-password-here How do you set the correct permissions on storage and bootstrap/cache?
This is the single most common reason a fresh Laravel deploy shows a 500 error. Nginx and PHP-FPM run as the www-data user, and Laravel must be able to write logs, compiled views, and the framework cache. Give www-data group ownership of the app and full write access to only the two directories Laravel writes to:
sudo chown -R $USER:www-data /var/www/example.com
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo chgrp -R www-data storage bootstrap/cache
sudo chmod -R ug+rwx storage bootstrap/cache The principle: files are readable, directories are traversable, and only storage/ and bootstrap/cache/ are group-writable. Do not chmod -R 777 the whole project — it is the fast path to both broken uploads and a security incident.
public/, and only storage/ and bootstrap/cache/ are writable by the www-data group.How do you configure the Nginx server block for Laravel?
Create a server block that roots at public/, routes every request that is not a real file through index.php, and forwards PHP to the FPM socket. Save this as /etc/nginx/sites-available/example.com:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
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.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
} Three lines do the heavy lifting. root must end in /public — pointing at the project root would expose your .env and source. The try_files directive is Laravel's front-controller pattern: serve the file if it exists, otherwise let index.php handle routing. And fastcgi_pass must match the exact socket PHP 8.4-FPM created. Enable the site, test the config, and reload:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx If nginx -t reports a syntax error, fix it before reloading — a bad config will refuse to reload and leave the old one running. A 502 Bad Gateway after this step almost always means the fastcgi_pass socket path does not match the running FPM version. This server-side setup is the natural companion to an automated pipeline; once it works by hand, wire it into a GitLab CI/CD pipeline for Laravel so future releases ship with zero downtime.
How do you add free SSL and finish the Laravel deployment?
With DNS pointing at the server and Nginx serving on port 80, Certbot can obtain and install a free Let's Encrypt certificate in one command. Install it from the snap store, which is the method Certbot recommends in 2026:
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot --nginx -d example.com -d www.example.com Certbot rewrites your server block to listen on 443, installs the certificate, and adds a permanent redirect from HTTP to HTTPS. Renewal is automatic via a systemd timer — verify it with sudo certbot renew --dry-run. Finally, run migrations and cache Laravel's configuration, routes, views, and events into a single optimized bundle:
php artisan migrate --force
php artisan storage:link
php artisan optimize php artisan optimize is the production accelerator: it runs config:cache, route:cache, view:cache, and event:cache together, so the framework skips filesystem discovery on every request. Remember to re-run it after any deploy that changes config or routes, since a stale config cache will keep serving old values. Your Laravel app is now live over HTTPS on your Ubuntu VPS.
Conclusion
You now have a repeatable path to deploy Laravel on an Ubuntu VPS with Nginx: provision the box, install PHP 8.4-FPM and MySQL, root Nginx at public/, keep storage/ and bootstrap/cache/ writable by www-data, add Let's Encrypt SSL, and finish with migrate and php artisan optimize. Do it once by hand so you understand every moving part, then automate it. If you want this provisioned, secured, and monitored for your own project, get in touch about a deployment or browse the DevOps case studies to see production setups I have shipped.