Nginx should expose only Laravel's public directory, pass application paths to index.php, and refuse hidden files or arbitrary PHP execution. On nginx 1.25.1+, use http2 on;, not listen ... http2.
server {
listen 443 ssl;
http2 on;
server_name example.com;
root /var/www/current/public;
index index.php;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
}
location ~ /\. { deny all; }
}
Use nginx -t before systemctl reload nginx. Keep TLS, redirect and FastCGI values in reviewed includes. If TLS terminates upstream, configure trusted proxies in Laravel; a client-supplied forwarded header is not trustworthy.
Reverse proxies and caching
WebSockets need HTTP/1.1 and upgrade headers. Cache immutable assets aggressively; do not cache authenticated HTML, cookie-sensitive pages or authorization responses without an explicit cache key and invalidation policy.
location /app/ {
proxy_pass http://reverb:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
A complete TLS boundary
Keep port 80 deliberately small and make the HTTPS virtual host the only place that serves Laravel. This prevents a forgotten HTTP server block from exposing a different root or PHP policy after a deployment.
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com;
root /var/www/current/public;
index index.php;
client_max_body_size 20m;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \\.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_param HTTPS on;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
}
location ~ /\\.(?!well-known).* { deny all; }
location ~* \\.(?:env|log|sql|bak)$ { deny all; }
}
$realpath_root resolves a release symlink before PHP-FPM receives the script.
That is important when a current symlink changes between requests. Do not add
a broad PHP location merely because an old CMS used one: Laravel has one public
front controller.
Cache artefacts, not personalised application responses
Vite assets have a content hash in their URL, which makes a long immutable cache
safe. HTML, /api, signed downloads and cookie-varying routes do not belong in
this location. A FastCGI cache is a separate system with a cache key, bypass
rules, invalidation and a proof that logged-in HTML cannot leak.
location ~* \\.(?:css|js|mjs|woff2|svg|png|jpe?g|webp)$ {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
Limit an abuse-prone route and verify the reload
Define a zone in http, then apply it narrowly to authentication endpoints.
It limits cheap abuse; it does not replace Laravel throttles, authorization or
webhook signature verification.
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
location = /login {
limit_req zone=login burst=10 nodelay;
try_files $uri $uri/ /index.php?$query_string;
}
sudo nginx -t
sudo systemctl reload nginx
curl -I https://example.com/health
When not to use this configuration
Do not paste this into shared hosting or a platform that owns Nginx; use its supported integration. Docker image builds, local development cache and deployment belong in focused guides, because their failure and rollback paths differ from a production proxy.