How to Deploy a Laravel App on an Ubuntu VPS (Nginx + PHP-FPM)

NE
Neema Kessy
· May 27, 2026 · 8 min read

A complete, production-grade walkthrough for deploying a Laravel application on a Momo Cloud Ubuntu VPS using Nginx, PHP-FPM 8.3, MySQL and Composer — from SSH login through Let's Encrypt and production caches.

Shared hosting is fine for a brochure site, but a real Laravel application deserves a server you control. On a VPS you choose the PHP version, run queue workers and the scheduler, tune Nginx, and deploy straight from Git. This guide walks through a complete, production-grade deployment of a Laravel app on a Momo Cloud Ubuntu 22.04/24.04 VPS using the LEMP stack — Nginx, PHP-FPM 8.3, MySQL and Composer.

By the end you will have your app served over HTTPS, with the database migrated, caches warmed, and the correct file ownership in place. The commands below are written for Ubuntu and use apt and systemd throughout.

Before You Start

  • An active Momo Cloud VPS running Ubuntu 22.04 or 24.04. If you do not have one yet, order it from cloud.momo.tz and pay the invoice with M-Pesa, Tigo Pesa, Airtel Money or card; the server is provisioned automatically.
  • Your server's IP address and root password — both appear on the VPS detail page in the client area (reveal the password with the eye icon). You can also open the in-browser Console if SSH ever locks you out.
  • A domain pointed at your VPS IP via an A record. Use Momo Cloud nameservers ns1.momo.tz and ns2.momo.tz; propagation can take up to 24 hours.
  • A Laravel project in a Git repository.

Step 1: SSH In and Update the System

Connect to your server using the IP address from the client area. Replace the IP with your own.

ssh root@YOUR_SERVER_IP

Refresh the package index and apply pending updates before installing anything.

apt update && apt upgrade -y

Tip: Working as root is fine for the initial setup, but for day-to-day operations create a sudo user and harden SSH with key-based login. See our guide on securing your VPS with a firewall, SSH keys and Fail2ban.

Step 2: Install Nginx

apt install nginx -y
systemctl enable --now nginx

If UFW is active, allow web traffic so requests can reach the server.

ufw allow 'Nginx Full'
ufw allow OpenSSH

Step 3: Install PHP 8.3 and Required Extensions

Ubuntu 24.04 ships PHP 8.3 in its default repositories. On 22.04, add the well-known ondrej/php PPA first so you can install 8.3.

add-apt-repository ppa:ondrej/php -y
apt update

Install PHP-FPM and the extensions Laravel needs. The php8.3-mysql, mbstring, xml, bcmath, curl and zip packages cover the common requirements.

apt install php8.3-fpm php8.3-cli php8.3-mysql php8.3-mbstring \
  php8.3-xml php8.3-bcmath php8.3-curl php8.3-zip php8.3-gd -y

Confirm the version and that PHP-FPM is running.

php -v
systemctl status php8.3-fpm

Step 4: Install MySQL

MySQL is a solid default for Laravel; MariaDB works identically if you prefer it.

apt install mysql-server -y
systemctl enable --now mysql

Run the security script to set a root password and remove insecure defaults.

mysql_secure_installation

Create the Database and User

Open the MySQL prompt and create a dedicated database and user for the application. Use a strong, unique password.

mysql -u root -p
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'CHANGE_ME_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Step 5: Install Composer

Composer manages your PHP dependencies. Install it globally so it is available as a single composer command.

curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
composer --version

Step 6: Clone the Project into /var/www

Place the application under /var/www. Replace the repository URL and folder name with your own.

cd /var/www
git clone https://github.com/your-org/myapp.git myapp
cd /var/www/myapp

Install dependencies optimised for production — no dev packages, and a classmap autoloader for speed.

composer install --no-dev --optimize-autoloader

Step 7: Configure the Environment

Copy the example environment file and generate an application key.

cp .env.example .env
php artisan key:generate

Edit .env and set the production values. At minimum, configure the environment, debug flag, app URL and database credentials.

nano .env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=myapp_user
DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD

Warning: Never commit .env to Git — it holds your database password, app key and API secrets. It is in Laravel's .gitignore by default; keep it there. With APP_DEBUG=false, Laravel hides stack traces from visitors, so secrets never leak through an error page.

Step 8: Run Migrations

Build the database schema. The --force flag is required to run migrations in a production environment without an interactive prompt.

php artisan migrate --force

Step 9: Set Ownership and Permissions

Nginx and PHP-FPM run as the www-data user, so the web server must own the files it writes to. Laravel only needs to write to storage and bootstrap/cache.

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

Warning: A great many "permission denied" and "failed to open stream" errors come down to forgetting this step. If logging, sessions or cached views break after deploy, re-run the chown on storage and bootstrap/cache first.

Step 10: Create the Nginx Server Block

Laravel serves from its public directory, never the project root. Create a server block that points there and forwards PHP to the PHP-FPM socket.

nano /etc/nginx/sites-available/myapp

Paste the following, replacing yourdomain.com and the document root with your own values.

server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/myapp/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:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

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

Save and exit. Enable the site, remove the default site so it does not shadow yours, test the configuration, then reload Nginx.

ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

The nginx -t output must report syntax is ok and test is successful before you reload. Visit http://yourdomain.com and your application should load over plain HTTP.

Step 11: Secure the Site with Let's Encrypt

Free, automatically renewing TLS certificates are available from Let's Encrypt via Certbot. Make sure your domain's A record already points at the VPS before running this.

apt install certbot python3-certbot-nginx -y
certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot edits your server block to add the HTTPS listener and a redirect from port 80. Confirm the automatic renewal timer is healthy.

systemctl status certbot.timer
certbot renew --dry-run

Step 12: Warm the Production Caches

Laravel can compile your config, routes and views into fast cached files. Always do this after the .env is final, and re-run it on every deploy.

php artisan config:cache
php artisan route:cache
php artisan view:cache
Command What it caches Clear with
config:cache All config files into one file (reads .env once) config:clear
route:cache Route definitions for faster registration route:clear
view:cache Pre-compiled Blade templates view:clear

Tip: Once config is cached, env() calls outside config files return null. Always read environment values through config() in your application code, and re-run config:cache whenever you change .env.

Queues and the Scheduler

Most production apps need two more pieces. Laravel's scheduler runs from a single system cron entry that fires every minute:

* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1

For background jobs you run a queue worker with php artisan queue:work. Because that process must stay alive and restart on failure or reboot, do not run it by hand — supervise it with Supervisor (or a systemd service), which keeps the worker running and restarts it automatically. We will cover a full Supervisor configuration in a separate guide.

Redeploying Later

On subsequent deploys the routine is short: pull the latest code, reinstall production dependencies, migrate, and rebuild the caches.

cd /var/www/myapp
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
chown -R www-data:www-data storage bootstrap/cache

Wrapping Up

You now have a Laravel app deployed the way a senior engineer would do it: served from public through Nginx and PHP-FPM 8.3, backed by a dedicated MySQL database, locked behind a free Let's Encrypt certificate, and running with compiled production caches. Keep .env out of Git, keep storage owned by www-data, and add Supervisor when you are ready for queues.

Need a server to put this on? Spin up an Ubuntu VPS from the Momo Cloud client area at cloud.momo.tz — and if you hit a snag, our 24/7 support team is one ticket away, in English or Swahili.

NE
Written by

Neema Kessy

Sharing insights on hosting, cloud, security and the technology that powers your business online.