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.tzandns2.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.