Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

How to Deploy Laravel on Ubuntu VPS with Nginx

By Kokil Thapa | Last reviewed: August 2026

Figuring out how to deploy Laravel on Ubuntu VPS with Nginx correctly prevents the majority of production outages I troubleshoot for clients. Most failures stem from incorrect file permissions, missing PHP-FPM socket configuration, or neglected OPcache invalidation rather than application bugs. This guide provides the exact server configuration, security hardening, and deployment workflow I use for production Laravel systems in 2026.

Before touching the server, ensure your local development environment matches production. If you are evaluating whether your current team can handle this infrastructure or if you need specialized help, reviewing the expectations for a Laravel developer in Nepal clarifies the skill gap between shared hosting management and true VPS administration. Getting the foundation right avoids costly rewrites later.

How do you prepare Ubuntu 24.04 for a Laravel production stack?

Ubuntu 24.04 LTS is the current standard for production PHP deployments in 2026. It ships with newer system libraries that align well with Laravel 12.x requirements. Do not use the default php package from the main repository; it often lags behind the latest stable release. Instead, add the Ondřej Surý PPA to get PHP 8.4 with all necessary FPM extensions.

sudo apt update && sudo apt upgrade -y
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update

sudo apt install -y nginx \
    php8.4-fpm php8.4-mysql php8.4-pgsql \
    php8.4-xml php8.4-curl php8.4-zip \
    php8.4-mbstring php8.4-bcmath \
    php8.4-gd php8.4-intl php8.4-redis \
    unzip git curl fail2ban ufw

After installation, verify the active PHP version and FPM status. A common mistake on fresh servers is having multiple PHP versions installed but the wrong one enabled in Nginx. Run php8.4 -v to confirm the CLI version, then check the FPM socket path:

sudo systemctl status php8.4-fpm
# Expected socket: /run/php/php8.4-fpm.sock

Security hardening must happen before deploying any code. Configure UFW to allow only SSH, HTTP, and HTTPS. Enable fail2ban immediately to protect against brute-force attacks on SSH and future web endpoints. For legal-tech portals handling sensitive client documents, this baseline protection is non-negotiable.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

sudo systemctl enable --now fail2ban
Ubuntu 24.04 Production StackNginx (Reverse Proxy + Static Assets)PHP 8.4-FPM (Socket: /run/php/php8.4-fpm.sock)Laravel App (/var/www/site/current)MySQL 8.4 / Redis 7.xUFW + Fail2ban + Let's Encrypt SSL
Production stack layers when configuring how to deploy Laravel on Ubuntu VPS with Nginx

How do you configure Nginx and PHP-FPM for Laravel securely?

The Nginx server block is where most deployments fail. Laravel requires all requests to route through index.php, and the document root must point exclusively to the /public directory. Never expose the project root to the web; doing so leaks .env files and source code. Create a new site configuration at /etc/nginx/sites-available/laravel-app:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/laravel-app/current/public;
    index index.php;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
    }

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

    access_log /var/log/nginx/laravel-app-access.log;
    error_log /var/log/nginx/laravel-app-error.log;
}

Enable the site and test the configuration before reloading. The fastcgi_param SCRIPT_FILENAME line using $realpath_root is critical for symlink-based deployments; without it, PHP resolves the symlink target incorrectly during atomic swaps, causing intermittent 404s or serving stale files.

sudo ln -s /etc/nginx/sites-available/laravel-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

PHP-FPM tuning matters as much as Nginx config. Edit /etc/php/8.4/fpm/pool.d/www.conf to match your server resources. On a 2GB RAM VPS, start with 10-15 workers. Set pm = dynamic with pm.max_children = 15, pm.start_servers = 5, and pm.max_requests = 500 to prevent memory leaks from long-running processes. Restart FPM after changes:

sudo nano /etc/php/8.4/fpm/pool.d/www.conf
sudo systemctl restart php8.4-fpm

What are the correct file permissions and ownership for Laravel?

Incorrect permissions cause more deployment failures than any other issue. The web server user (www-data) must own the storage and cache directories, while the deploy user should own the application code. This separation prevents the web process from modifying source files, reducing attack surface.

  1. Create the base directory structure outside the web root: sudo mkdir -p /var/www/laravel-app/{current,releases,shared}
  2. Set ownership: sudo chown -R deploy:www-data /var/www/laravel-app
  3. Configure shared directories for persistent data across deployments: storage/, .env, and any uploaded media folders
  4. Set restrictive permissions: find /var/www/laravel-app -type d -exec chmod 755 {} \; and find /var/www/laravel-app -type f -exec chmod 644 {} \;
  5. Grant write access only where needed: sudo chown -R www-data:www-data /var/www/laravel-app/shared/storage

On projects like legal service portals where document uploads are frequent, I create a separate uploads/ directory under shared/ with www-data ownership. This keeps user-generated content isolated from application code and survives deployments without permission resets. Never make the entire project writable by www-data; this is a security vulnerability that allows compromised PHP processes to inject malicious code.

Permission Model: deploy User vs www-dataApplication Code (deploy:deploy)755 dirs / 644 files/releases/20260816120000//current → symlink to releasevendor/, app/, config/Writable Storage (www-data:www-data)Persistent Across Deploys/shared/storage/logs//shared/storage/framework/cache//shared/.env/shared/uploads/ (user docs)⚠ Never make /current writable by www-data
Secure permission boundary between deploy-owned code and www-data-owned storage

How do you optimize PHP OPcache and SSL for Laravel performance?

OPcache is mandatory for production Laravel. Without it, PHP recompiles every file on each request, adding 200-500ms latency. Edit /etc/php/8.4/fpm/conf.d/10-opcache.ini with these validated settings for Laravel 12:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.jit=disable

Setting validate_timestamps=0 means PHP never checks for file changes, maximizing performance. The tradeoff is that you must explicitly reset OPcache after every deployment. Add opcache_reset(); to your deploy script's post-release hook, or use the cachetool utility to clear it via CLI without restarting FPM. JIT is disabled because Laravel's workload is I/O-bound; JIT adds overhead without measurable benefit for typical web applications.

SSL termination happens at Nginx. Use Certbot with the Nginx plugin for automatic certificate management. After obtaining certificates, Certbot modifies your server block to redirect HTTP to HTTPS and configure modern TLS parameters. Verify your SSL configuration scores A+ on SSL Labs by ensuring HSTS headers and OCSP stapling are enabled.

sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run

For sites serving Nepali users alongside global traffic, consider Cloudflare's free tier in front of your VPS. This provides DDoS protection and edge caching without changing your origin server configuration. However, ensure your Nginx config trusts Cloudflare's IP ranges so Laravel receives the real client IP for rate limiting and logging.

What is the most reliable zero-downtime deployment workflow?

Manual deployments via git pull in production cause downtime and inconsistency. Use Deployer 7 for atomic, symlink-based releases. This approach keeps the previous release intact, enabling instant rollback if something breaks. Install Deployer globally on your deployment machine:

composer global require deployer/deployer:^7.0

Create a deploy.php in your project root configured for Ubuntu 24.04 + Nginx:

<?php
namespace Deployer;

require 'recipe/laravel.php';

host('production')
    ->set('remote_user', 'deploy')
    ->set('hostname', 'your-server-ip')
    ->set('deploy_path', '/var/www/laravel-app')
    ->set('branch', 'main');

set('keep_releases', 3);
set('http_user', 'www-data');

task('deploy:opcache_reset', function () {
    run('sudo -u www-data php {{release_path}}/artisan opcache:reset');
});

after('deploy:symlink', 'deploy:opcache_reset');
after('deploy:failed', 'deploy:unlock');

This configuration handles Composer installation, migration, cache clearing, and symlink swapping atomically. The opcache_reset task runs after the symlink changes, ensuring PHP picks up new files immediately. Store sensitive environment variables in /var/www/laravel-app/shared/.env — never commit them to Git. Link shared directories in your deploy config so they persist across releases.

Deployment MethodDowntimeRollback SpeedComplexityBest For
Manual git pullHigh (minutes)Slow (manual revert)LowHobby projects only
FTP/SFTP uploadVery HighImpossibleMediumNever use for Laravel
Deployer 7 (symlink)Zero (<1s swap)Instant (dep rollback)MediumProduction Laravel apps
Docker/KubernetesZeroFast (image rollback)HighLarge teams/microservices

On sister sites sharing infrastructure, I standardize on Deployer 7 + GitLab CI. The pipeline runs tests, builds frontend assets locally (the server has no Node.js), commits the compiled assets as artifacts, then triggers Deployer. This eliminates build-time dependencies on the production server and ensures identical artifacts across staging and production.

Zero-Downtime Deployment Sequence1. Upload Release2. Install Deps3. Migrate DB4. Swap Symlink5. ResetAtomic Symlink Swap (ln -sfn)/var/www/laravel-app/current → /var/www/laravel-app/releases/20260816120000Previous release remains untouched for instant rollbackCritical: OPcache Reset After SymlinkWithout reset, PHP serves cached bytecode from old release → errors or stale behavior
Atomic deployment steps ensuring zero downtime and safe rollback capability

Deploy Laravel on Ubuntu VPS with Nginx Reliably

Mastering how to deploy Laravel on Ubuntu VPS with Nginx requires treating infrastructure as part of the application, not an afterthought. Correct permissions, validated OPcache settings, and atomic deployments separate production systems from fragile prototypes. Every step in this guide reflects configurations running on live client sites in 2026.

If your team lacks bandwidth to maintain this stack or you need a second opinion on an existing deployment, reach out directly. I regularly audit Laravel infrastructure for businesses in Nepal and internationally, identifying permission issues, missing optimizations, and deployment risks before they cause outages. For deeper context on building maintainable Laravel applications, review modern Laravel architecture best practices to ensure your codebase supports reliable operations.

Frequently Asked Questions

Laravel 12 requires PHP 8.2 or higher, so Ubuntu 22.04 or 24.04 LTS is recommended. You need at least 1GB RAM and 1 vCPU for small apps, though 2GB is safer for production workloads running Nginx, PHP-FPM, and MySQL simultaneously. Ensure Composer, Git, and Unzip are installed before starting.

A basic 2GB RAM VPS suitable for Laravel typically costs Rs 1,500 to Rs 3,000 per month (approx. USD 11–22). Local Nepali hosting providers often charge in NPR and include local support, while international providers like DigitalOcean or Hetzner bill in USD. Factor in domain costs and potential backup storage fees when budgeting.

Nginx handles high concurrency better than Apache due to its event-driven architecture and lower memory footprint per connection. For Laravel specifically, Nginx serves static assets directly without invoking PHP-FPM, reducing overhead. In my experience deploying legal-tech portals and eCommerce sites, Nginx consistently delivers faster response times under load compared to Apache's process-based model.

Create a config file at /etc/nginx/sites-available/your-site with root pointing to /var/www/your-site/public. Set index to index.php and add location / with try_files $uri $uri/ /index.php?$query_string. Configure a separate location ~ \.php$ block passing requests to unix:/run/php/php8.4-fpm.sock with fastcgi_params included. Always test with nginx -t before reloading.

Beyond php8.4-fpm, you must install php8.4-mysql, php8.4-xml, php8.4-mbstring, php8.4-curl, php8.4-zip, php8.4-bcmath, and php8.4-gd. Missing extensions cause silent failures during composer install or runtime errors. Run sudo apt install php8.4-{mysql,xml,mbstring,curl,zip,bcmath,gd} to install them all at once, then restart PHP-FPM.

Store credentials only in the .env file outside the web root, never commit it to Git. Set file permissions to 600 and ownership to www-data. Use APP_KEY generated via php artisan key:generate. For production, consider using Laravel's encrypted environment files or external secret managers. Never expose database passwords or API keys in version control or publicly accessible directories.

The storage/ and bootstrap/cache/ directories must be writable by www-data. Run sudo chown -R www-data:www-data storage bootstrap/cache and sudo chmod -R 775 storage bootstrap/cache. Incorrect permissions cause 500 errors when Laravel writes logs, caches views, or stores sessions. I've debugged this exact issue repeatedly after fresh deployments where the deploy user differs from the web server user.

Install certbot and python3-certbot-nginx via apt. Run sudo certbot --nginx -d yourdomain.com to automatically obtain and configure certificates. Certbot modifies your Nginx config to redirect HTTP to HTTPS and sets up auto-renewal via systemd timer. Verify renewal works with sudo certbot renew --dry-run. Always test SSL configuration using SSL Labs after setup to ensure proper security headers.

For any project beyond hobby work, use Deployer 7. Manual deployments risk downtime, inconsistent states, and forgotten steps. Deployer provides zero-downtime symlinked releases, atomic rollbacks via dep rollback, and shared persistent directories for .env and storage/. On sister sites like notarykathmandu.com and translationnepal.com, I use the same Deployer 7 + GitLab CI pipeline to eliminate human error during releases.

Edit /etc/php/8.4/fpm/pool.d/www.conf and set pm = dynamic with pm.max_children calculated as available RAM divided by average PHP process size (typically 30-50MB). Set pm.start_servers to 20% of max_children, pm.min_spare_servers to 10%, and pm.max_spare_servers to 30%. Enable opcache with opcache.validate_timestamps=0 in production and reload PHP-FPM after deploys to clear opcode cache.

This usually means PHP-FPM isn't running or the socket path in Nginx doesn't match the actual FPM socket. Check systemctl status php8.4-fpm and verify the socket exists at /run/php/php8.4-fpm.sock. Also confirm the Nginx fastcgi_pass directive points to the correct socket or TCP port. After upgrading PHP versions, old socket paths become stale; always validate both configs match.

Run php artisan migrate --force within your Deployer task after composer install but before symlink swap. Wrap migrations in maintenance mode using php artisan down before migrating and php artisan up after. Always backup the database first with mysqldump. For zero-downtime deploys, ensure migrations are backward-compatible so old code can still function until the new release activates. Never run migrations manually on production without automation.

Disable SSH password authentication and use key-only access. Configure UFW to allow only ports 22, 80, and 443. Install fail2ban to block brute-force attempts. Keep Ubuntu and PHP packages updated via unattended-upgrades. Remove default Nginx configs and disable server_tokens. Restrict database access to localhost only. Regularly audit installed packages and remove unused services. Security is layered; no single measure is sufficient.

First check if the bottleneck is PHP, database, or Nginx using tools like htop, slow query logs, and Laravel Debugbar. Enable opcache and verify it's active with php -i | grep opcache. Review Nginx access logs for high request volumes hitting PHP unnecessarily. Profile Eloquent queries for N+1 problems. Add Redis caching for expensive computations. On production systems I maintain, enabling query logging and opcache typically reveals the primary performance constraint within minutes.

Yes, create separate Nginx server blocks and PHP-FPM pools for each site. Each pool needs unique socket names and resource limits to prevent one app from starving others. Use distinct system users per site for isolation. Monitor memory usage carefully; each PHP-FPM pool consumes RAM independently. I've successfully hosted multiple legal-tech portals on shared EC2 infrastructure using this approach, but scale vertically or horizontally once combined resource usage exceeds 80%.

Share this article

Quick Contact Options
Choose how you want to connect me: