{"id":1598,"date":"2025-11-09T21:29:23","date_gmt":"2025-11-09T18:29:23","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/one-server-many-phps-how-i-run-per%e2%80%91site-nginx-php%e2%80%91fpm-pools-without-the-drama\/"},"modified":"2025-11-09T21:29:23","modified_gmt":"2025-11-09T18:29:23","slug":"one-server-many-phps-how-i-run-per%e2%80%91site-nginx-php%e2%80%91fpm-pools-without-the-drama","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/one-server-many-phps-how-i-run-per%e2%80%91site-nginx-php%e2%80%91fpm-pools-without-the-drama\/","title":{"rendered":"One Server, Many PHPs: How I Run Per\u2011Site Nginx + PHP\u2011FPM Pools Without the Drama"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, staring at a server with a mix of WordPress, a legacy Laravel 5 app, and a shiny new Symfony project that really wanted PHP 8.2. Sound familiar? One client needed an old extension that wouldn\u2019t compile beyond 7.4, another was excited about JIT on 8.x, and the third just wanted to move fast without breaking anything. That\u2019s the moment I realized you don\u2019t have to choose. You can run multiple PHP versions on one server\u2014cleanly, calmly, and with a plan\u2014for each site. And once you see the rhythm of Nginx handing off requests to dedicated PHP\u2011FPM pools, it starts to feel almost\u2026 relaxing.<\/p>\n<p>Ever had that moment when upgrading PHP broke a plugin you didn\u2019t even know was still there? Or when you wanted to test PHP 8.3 on staging without touching production? That\u2019s exactly what we\u2019re going to untangle together. In this guide, I\u2019ll walk you through the mental model, the file layout, the Nginx blocks, and the PHP\u2011FPM pool configs I use day\u2011to\u2011day. We\u2019ll talk about how to give each site its own PHP home, how to tune those worker pools, and how to switch versions for a site without downtime. If you\u2019ve ever wanted your server to feel like a neat little neighborhood instead of a spaghetti bowl, you\u2019re in the right place.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_Bother_With_Multiple_PHP_Versions\"><span class=\"toc_number toc_depth_1\">1<\/span> Why Bother With Multiple PHP Versions?<\/a><\/li><li><a href=\"#The_Mental_Model_Nginx_as_Traffic_Cop_PHPFPM_Pools_as_Kitchens\"><span class=\"toc_number toc_depth_1\">2<\/span> The Mental Model: Nginx as Traffic Cop, PHP\u2011FPM Pools as Kitchens<\/a><\/li><li><a href=\"#Installing_Multiple_PHP_Versions_and_the_Simple_Layout_That_Keeps_Me_Sane\"><span class=\"toc_number toc_depth_1\">3<\/span> Installing Multiple PHP Versions (and the Simple Layout That Keeps Me Sane)<\/a><ul><li><a href=\"#Where_files_live\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Where files live<\/a><\/li><li><a href=\"#System_users_for_each_site\"><span class=\"toc_number toc_depth_2\">3.2<\/span> System users for each site<\/a><\/li><li><a href=\"#Installing_multiple_PHPs\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Installing multiple PHPs<\/a><\/li><\/ul><\/li><li><a href=\"#PerSite_PHPFPM_Pools_The_Heart_of_the_Setup\"><span class=\"toc_number toc_depth_1\">4<\/span> Per\u2011Site PHP\u2011FPM Pools: The Heart of the Setup<\/a><ul><li><a href=\"#Example_site1_on_PHP_82\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Example: site1 on PHP 8.2<\/a><\/li><li><a href=\"#Example_legacy_site_on_PHP_74\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Example: legacy site on PHP 7.4<\/a><\/li><li><a href=\"#A_quick_note_about_OpCache\"><span class=\"toc_number toc_depth_2\">4.3<\/span> A quick note about OpCache<\/a><\/li><\/ul><\/li><li><a href=\"#Nginx_Server_Blocks_Point_Each_Site_to_Its_Pool\"><span class=\"toc_number toc_depth_1\">5<\/span> Nginx Server Blocks: Point Each Site to Its Pool<\/a><ul><li><a href=\"#site1_on_PHP_82\"><span class=\"toc_number toc_depth_2\">5.1<\/span> site1 on PHP 8.2<\/a><\/li><li><a href=\"#legacy_site_on_PHP_74\"><span class=\"toc_number toc_depth_2\">5.2<\/span> legacy site on PHP 7.4<\/a><\/li><\/ul><\/li><li><a href=\"#Test_Verify_and_Avoid_the_Classic_Traps\"><span class=\"toc_number toc_depth_1\">6<\/span> Test, Verify, and Avoid the Classic Traps<\/a><ul><li><a href=\"#Quick_smoke_tests\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Quick smoke tests<\/a><\/li><li><a href=\"#Common_pitfalls_I_still_see\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Common pitfalls I still see<\/a><\/li><\/ul><\/li><li><a href=\"#Tuning_the_Pools_Calm_Performance_Without_Guesswork\"><span class=\"toc_number toc_depth_1\">7<\/span> Tuning the Pools: Calm Performance Without Guesswork<\/a><\/li><li><a href=\"#Security_Basics_That_Age_Well\"><span class=\"toc_number toc_depth_1\">8<\/span> Security Basics That Age Well<\/a><\/li><li><a href=\"#Upgrades_Switchovers_and_Rollbacks_Without_Panic\"><span class=\"toc_number toc_depth_1\">9<\/span> Upgrades, Switchovers, and Rollbacks Without Panic<\/a><\/li><li><a href=\"#Little_QualityofLife_Improvements\"><span class=\"toc_number toc_depth_1\">10<\/span> Little Quality\u2011of\u2011Life Improvements<\/a><\/li><li><a href=\"#Docs_I_Keep_Handy_and_Why\"><span class=\"toc_number toc_depth_1\">11<\/span> Docs I Keep Handy (and Why)<\/a><\/li><li><a href=\"#Putting_It_All_Together_A_Short_Walkthrough\"><span class=\"toc_number toc_depth_1\">12<\/span> Putting It All Together: A Short Walkthrough<\/a><\/li><li><a href=\"#WrapUp_Calm_Servers_Happy_Migrations\"><span class=\"toc_number toc_depth_1\">13<\/span> Wrap\u2011Up: Calm Servers, Happy Migrations<\/a><\/li><\/ul><\/div>\n<h2 id='section-1'><span id=\"Why_Bother_With_Multiple_PHP_Versions\">Why Bother With Multiple PHP Versions?<\/span><\/h2>\n<p>Let\u2019s start with the truth: most servers don\u2019t run a single perfect app that updates on command. Real servers host a mix of projects in different stages of life. One might be stuck on an old framework because the budget for refactoring hasn\u2019t arrived yet. Another is green\u2011field and eager to use the latest language features. I remember a migration where a client wanted to ride the performance gains of PHP 8 but couldn\u2019t abandon a custom ionCube loader in the old backend. We didn\u2019t tear anything down. We just placed each site in its own lane.<\/p>\n<p>Here\u2019s the thing: PHP-FPM is happy to run different versions side\u2011by\u2011side. You can have php7.4-fpm humming along for one pool, php8.2-fpm powering another, and even a third one for staging. Nginx doesn\u2019t care which version parses the request; it simply passes .php traffic to the socket you tell it to. That separation gives you breathing room. You can test upgrades per site, phase changes gradually, and avoid those scary full\u2011stack rollbacks at 2 a.m. If something goes sideways, you repoint that one site back to its old pool and recover in seconds.<\/p>\n<p>The best part? You also gain better security isolation. Each site runs as its own system user, with its own PHP\u2011FPM pool, and you can restrict file permissions accordingly. A noisy neighbor can\u2019t chew up the entire server because their pool has its own process manager settings and memory limits. And when you\u2019re ready to retire a PHP version, you reduce the blast radius\u2014migrate one site at a time.<\/p>\n<h2 id='section-2'><span id=\"The_Mental_Model_Nginx_as_Traffic_Cop_PHPFPM_Pools_as_Kitchens\">The Mental Model: Nginx as Traffic Cop, PHP\u2011FPM Pools as Kitchens<\/span><\/h2>\n<p>Think of Nginx as a friendly traffic cop at an intersection. HTML, CSS, and images get waved through directly from disk. But when a .php request comes in, Nginx takes a peek at the domain and routes that request to the right kitchen. That kitchen is a PHP\u2011FPM pool. Each pool is staffed (the number of workers), has a pantry (its INI settings and extensions), and listens at its own private door (a Unix socket).<\/p>\n<p>Multiple kitchens, multiple menus. You can have php7.4-fpm and php8.2-fpm both installed on the same server. Inside each PHP version, you create named pools\u2014one per site. Those pools are where you set the Unix user and group the code runs as, define how many PHP workers to keep around, and specify where logs go. Nginx connects each site\u2019s .php location to the correct pool socket, and that\u2019s it. Suddenly, your server isn\u2019t one giant pot\u2014it\u2019s a tidy row of stoves.<\/p>\n<p>I like this model because it maps to daily life. Want to upgrade one app from 7.4 to 8.2? Open the Nginx config, change the socket from the 7.4 pool to the 8.2 pool, reload Nginx, and test. If it sings, you\u2019re done. If it squeaks, point it back and debug without panic. You\u2019ve got time and options.<\/p>\n<h2 id='section-3'><span id=\"Installing_Multiple_PHP_Versions_and_the_Simple_Layout_That_Keeps_Me_Sane\">Installing Multiple PHP Versions (and the Simple Layout That Keeps Me Sane)<\/span><\/h2>\n<h3><span id=\"Where_files_live\">Where files live<\/span><\/h3>\n<p>Over the years I settled into a simple, predictable structure. Each site gets its own system user and directory:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/var\/www\/site1\/current\/public\n\/var\/www\/site2\/current\/public\n<\/code><\/pre>\n<p>I deploy code to a versioned release directory and symlink <strong>current<\/strong> to it. Nginx points at <strong>current\/public<\/strong>. That way I can do zero\u2011downtime releases by swapping the symlink. But even if you\u2019re not doing fancy deploys, this layout keeps your paths clean and makes rolling back easy.<\/p>\n<h3><span id=\"System_users_for_each_site\">System users for each site<\/span><\/h3>\n<p>Create a dedicated Unix user per site. It\u2019s a small step that unlocks better isolation.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo adduser --system --group --home \/var\/www\/site1 site1\nsudo adduser --system --group --home \/var\/www\/site2 site2\nsudo mkdir -p \/var\/www\/site1\/current\/public\nsudo mkdir -p \/var\/www\/site2\/current\/public\nsudo chown -R site1:site1 \/var\/www\/site1\nsudo chown -R site2:site2 \/var\/www\/site2\n<\/code><\/pre>\n<h3><span id=\"Installing_multiple_PHPs\">Installing multiple PHPs<\/span><\/h3>\n<p>On Debian\/Ubuntu, the built\u2011in repos usually include one current PHP. For older or multiple versions, add Ond\u0159ej Sur\u00fd\u2019s PHP repo. It\u2019s the backbone of many multi\u2011PHP setups on Debian\/Ubuntu. I won\u2019t paste the entire repository configuration here\u2014follow <a href=\"https:\/\/packages.sury.org\/php\/README.txt\" rel=\"nofollow noopener\" target=\"_blank\">Ond\u0159ej Sur\u00fd&#8217;s PHP repository instructions for Debian\/Ubuntu<\/a> for the exact steps\u2014then install what you need:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo apt update\nsudo apt install -y nginx php7.4-fpm php8.2-fpm\nsudo systemctl enable --now php7.4-fpm php8.2-fpm nginx\n<\/code><\/pre>\n<p>On RHEL\/CentOS\/Alma\/Rocky, you\u2019ll typically lean on Remi\u2019s repository to install multiple PHP streams. The idea is the same: install separate PHP\u2011FPM packages per version. Regardless of distro, verify that each PHP\u2011FPM service runs and exposes its own global pool directory, usually at something like <strong>\/etc\/php\/7.4\/fpm\/pool.d<\/strong> and <strong>\/etc\/php\/8.2\/fpm\/pool.d<\/strong>.<\/p>\n<p>One more thing: remember that the CLI version (<code>php -v<\/code>) doesn\u2019t decide what FPM runs. Your FPM services are separate. You can have the CLI on 8.2 while some sites run through 7.4\u2011FPM, and that\u2019s perfectly fine.<\/p>\n<h2 id='section-4'><span id=\"PerSite_PHPFPM_Pools_The_Heart_of_the_Setup\">Per\u2011Site PHP\u2011FPM Pools: The Heart of the Setup<\/span><\/h2>\n<p>This is where the magic happens. We create a pool per site, and we run that pool as the site\u2019s Unix user. Each pool listens on its own Unix socket with permissions that allow Nginx to connect. You get isolation, flexible tuning, and clear logs.<\/p>\n<h3><span id=\"Example_site1_on_PHP_82\">Example: site1 on PHP 8.2<\/span><\/h3>\n<p>Create <strong>\/etc\/php\/8.2\/fpm\/pool.d\/site1.conf<\/strong>:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[site1]\nuser = site1\ngroup = site1\n\n; The socket unique to this site and version\nlisten = \/run\/php\/php8.2-fpm-site1.sock\nlisten.owner = www-data\nlisten.group = www-data\nlisten.mode = 0660\n\n; Process manager tuning\npm = dynamic\npm.max_children = 12\npm.start_servers = 4\npm.min_spare_servers = 2\npm.max_spare_servers = 6\npm.max_requests = 500\n\n; Timeouts and stability\nrequest_terminate_timeout = 60s\nrequest_slowlog_timeout = 3s\nemergency_restart_threshold = 10\nemergency_restart_interval = 1m\n\n; Logs\nslowlog = \/var\/log\/php8.2-fpm\/site1-slow.log\nphp_admin_value[error_log] = \/var\/log\/php8.2-fpm\/site1-error.log\nphp_admin_flag[log_errors] = on\ncatch_workers_output = yes\n\n; Security\nsecurity.limit_extensions = .php .php8\n\n; Env and working dir\nenv[APP_ENV] = production\nchdir = \/var\/www\/site1\/current\n<\/code><\/pre>\n<p>Make sure the log directories exist and are writable by the FPM master (often root creates them, and the FPM service writes into them):<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo mkdir -p \/var\/log\/php8.2-fpm\nsudo touch \/var\/log\/php8.2-fpm\/site1-error.log \/var\/log\/php8.2-fpm\/site1-slow.log\nsudo systemctl reload php8.2-fpm\n<\/code><\/pre>\n<h3><span id=\"Example_legacy_site_on_PHP_74\">Example: legacy site on PHP 7.4<\/span><\/h3>\n<p>Create <strong>\/etc\/php\/7.4\/fpm\/pool.d\/legacy.conf<\/strong>:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[legacy]\nuser = site2\ngroup = site2\n\nlisten = \/run\/php\/php7.4-fpm-legacy.sock\nlisten.owner = www-data\nlisten.group = www-data\nlisten.mode = 0660\n\npm = dynamic\npm.max_children = 8\npm.start_servers = 3\npm.min_spare_servers = 2\npm.max_spare_servers = 4\npm.max_requests = 400\n\nrequest_terminate_timeout = 60s\nrequest_slowlog_timeout = 5s\nslowlog = \/var\/log\/php7.4-fpm\/legacy-slow.log\nphp_admin_value[error_log] = \/var\/log\/php7.4-fpm\/legacy-error.log\nphp_admin_flag[log_errors] = on\n\nsecurity.limit_extensions = .php .php7\nchdir = \/var\/www\/site2\/current\n<\/code><\/pre>\n<p>Create log directories and reload:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo mkdir -p \/var\/log\/php7.4-fpm\nsudo touch \/var\/log\/php7.4-fpm\/legacy-error.log \/var\/log\/php7.4-fpm\/legacy-slow.log\nsudo systemctl reload php7.4-fpm\n<\/code><\/pre>\n<h3><span id=\"A_quick_note_about_OpCache\">A quick note about OpCache<\/span><\/h3>\n<p>OpCache lives inside each PHP\u2011FPM service. That means all pools within the same FPM version share the same OpCache memory. If you need completely separate OpCache configurations, you run separate FPM services (which you already do per version). Many OpCache directives are system\u2011level and best set in the global <strong>php.ini<\/strong> for that version. Per\u2011pool tweaks that are allowed by the SAPI can go into <strong>php_admin_value<\/strong> lines, but I prefer to keep OpCache sizing consistent per PHP version to avoid surprises.<\/p>\n<h2 id='section-5'><span id=\"Nginx_Server_Blocks_Point_Each_Site_to_Its_Pool\">Nginx Server Blocks: Point Each Site to Its Pool<\/span><\/h2>\n<p>We\u2019ll set up one server block per site. Each block serves static files directly and forwards only .php requests to the correct PHP\u2011FPM socket. Keep the config boring and predictable\u2014future you will thank you.<\/p>\n<h3><span id=\"site1_on_PHP_82\">site1 on PHP 8.2<\/span><\/h3>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">server {\n    listen 80;\n    server_name site1.example.com;\n    root \/var\/www\/site1\/current\/public;\n\n    access_log \/var\/log\/nginx\/site1.access.log;\n    error_log  \/var\/log\/nginx\/site1.error.log warn;\n\n    index index.php index.html;\n\n    # Serve static quickly\n    location \/ {\n        try_files $uri $uri\/ \/index.php?$query_string;\n    }\n\n    # Deny PHP execution in uploads\n    location ~* \/(?:uploads|files|media)\/.*.php$ {\n        deny all;\n    }\n\n    # PHP handler\n    location ~ .php$ {\n        try_files $uri =404;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n        fastcgi_param DOCUMENT_ROOT $realpath_root;\n        fastcgi_pass unix:\/run\/php\/php8.2-fpm-site1.sock;\n        fastcgi_buffers 16 16k;\n        fastcgi_buffer_size 32k;\n        fastcgi_read_timeout 60s;\n    }\n\n    client_max_body_size 64m;\n}\n<\/code><\/pre>\n<h3><span id=\"legacy_site_on_PHP_74\">legacy site on PHP 7.4<\/span><\/h3>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">server {\n    listen 80;\n    server_name legacy.example.com;\n    root \/var\/www\/site2\/current\/public;\n\n    access_log \/var\/log\/nginx\/legacy.access.log;\n    error_log  \/var\/log\/nginx\/legacy.error.log warn;\n\n    index index.php index.html;\n\n    location \/ {\n        try_files $uri $uri\/ \/index.php?$query_string;\n    }\n\n    location ~* \/(?:uploads|files|media)\/.*.php$ {\n        deny all;\n    }\n\n    location ~ .php$ {\n        try_files $uri =404;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n        fastcgi_param DOCUMENT_ROOT $realpath_root;\n        fastcgi_pass unix:\/run\/php\/php7.4-fpm-legacy.sock;\n        fastcgi_read_timeout 60s;\n    }\n\n    client_max_body_size 32m;\n}\n<\/code><\/pre>\n<p>Two details save me headaches. First, I always use <strong>$realpath_root<\/strong> so that if a symlink points to a new release directory, PHP sees the resolved path. Second, I scope Nginx logs per site. When things misbehave, site\u2011specific logs cut the noise. When you want a full picture, ship Nginx and PHP\u2011FPM logs to centralized storage\u2014if you\u2019re curious, here\u2019s how I handle <a href=\"https:\/\/www.dchost.com\/blog\/en\/merkezi-loglama-ve-gozlemlenebilirlik-vpste-loki-promtail-grafana-ile-sakin-kalan-bir-zihin\/\">centralised logging with Loki, Promtail and Grafana<\/a>.<\/p>\n<p>If you prefer to keep configs DRY, you can include a common snippet for the PHP location and only change the <strong>fastcgi_pass<\/strong> socket per site. Just don\u2019t over\u2011abstract to the point of confusion. Clarity wins at 3 a.m.<\/p>\n<h2 id='section-6'><span id=\"Test_Verify_and_Avoid_the_Classic_Traps\">Test, Verify, and Avoid the Classic Traps<\/span><\/h2>\n<h3><span id=\"Quick_smoke_tests\">Quick smoke tests<\/span><\/h3>\n<p>Create a simple PHP info file per site, hit it once, then remove it:<\/p>\n<pre class=\"language-php line-numbers\"><code class=\"language-php\">echo '&lt;?php phpinfo();' | sudo -u site1 tee \/var\/www\/site1\/current\/public\/info.php\ncurl -s http:\/\/site1.example.com\/info.php | head -n 5\nrm \/var\/www\/site1\/current\/public\/info.php\n<\/code><\/pre>\n<p>Make sure the page shows the expected PHP version. If you see a 502 Bad Gateway, check socket paths, permissions, and whether the FPM service is running. If you see a 404, you might not have deployed an <strong>index.php<\/strong> yet or <strong>try_files<\/strong> isn\u2019t falling back to it correctly.<\/p>\n<h3><span id=\"Common_pitfalls_I_still_see\">Common pitfalls I still see<\/span><\/h3>\n<p>One, the FPM socket permissions don\u2019t let Nginx connect. Nginx usually runs as <strong>www-data<\/strong> or <strong>nginx<\/strong>, so set <strong>listen.owner<\/strong> and <strong>listen.group<\/strong> appropriately and use mode 0660. Two, the pool user doesn\u2019t match the site\u2019s owner, so file writes fail. Keep <strong>user<\/strong>\/<strong>group<\/strong> aligned with the site owner. Three, <strong>pm.max_children<\/strong> is too low or too high. Too low starves busy sites; too high chews through RAM. Start conservative, then tune with data. Four, someone confuses the CLI version with FPM. They\u2019re separate worlds.<\/p>\n<p>I also like to have a status endpoint per pool while I\u2019m tuning. You can add <strong>pm.status_path = \/fpm-status<\/strong> in the pool config and then expose it behind an IP allowlist in Nginx. It shows active processes and queue length. Just don\u2019t leave it open to the world.<\/p>\n<h2 id='section-7'><span id=\"Tuning_the_Pools_Calm_Performance_Without_Guesswork\">Tuning the Pools: Calm Performance Without Guesswork<\/span><\/h2>\n<p>The biggest lever is the process manager. I lean on <strong>pm = dynamic<\/strong> for most websites, because it keeps a warm set of workers ready under load and shrinks during quiet hours. If your server hosts many low\u2011traffic sites, <strong>pm = ondemand<\/strong> can be frugal, spinning workers up only when needed. The catch is that first requests might feel colder. For heavy, steady traffic, <strong>pm = static<\/strong> can reduce latency jitter at the cost of memory.<\/p>\n<p>Speaking of memory: each PHP worker consumes RAM. How much depends on the app and loaded extensions. I usually estimate with a small load test and observe the RSS of worker processes. From there, I cap <strong>pm.max_children<\/strong> so total potential usage per pool won\u2019t drown the server. Remember, multiple pools and versions all live on the same box. Leave room for Nginx, databases, and caches.<\/p>\n<p>Set <strong>pm.max_requests<\/strong> to recycle workers occasionally. It nudges away slow memory leaks in userland code or extensions. Values between 300 and 1000 are common for me, depending on traffic patterns. Timeouts help too: <strong>request_terminate_timeout<\/strong> keeps zombie requests from hanging forever, and <strong>request_slowlog_timeout<\/strong> is perfect for catching code paths that suddenly take ages. The slow log will become your new favorite detective tool.<\/p>\n<p>For insight, the FPM status page is gold. Watch queue length\u2014if requests pile up, you might need more children or faster code. Also watch CPU saturation and IO. Sometimes what looks like a PHP problem is actually slow disk or a chatty database. That\u2019s when I\u2019m glad I can jump into centralized metrics and logs instead of guessing.<\/p>\n<h2 id='section-8'><span id=\"Security_Basics_That_Age_Well\">Security Basics That Age Well<\/span><\/h2>\n<p>Even a simple setup benefits from a few habits. First, run each pool as the site\u2019s own Unix user, and make the code directories owned by that user. Nginx should only need read access, and only the pool for that site should have write access where uploads land. A common approach is to set directory permissions to 750 and files to 640, with group access tuned to your web user if needed.<\/p>\n<p>Second, keep sockets private. Placing them under <strong>\/run\/php\/<\/strong> with mode 0660 and group <strong>www-data<\/strong> is a sensible default. If your distro uses <strong>nginx<\/strong> as the worker user, adjust groups accordingly. Third, give each site separate logs. When someone uploads a massive video and runs the disk near full, you\u2019ll know which site to call first and you won\u2019t drown in mixed output. If you haven\u2019t set up an observability stack yet, now\u2019s a good time to think about it\u2014shipping logs off box makes troubleshooting faster when you need it most.<\/p>\n<p>Finally, don\u2019t rely on <strong>open_basedir<\/strong> to save the day. Clean file permissions and process isolation are sturdier. If you need stronger boundaries, containers or VMs per project are worth considering, but for many teams, per\u2011site FPM pools with sane permissions hit the sweet spot of simplicity and safety.<\/p>\n<h2 id='section-9'><span id=\"Upgrades_Switchovers_and_Rollbacks_Without_Panic\">Upgrades, Switchovers, and Rollbacks Without Panic<\/span><\/h2>\n<p>Here\u2019s the smooth way to upgrade a site from PHP 7.4 to 8.2 without breaking a sweat. Keep both FPM versions installed. Create a new pool on 8.2 mirroring the old pool\u2019s settings. Make sure extensions and INI tweaks match what the app expects. Then duplicate the Nginx server block into a temporary test host (or use <strong>hosts<\/strong> file mapping on your laptop) and point that copy at the new 8.2 socket. Hit your critical pages, run a quick smoke test, check logs, and watch the FPM status. If it looks healthy, flip the production block\u2019s <strong>fastcgi_pass<\/strong> to the 8.2 socket and reload Nginx.<\/p>\n<p>If something misbehaves? Switch <strong>fastcgi_pass<\/strong> back to the 7.4 socket and reload. You\u2019ve rolled back in seconds, with no package downgrades or server restarts. That\u2019s the beauty of per\u2011site sockets. When I first adopted this approach, the stress of PHP upgrades dropped like a stone. I could schedule careful tests during the day instead of crossing my fingers at midnight.<\/p>\n<p>One thing to remember: after deploying new code, you may want to clear OpCache for that site\u2019s version so new files load immediately. A quick FPM reload for that version does the trick and is typically safe in production\u2014workers restart gracefully and pick up the new code.<\/p>\n<h2 id='section-10'><span id=\"Little_QualityofLife_Improvements\">Little Quality\u2011of\u2011Life Improvements<\/span><\/h2>\n<p>Over time, small touches make the stack pleasant to live with. I like to set a distinct <strong>pm.status_path<\/strong> per pool and hide it behind an allowlist so I can peek at live activity. I add a ping path (<strong>ping.path<\/strong>, <strong>ping.response<\/strong>) in case I need a simple health check that doesn\u2019t hit the app. In Nginx, I drop in a rule to deny PHP execution in the uploads directory\u2014it\u2019s a cheap, effective guardrail. And for performance, a couple of FastCGI buffer lines stop big responses from thrashing.<\/p>\n<p>For Laravel and Symfony, ensure the worker user can write to <strong>storage<\/strong> or <strong>var<\/strong> directories. For WordPress, watch the <strong>wp-content\/uploads<\/strong> permissions. If you\u2019re using Composer, run it as the site user so vendor files aren\u2019t owned by root. It\u2019s the little things that keep deploys calm. When your process is boring, you\u2019re doing it right.<\/p>\n<h2 id='section-11'><span id=\"Docs_I_Keep_Handy_and_Why\">Docs I Keep Handy (and Why)<\/span><\/h2>\n<p>Whenever I forget a directive name or want to double\u2011check behavior, I lean on the official docs. The <a href=\"https:\/\/www.php.net\/manual\/en\/install.fpm.configuration.php\" rel=\"nofollow noopener\" target=\"_blank\">the PHP\u2011FPM pool configuration directives<\/a> page is where I confirm what can go into <strong>php_admin_value<\/strong> and how timeouts behave. For Nginx, the <a href=\"https:\/\/nginx.org\/en\/docs\/http\/ngx_http_fastcgi_module.html\" rel=\"nofollow noopener\" target=\"_blank\">Nginx FastCGI parameters<\/a> reference is my go\u2011to whenever I need to tweak buffering or pass extra params to PHP. And for Debian\/Ubuntu, I bookmark <a href=\"https:\/\/packages.sury.org\/php\/README.txt\" rel=\"nofollow noopener\" target=\"_blank\">Ond\u0159ej Sur\u00fd\u2019s PHP repository instructions<\/a> to refresh how to add or pin versions cleanly. It saves a lot of tab\u2011chaos in the heat of the moment.<\/p>\n<h2 id='section-12'><span id=\"Putting_It_All_Together_A_Short_Walkthrough\">Putting It All Together: A Short Walkthrough<\/span><\/h2>\n<p>Let\u2019s do a quick lap to make it real. Imagine you\u2019re adding a new app on PHP 8.2 while keeping an old site on 7.4. You create the <strong>site1<\/strong> user and directories, install <strong>php8.2-fpm<\/strong>, and write the <strong>site1.conf<\/strong> pool that runs as the site user and listens on a unique socket. You point an Nginx server block at <strong>\/var\/www\/site1\/current\/public<\/strong>, give it a clean PHP location that sends requests to <strong>\/run\/php\/php8.2-fpm-site1.sock<\/strong>, and restart services. You deploy code, run a phpinfo to confirm the version, then remove it. You visit the home page. It\u2019s fast. Logs are clean.<\/p>\n<p>Next, you clone that process for <strong>legacy<\/strong> on 7.4. Different user, different pool, different socket. Nginx server block points to the legacy socket. You repeat the smoke test. Now both sites run on the same server, with their own PHP versions and their own lanes. You didn\u2019t make a complicated cluster or container mesh. You just used what PHP and Nginx already offer\u2014properly.<\/p>\n<p>When it\u2019s time to upgrade the legacy site, you prepare a new 8.2 pool and a test Nginx block, shake it down, and switch the socket in production when you\u2019re ready. If a plugin chokes, you revert with a single line and try again tomorrow. Zero midnight drama, zero panicked rollbacks.<\/p>\n<h2 id='section-13'><span id=\"WrapUp_Calm_Servers_Happy_Migrations\">Wrap\u2011Up: Calm Servers, Happy Migrations<\/span><\/h2>\n<p>I love this pattern because it lowers the stakes. Running multiple PHP versions on one server with per\u2011site Nginx + PHP\u2011FPM pools turns scary platform upgrades into tiny, reversible steps. Each site lives in its own space. You can dial in worker counts, control timeouts, and collect logs per app. You can test a new PHP on a single domain without touching the rest. And when you finally retire an old version, you\u2019ll feel it in your shoulders\u2014less weight.<\/p>\n<p>If you\u2019re starting from scratch, begin with one site and make it spotless. Create the system user. Write the FPM pool by hand. Point Nginx at that pool. Add the deny rule for uploads. Tail the logs while you click around. Then repeat for the next site, and the next. Keep a short checklist near your terminal, and don\u2019t be shy about adding small quality\u2011of\u2011life improvements over time. Your future self will send you a thank\u2011you note.<\/p>\n<p>Hope this was helpful! If you have questions or want me to look over a config, drop me a line. I\u2019ll be around\u2014with a warm coffee and a soft spot for tidy server neighborhoods.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, staring at a server with a mix of WordPress, a legacy Laravel 5 app, and a shiny new Symfony project that really wanted PHP 8.2. Sound familiar? One client needed an old extension that wouldn\u2019t compile beyond 7.4, another was excited about JIT on 8.x, and the third just wanted to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1599,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1598","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1598","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1598"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1598\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1599"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1598"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1598"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1598"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}