Nginx for Laravel with PHP-FPM: production config, permissions and queues
Deploy Laravel on Nginx and PHP-FPM: a complete HTTPS config pointing at public, permissions for storage, the optimize and queue steps, and the errors you may see.
by OhSudo ·
The most important line in this config is root: it must point at Laravel’s public folder, not the project folder. With the wrong one, your .env, with its database password and app key, can be downloaded by anyone.
The Nginx Config Generator with these options writes the HTTPS and redirect parts for your domain. It doesn’t write PHP, so add the location blocks below.
The config
server { listen 80; listen [::]:80; server_name example.com; return 301 https://example.com$request_uri;}
server { listen 443 ssl; listen [::]:443 ssl; server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/myapp/public; index index.php; charset utf-8;
client_max_body_size 64m;
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;
# Only index.php runs, and no other PHP file. location ~ ^/index\.php(/|$) { fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; }
# Hidden files such as .env, but not Let's Encrypt's folder. location ~ /\.(?!well-known).* { deny all; }}1. Install PHP
sudo apt install nginx php-fpm php-mysql php-mbstring php-xml php-curl php-zip php-bcmath composerphp -vls /run/php/Change php8.3-fpm.sock in the config if your version differs. A wrong socket name is the usual reason for a 502.
2. Deploy the code
cd /var/www/myappcomposer install --no-dev --optimize-autoloadercp .env.example .env # first time only, then fill it inphp artisan key:generate # first time onlyphp artisan migrate --forcenpm ci && npm run buildIn .env, for production:
APP_ENV=productionAPP_DEBUG=falseAPP_URL=https://example.comAPP_DEBUG=true in production shows your settings and passwords on any error page, so check that one twice.
3. Permissions
PHP has to write in storage and bootstrap/cache, and nowhere else:
sudo chown -R root:www-data /var/www/myappsudo find /var/www/myapp -type d -exec chmod 750 {} +sudo find /var/www/myapp -type f -exec chmod 640 {} +sudo chown -R www-data:www-data /var/www/myapp/storage /var/www/myapp/bootstrap/cacheFiles uploaded by users need a link from public to storage, as Nginx only serves public:
php artisan storage:linkThe chmod Calculator explains these numbers.
4. Speed it up
Each of these needs running again after a deploy, or the old values stay in the cache:
php artisan config:cachephp artisan route:cachephp artisan view:cachephp artisan event:cacheAfter config:cache, the app no longer reads .env directly, so calling env() outside config files returns null. Then restart PHP-FPM, which also clears OPcache:
sudo systemctl reload php8.3-fpm5. Queues and the scheduler
These don’t run by themselves. For the queue, use a service in /etc/systemd/system/myapp-queue.service:
[Unit]Description=Laravel queue workerAfter=network.target
[Service]User=www-dataWorkingDirectory=/var/www/myappExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3 --max-time=3600Restart=always
[Install]WantedBy=multi-user.targetThe worker keeps your code in memory, so restart it on each deploy, with php artisan queue:restart. The systemd Unit Generator can write a fuller unit.
For the scheduler, add this line with sudo crontab -u www-data -e:
* * * * * cd /var/www/myapp && php artisan schedule:run >> /dev/null 2>&1If it doesn’t run, see cron job not running.
6. Get HTTPS, and turn it on
sudo certbot certonly --standalone -d example.com \ --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/sudo nginx -tsudo systemctl reload nginxIf that fails, see Certbot failed to authenticate.
Behind Cloudflare or a load balancer, tell Laravel to trust it, or it builds http:// links and can redirect in a loop. Since Laravel 11, this goes in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->trustProxies(at: ['10.0.0.0/8']);})Use your proxy’s real address range rather than * where you can.
When it goes wrong
| You see | Means | Go to |
|---|---|---|
500, with an empty page |
Read storage/logs/laravel.log |
Often permissions, as in section 3 |
The stream or file … could not be opened … Permission denied |
PHP can’t write in storage |
Section 3 |
404 on every route but / |
No try_files |
The config above |
403 Forbidden |
root isn’t the public folder, or wrong permissions |
Nginx 403 |
502 Bad Gateway |
Wrong PHP socket, or PHP-FPM stopped | Nginx 502 |
419 Page Expired |
A session or CSRF problem: check APP_URL, SESSION_DOMAIN and HTTPS |
Section 6 |
No application encryption key |
Run php artisan key:generate, then config:cache |
Section 2 |
SQLSTATE[HY000] [2002] |
Can’t reach the database | MySQL socket |
Warning
Check that .env really is out of reach: curl -I https://example.com/.env should give 403 or 404. Then also make sure it isn’t in git. If it has ever been public, change the app key and every password in it.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.