How to Set Up Nginx for a Laravel Application

How to Set Up Nginx for a Laravel Application

The first time I deployed a Laravel app to a production server, I made the classic beginner mistake: I pointed Nginx’s document root straight at the project folder instead of the public directory. Suddenly my .env file, my app folder, and my entire codebase were browsable from the internet. Nobody exploited it before I caught it, but it scared me enough that I’ve never made that mistake again — and I’ve made sure to explain it clearly every time I write about this topic since.

This guide covers everything you need to correctly and securely serve a Laravel application with Nginx and PHP-FPM.

Why Nginx for Laravel?

Laravel doesn’t ship with its own production-grade web server (the php artisan serve command is explicitly for local development only). In production, the standard, battle-tested combination is Nginx as the web server, paired with PHP-FPM (FastCGI Process Manager) to actually execute your PHP code. Nginx handles incoming HTTP requests, serves static files directly, and passes anything that needs PHP execution off to PHP-FPM over a socket or TCP connection.

This combination is popular because Nginx is lightweight and handles concurrent connections efficiently, while PHP-FPM gives you fine-grained control over how many PHP worker processes run and how they’re managed.

Requirements

  • A Linux server (Ubuntu 22.04/24.04 examples here)
  • Nginx installed
  • PHP-FPM installed, matching the PHP version your Laravel app needs (Laravel 11 requires PHP 8.2+, for example — check your composer.json)
  • Your Laravel application deployed to the server, with composer install --optimize-autoloader --no-dev already run
  • Correct file permissions on the storage and bootstrap/cache directories

Install PHP-FPM and common Laravel extensions:

sudo apt update
sudo apt install php-fpm php-mysql php-mbstring php-xml php-curl php-zip php-bcmath php-gd -y

Confirm PHP-FPM is running:

sudo systemctl status php8.2-fpm

(Adjust the version number to match what’s installed — php -v will tell you.)

Understanding the Document Root Rule

This is the single most important thing to get right, so I’ll say it plainly: Nginx’s root must point to Laravel’s public directory, never to the project root.

Laravel’s public/index.php file is the only PHP file meant to be publicly reachable. Everything else — your .env file with database credentials, your app directory with your business logic, your vendor directory with third-party code — must never be directly accessible over HTTP. Laravel’s architecture assumes this boundary is enforced by the web server config, not by Laravel itself.

/var/www/myapp/           ← project root, NOT the web root
/var/www/myapp/public/    ← THIS is what Nginx should serve

Step 1: Create the Nginx Server Block

sudo nano /etc/nginx/sites-available/myapp

Here’s a complete, production-ready configuration:

server {
    listen 80;
    server_name myapp.example.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:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

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

    client_max_body_size 20M;
}

Let me walk through the parts that matter most:

  • root /var/www/myapp/public; — as discussed, this is non-negotiable for security.
  • try_files $uri $uri/ /index.php?$query_string; — this implements Laravel’s “pretty URL” routing. If a requested file doesn’t physically exist, the request gets routed through index.php, which lets Laravel’s router take over.
  • fastcgi_pass unix:/run/php/php8.2-fpm.sock; — this is the socket path PHP-FPM listens on. Confirm yours matches by checking /etc/php/8.2/fpm/pool.d/www.conf for the listen directive.
  • location ~ /\.(?!well-known).* { deny all; } — this blocks access to all dotfiles (like .env and .git) except the .well-known directory, which is needed for things like Let’s Encrypt’s ACME challenge and Apple Pay domain verification.
  • fastcgi_hide_header X-Powered-By; — a small hardening step that avoids leaking your PHP version in response headers.

Step 2: Enable the Site

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

Step 3: Set Correct Permissions

Laravel needs to write to storage and bootstrap/cache. I set ownership to the web server user (www-data on Debian/Ubuntu) and restrict permissions appropriately:

sudo chown -R www-data:www-data /var/www/myapp
sudo find /var/www/myapp -type f -exec chmod 644 {} \;
sudo find /var/www/myapp -type d -exec chmod 755 {} \;
sudo chmod -R ug+rwx /var/www/myapp/storage /var/www/myapp/bootstrap/cache

Step 4: Configure Your .env for Production

Before going live, double-check these in your .env:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://myapp.example.com

APP_DEBUG=false is critical — leaving debug mode on in production means detailed stack traces (including file paths and environment variables) get shown to anyone who triggers an error. I’ve seen this leak database credentials on more than one audit.

Then cache Laravel’s configuration for a performance boost:

php artisan config:cache
php artisan route:cache
php artisan view:cache

Adding HTTPS

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myapp.example.com

Certbot updates your server block automatically, adding a 443 listener and redirecting HTTP to HTTPS. Once that’s done, update APP_URL in .env to use https:// and re-run php artisan config:cache.

Testing Your Setup

  1. sudo nginx -t — validate syntax
  2. curl -I http://myapp.example.com — confirm a response, ideally a redirect to HTTPS if Certbot is configured
  3. Visit the site in a browser and confirm the homepage loads
  4. Test a route that hits the database to confirm PHP-FPM and your DB connection both work
  5. Try directly requesting /.env in a browser — you should get a 403 or 404, never the actual file contents
  6. Check php artisan route:list matches what you see working in the browser

Troubleshooting Common Issues

502 Bad Gateway — Usually means PHP-FPM isn’t running, or the socket path in your Nginx config doesn’t match the actual PHP-FPM socket. Run sudo systemctl status php8.2-fpm and check ls -l /run/php/.

404 on every route except the homepage — This almost always means the try_files directive is missing or misconfigured, so Laravel’s router never gets a chance to handle the request.

“The stream or file … could not be opened” in Laravel logs — A permissions issue on storage/logs. Re-run the chown/chmod commands above.

White screen with no error — Check storage/logs/laravel.log first, then Nginx’s error log at /var/log/nginx/error.log. If both are empty, temporarily set APP_DEBUG=true (only on a non-public staging environment!) to see the actual error, then set it back to false.

Uploaded files failing — Increase client_max_body_size in Nginx and upload_max_filesize/post_max_size in php.ini.

Security Considerations

  • Never expose the project root — only public/
  • Block dotfile access as shown above
  • Keep APP_DEBUG=false in production, always
  • Rotate your APP_KEY only when you understand the consequences (it invalidates existing encrypted data and sessions)
  • Run composer install --no-dev in production to avoid shipping dev dependencies
  • Set up a firewall (ufw) to only expose ports 80/443 and your SSH port
  • Consider fail2ban for repeated failed login attempts on Laravel’s auth routes

Performance Tips

  • Enable OPcache in php.ini — this alone can meaningfully speed up PHP execution by caching compiled bytecode
  • Use php artisan config:cache, route:cache, and view:cache in every deployment — skipping this is one of the most common reasons Laravel apps feel slow in production
  • Enable gzip in Nginx for text-based responses
  • Use a queue worker (Laravel Horizon or plain queue:work with Supervisor) for anything slow — sending emails, processing uploads — instead of doing it inline during the request
  • Tune PHP-FPM’s process manager — for most small-to-medium apps, pm = dynamic with sensible pm.max_children based on available RAM works well; for high-traffic apps, benchmark and adjust

Real-World Use Case

I’ve deployed this exact pattern for a SaaS product doing several hundred requests per second at peak: Nginx serving static assets directly, PHP-FPM tuned with pm.max_children sized to available memory, Laravel’s config/route/view caches warmed on every deploy, and Redis handling both the cache and queue drivers. The whole stack ran comfortably on a modest VPS because Nginx and PHP-FPM are both efficient by default — the config above is genuinely close to what I use in production, not a simplified teaching example.

Best Practices Recap

  • Document root is always public/, never the project root
  • Block .env, .git, and other dotfiles explicitly
  • APP_DEBUG=false in production, no exceptions
  • Cache config, routes, and views on every deploy
  • Correct ownership and permissions on storage and bootstrap/cache
  • HTTPS via Let’s Encrypt, with APP_URL matching
  • Queue anything slow instead of blocking the request cycle

Getting Laravel and Nginx working together really comes down to respecting that document root boundary and getting the try_files directive right. Once those two things click, everything else — caching, HTTPS, performance tuning — is just refinement on top of a solid foundation.

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Nginx for a Flask Application

How to Set Up Nginx for a Flask Application

Next Post
How to Set Up Nginx for a Java Application

How to Set Up Nginx for a Java Application

Related Posts