The Laravel Production Server Checklist: From Code to Live Traffic

Tuning PHP-FPM, OPcache, queue workers, config caching, permissions, and security can cut Laravel response time by more than half and close every open door before the first visitor arrives.
The short list: what makes a Laravel server production ready
A production ready Laravel server means eight things are tuned before your first visitor: PHP-FPM sized to the server's memory, OPcache enabled with enough memory and timestamp validation turned off, config, route, and view caching baked in through php artisan config:cache, route:cache, and view:cache, queue workers supervised instead of run by hand, a single cron entry driving the Laravel scheduler every minute, tight file permissions that grant writes to only two directories, a security layer that locks APP_DEBUG and forces HTTPS, and monitoring that warns you before your customer does.
The difference is not cosmetic. A Laravel app without OPcache and without config caching can spend an extra 40 to 60 milliseconds per request just reading config files and recompiling code, and that alone can double the response time of an average page. With correct tuning, and with a server inside Riyadh delivering roughly 8ms of network latency instead of the 100 to 120ms of a foreign host, the application becomes fast at both the infrastructure and the application layer. This checklist walks you in order from the deepest PHP layer up to the final security lock, and every item is something you can apply in minutes on a cloud VPS.
PHP-FPM: the performance nerve everyone ignores
PHP-FPM is what actually executes your PHP behind Nginx, and how you size its process pool decides how many concurrent requests your server survives before it falls over. The key setting is pm, and its best production value is dynamic or static depending on server size. The practical rule: measure the average memory a PHP process uses on your app, usually between 40 and 80 megabytes for a typical Laravel install, then divide the memory available to PHP by that number to get pm.max_children.
On a 4GB server, leaving 1GB for the database and the operating system, about 3GB remains for PHP. If a process uses 60MB, the sane ceiling is around 50 processes. Going past that pushes the server into swap and collapses performance under load instead of cleanly refusing the extra request.
- pm.max_children: available memory divided by the size of one process.
- pm.start_servers: roughly a quarter of the ceiling so the server is warm at boot.
- pm.min_spare_servers and pm.max_spare_servers: the window of idle processes ready for sudden traffic spikes.
- pm.max_requests: set it to 500 so each process recycles and releases any accumulated memory leaks.
Watch the PHP-FPM log for the warning that max_children has been reached, because seeing it repeatedly means the server is rejecting real requests, and the fix is either adding a memory resource on the cloud server or reducing per process usage.
OPcache: the cheapest speedup you will ever get
OPcache stores compiled PHP bytecode in memory so it is not parsed and compiled on every request, and the effect can cut PHP processing time by more than 50% in file heavy applications like Laravel, which loads hundreds of classes per request. Turning it on is one line, but a wrong configuration throws away half the benefit.
The three settings that matter in production: opcache.memory_consumption starting at 256 megabytes for a large app, opcache.max_accelerated_files raised to 20000 because Laravel and its packages easily exceed several thousand files, and most importantly opcache.validate_timestamps=0 in production only. That last value stops OPcache from checking each file's modification time on every request, buying real performance, but it means OPcache will not see your changes until you reload PHP-FPM or clear the cache manually after each deploy.
The golden rule: in development leave validate_timestamps on, and in production turn it off while making your deploy step reload PHP-FPM. Without that step you will see old code running even though you shipped the new version, one of the most confusing deploy traps there is.
Caching config, routes, and views
Laravel reads dozens of config and route files on every request unless you tell it to compile them into one cached file. These four commands are a mandatory part of every deploy:
- php artisan config:cache: merges all config files into one. After it runs, env() stops working outside config files, so make sure every variable read goes through config() rather than env() directly.
- php artisan route:cache: turns route definitions into a compiled array and noticeably cuts route registration time in apps with hundreds of them.
- php artisan view:cache: precompiles every Blade template instead of compiling it on the first request to each page.
- php artisan event:cache: caches event and listener bindings if you use auto discovery.
Put these commands in one deploy script that runs after every code pull, and always precede them with php artisan config:clear if you changed environment variables. The classic mistake is editing .env on the server and forgetting to rebuild the config cache, so the app keeps reading the old merged values and you burn hours chasing a bug that does not exist in the code.
Queue workers: do not run them by hand
Any slow operation such as sending an email, a WhatsApp notification, or processing an image should go to a queue instead of blocking the request. But a queue worker is a long lived process, and running it by hand with php artisan queue:work in an SSH session means it dies the moment the session closes or the first error hits. The standard answer is Supervisor, which keeps the required number of workers alive at all times and restarts any that fall within seconds.
A typical Supervisor config sets numprocs to the number of concurrent workers, sets autostart and autorestart to true, and directs output to a log file. A critical point: always run queue:work with --max-time=3600 or a request limit, because a worker loads code once into memory, so after a new deploy it will not see your changes until you run php artisan queue:restart, which tells every worker to stop gracefully after finishing its current job so Supervisor can bring them back on the new code.
Match the worker count to the nature of the jobs. Jobs bound to a slow external network benefit from a higher count, while CPU bound jobs can choke the server if they exceed the available cores.
Cron: one heartbeat runs everything
The Laravel scheduler does not need dozens of cron entries, only one that runs schedule:run every minute, after which Laravel decides internally which task is due. The standard entry sends output to null and runs under the application user rather than root, a security point beginners often get wrong.
Make sure the PHP path in the cron entry is the same PHP version that serves the app, because a version mismatch between cron and web requests is one of the most common sources of silent errors. Verify the scheduler actually runs with php artisan schedule:list, which shows every task and its next run time, and if a task you expected does not appear, the problem is in its definition rather than in cron.
Permissions and file ownership: least privilege always
The core security rule is that Laravel code needs write access to exactly two directories: storage and bootstrap/cache. Everything else should be read and execute only for the web server user. This restriction alone sharply limits an attacker's ability to plant executable code even if they find a file upload flaw.
- File ownership belongs to the deploy user and the web server group together.
- Directories at 755 and files at 644 as a general rule.
- storage and bootstrap/cache at 775 with group ownership assigned to the web server.
- The .env file at 600 so only its owner reads it, since it holds database passwords and payment keys.
Never reach for 777 as a quick fix for a permissions problem. That mode makes the file writable by any user on the system and turns a small flaw into a full compromise. If you hit a write error, the correct fix is correcting ownership and group, not throwing the permissions wide open.
Security and the final launch checklist
Before routing a single visitor to the server, review these non negotiable security items:
- APP_DEBUG=false and APP_ENV=production. Leaving debug on exposes your file paths, environment variables, and sometimes database credentials to any visitor on the first error.
- Mandatory HTTPS with a valid SSL certificate and every HTTP request redirected to HTTPS, using free Let's Encrypt automatic provisioning.
- A unique app key from php artisan key:generate, the basis for encrypting sessions and cookies.
- Scoped database permissions: the app user has rights on its own database only, not administrative rights on the server.
- A firewall that closes every port except 80, 443, and 22, with 22 restricted to known admin addresses.
- Regular security updates for the operating system and PHP, plus a composer audit review before every deploy to catch any package with a known vulnerability.
Finally, no production server exists without monitoring and backups. Watch memory, CPU, and queue length, and keep backups daily at minimum. On Saudi cloud infrastructure the backup comes bundled with the plans, and it is your last line of defense when everything above fails. Gather these eight items into one deploy script and run it every single time, because deploy discipline is what turns an app that works on your machine into a service your customers can rely on.


