Nginx configuration for PHP in PHP & Laravel Development
Most Laravel deployments fail not because of code, but because the web server isn't configured correctly. We've seen production incidents traced back to a single missing directive in the Nginx config that caused requests to bypass Laravel's routing entirely. Getting Nginx right means understanding how it serves PHP, how it routes requests, and how it optimizes asset delivery—and that's what we're covering today on day 63 of the 100-day PHP & Laravel Development series.
Why Nginx configuration matters for Laravel
When you deploy a Laravel application, the web server (Nginx) sits between your users and your PHP application. Unlike Apache, Nginx doesn't execute PHP directly—it passes requests to PHP-FPM (FastCGI Process Manager) over a socket or TCP connection. The configuration you write determines whether requests reach Laravel's router, whether static assets bypass PHP entirely, and whether your application performs well under load.
A misconfigured Nginx server can silently break your application in ways that are hard to debug. You might see 404 errors on routes that work locally, or static assets loading but returning the wrong cache headers. We'll walk through the essential directives you need.
Root points to the public folder
The first rule of Laravel on Nginx: your root directive must point to the public folder, not the project root. Laravel's public folder contains index.php, and that's where web requests should land. Everything else in your project (app, config, storage, routes, etc.) sits outside the web root and remains invisible to HTTP requests.
Here's the structure you're protecting:
/var/www/myapp/
├── app/
├── config/
├── public/
│ ├── index.php
│ ├── css/
│ └── js/
├── routes/
├── storage/
└── vendor/
If you set root to /var/www/myapp instead of /var/www/myapp/public, Nginx will try to serve files directly from the project root, and users could potentially access your .env file or source code. More commonly, you'll get 404 errors because the files Nginx looks for won't be where it expects.
Here's the server block with the correct root:
server {
listen 80;
server_name example.com;
root /var/www/myapp/public;
index index.php index.html index.htm;
# Rest of configuration below
}
The index directive tells Nginx which file to serve when a directory is requested. By listing index.php first, we ensure Laravel's front controller is invoked for directory requests.
try_files to route to index.php
This is the critical piece that makes Laravel routing work. When a request comes in for a route like /users/123, Nginx first checks if that path exists as a real file or directory in the public folder. It doesn't—because the route is handled by Laravel's router, not a physical file. Without the right try_files directive, Nginx returns a 404.
The solution is to tell Nginx: "If the file doesn't exist, pass the request to index.php." Laravel's router will then parse the request and handle it.
server {
listen 80;
server_name example.com;
root /var/www/myapp/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# FastCGI configuration (below)
}
Breaking down try_files $uri $uri/ /index.php?$query_string:
$uri— First, check if the request path exists as a file (e.g.,/css/app.css).$uri/— Then, check if it exists as a directory./index.php?$query_string— If neither exists, rewrite the request to index.php and pass the query string along (so GET parameters are preserved).
This directive is why static assets (CSS, JS, images) work correctly—they're real files in the public folder, so Nginx serves them directly. But route requests go to index.php, which boots Laravel and invokes the router.
A real-world scenario: a user accesses /api/users?page=2. Nginx checks if /api/users exists as a file (it doesn't), checks if it's a directory (it's not), then rewrites to /index.php?page=2. Laravel boots, routes the request to your controller, and returns the user list.
FastCGI to PHP-FPM
Now that Nginx knows to send requests to index.php, it needs to actually execute PHP. It does this via FastCGI, communicating with PHP-FPM. PHP-FPM is a separate process manager that handles PHP execution, pooling requests across worker processes for concurrency.
Nginx doesn't talk to PHP-FPM by itself—you tell it how to do so with the fastcgi_pass directive. Most setups use a Unix socket (faster, more secure) rather than TCP.
server {
listen 80;
server_name example.com;
root /var/www/myapp/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
The location ~ \.php$ block matches any request ending in .php. When it matches:
fastcgi_passpoints to the PHP-FPM socket. Verify this path matches your PHP-FPM configuration (often/run/php/php8.2-fpm.sockor127.0.0.1:9000for TCP).fastcgi_param SCRIPT_FILENAMEtells PHP-FPM which file to execute.$document_rootis your root directive, and$fastcgi_script_nameis the request path.include fastcgi_paramsloads standard FastCGI variables like REQUEST_METHOD, QUERY_STRING, etc.
A common gotcha: if the socket path in your Nginx config doesn't match where PHP-FPM is actually listening, you'll see "502 Bad Gateway" errors. Check your PHP-FPM pool configuration (usually /etc/php/8.2/fpm/pool.d/www.conf) to confirm the socket path.
In production, we also add a timeout and buffer settings to prevent issues under high load:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
}
Gzip and cache headers for assets
Static assets (CSS, JavaScript, images) don't change often and don't need to hit PHP. Nginx can serve them directly and set cache headers so browsers and CDNs cache them, reducing load on your application.
First, enable Gzip compression in the http block (affects all servers):
http {
gzip on;
gzip_vary on;
gzip_types text/css text/javascript application/javascript application/json;
gzip_min_length 1000;
}
This compresses CSS, JavaScript, and JSON responses, reducing bandwidth. gzip_min_length avoids compressing tiny files (compression overhead isn't worth it).
For cache headers on static assets, add a location block in your server configuration:
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
This tells browsers to cache these files for 30 days. The immutable directive tells browsers never to revalidate, which is safe because Laravel typically busts asset filenames when they change (e.g., app.a1b2c3d4.js).
For dynamic content (HTML from Laravel), you typically want no caching or short-lived caching:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
Putting it all together
Here's a complete, production-ready Nginx configuration for Laravel:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/myapp/public;
index index.php;
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Static assets with long caching
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Main routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP execution
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 32k;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
We've also added a block to deny access to hidden files (like .env or .git), which is a security best practice.
Common mistakes
- Wrong root path — Setting root to the project directory instead of public. This breaks routing and exposes sensitive files.
- Missing try_files — Routes don't work; users get 404 errors. The application never receives requests.
- Incorrect PHP-FPM socket path — Results in 502 Bad Gateway. Always verify the socket path in PHP-FPM's configuration matches your Nginx config.
- No cache headers on assets — Every page load fetches CSS and JS again. Users experience slow page loads and your server handles unnecessary traffic.
- Executing PHP in the storage or vendor directories — If someone uploads a .php file, it could be executed. Use
location ~* /(?:storage|vendor).*\.php$to deny execution in those paths. - Forgetting fastcgi_param SCRIPT_FILENAME — PHP-FPM won't know which file to execute. Requests hang or return errors.
Key takeaways
- Always point root to the public folder, never the project root.
- Use try_files to route non-existent paths to index.php so Laravel's router can handle them.
- Configure fastcgi_pass with the correct PHP-FPM socket or TCP address, and set SCRIPT_FILENAME properly.
- Enable Gzip compression for text-based assets to reduce bandwidth.
- Set long cache expiry and immutable headers on static assets that have filename-based cache busting.
- Deny execution of PHP files outside the public folder (storage, vendor) for security.
- Test your configuration with
nginx -tbefore reloading and monitor error logs during deployment.






Comments
Loading comments…