Teknoloji

Ofiste Bir Sabah: PHP Yükseltmesi, Ter Damlaları ve Küçük Bir Aydınlanma

{
“title”: “Tek Sunucuda Birden Fazla PHP Sürümü Nasıl Barış İçinde Yaşar? Nginx + PHP‑FPM Havuzlarıyla Site Bazlı Kurulum”,
“content”: “

Hiç başınıza geldi mi? Bir müşterinin sitesi PHP 8.2 istiyor, diğeri sakin sakin 7.4’te takılıyor. Birini güncelliyorsunuz, öteki pat diye beyaz ekran. Benzer bir sabahı, kahvemi bile bitiremeden yaşadım. Ekranın bir köşesinde hata logu, diğer köşesinde Slack. O an anladım ki tek sunucuda tek sürüm, bazı projeler için biraz fazla romantik bir hayal.

İşte o gün Nginx ve PHP‑FPM havuzlarının sihriyle tanıştım. Her siteye kendi PHP sürümünü, kendi havuzunu, kendi kurallarını verince ortalık bir anda düzene girdi. Ne tek sunucu kullanmaktan vazgeçtim, ne de eski uygulamalara fazlaca müdahale etmek zorunda kaldım. Bu yazıda, aynı sunucuda birden fazla PHP sürümünü, Nginx ve PHP‑FPM havuzlarıyla nasıl yan yana, kavgasız döndüreceğimizi akıcı bir dille anlatacağım. Hem adım adım kurulumdan bahsedeceğiz, hem de pratik ipuçları ve çarpıcı hatalar üzerinden geçeceğiz. Sonunda, siz de “Ben bunu neden daha önce yapmadım?” diyebilirsiniz.

Neden Tek Sunucuda Birden Fazla PHP Sürümüne İhtiyaç Duyarız?

Her projenin nefesi, ritmi, yaşı farklı. Kimi Laravel’le genç ve atak, kimi yılların WordPress’iyle temkinli. Tümünü tek tipleştirince birinin kalbi kırılıyor. Bazı eklentiler güncel PHP’yi severken, bazı miras kodlar aceleye gelemez. Tek bir sunucuda birden fazla sürümü çalıştırabilmek, “hepsini memnun etme” şansını veriyor. Bazen geçiş sürecinde kontrollü ilerlemek istersiniz; staging ortamı 8.2’de, canlı hala 7.4’te kalır. Böylece acele edilmez; hatalar daha yola çıkmadan toplanır.

Masraf kısmını da düşünün. Her sürüm için ayrı sunucu açmak yerine, eldeki kaynakları akıllıca bölmek hem cüzdana hem zihne iyi geliyor. Ayrıca yönetimi tek merkezde tutunca izleme, yedek, güvenlik gibi işler daha derli toplu hale geliyor. Sadece bir koşul var: Her şeyin kimlikli ve net olması. Hangi site, hangi havuz, hangi soket; karışmayacak.

Mantığı Kafada Oturtalım: Nginx İster, PHP‑FPM Yapar

Resmi şöyle düşünün: Nginx önde duran nazik bir garson. Gelen .php isteklerini mutfağa, yani PHP‑FPM’e taşıyor. PHP‑FPM ise arka tarafta, her biri farklı sürümde, farklı kurallara sahip havuzlardan oluşan bir mutfak. Siz hangi sitenin hangi havuza gideceğini Nginx’te seçiyorsunuz. Havuzları da PHP‑FPM’in kendi yapılandırma dosyalarında kuruyorsunuz. Geriye, herkesin kendi tabak ve çatallarıyla sofraya gelmesi kalıyor.

Havuzlar Ne İşe Yarar?

Havuz dediğimiz şey, belirli bir kullanıcıyla, belirli bir klasöre ve belirli sınırlara bağlı çalışan PHP süreçleri topluluğu. Bir siteyi yoracak trafik geldiğinde diğerini etkilemesin diye, havuzlar adeta kum havuzları gibi. İçeride doya doya koşturulur, ama dışarıya taşmaz.

Soket mi, 127.0.0.1:9000 mi?

Yerel sunucuda, aynı makinede Nginx ile PHP‑FPM konuşurken Unix soket kullanmak pratik ve hızlıdır. Sadece dosya izinlerine dikkat edersiniz, olay biter. TCP portu ise ağ sınırları aştığınızda, konteynerler arası ya da başka sunucuya yönlendirirken güzeldir. Bu yazıda sokete yaslanacağız; işler sade kalsın.

Ortama Hazırlık: Paketler, Servisler ve Dosya Düzeni

Önce paketler. Ubuntu/Debian tarafında birden fazla PHP sürümünü yan yana kurmak çok konforlu. Toplulukta sık kullanılan paket deposu üzerinden 7.x ile 8.x’i aynı anda kurabiliyorsunuz. Ben çoğu zaman 7.4, 8.1 ve 8.2 üçlüsüyle çalıştım. Ubuntu’da birden fazla PHP sürümü için Ondřej Surý paket deposu bu işte yıllardır hayat kurtarıyor. RHEL/CentOS ekosisteminde de benzer şekilde alternatif depolarla ilerleniyor; mantık değişmiyor.

Kurulumdan sonra şuna dikkat: Her sürümün kendi PHP‑FPM servisi oluyor. Örneğin php7.4‑fpm, php8.1‑fpm, php8.2‑fpm. Her birinin altında da pool.d klasörü var. İşte site bazlı havuz dosyalarını oraya bırakacağız.

Küçük ama Kritik Not: CLI Sürümü

Sunucuda komut satırında kullandığınız php ile Nginx’in arka tarafta çağırdığı PHP‑FPM sürümü farklı olabilir. Composer ya da cron işleri için bunu bilerek davranın. Komutları tümden ‘php’ yerine ‘php8.1’ gibi tam sürüm adıyla çağırmak, karışıklığı sıfıra indirir.

Site Bazlı PHP‑FPM Havuzları: İki Sitede İki Ayrı Dünya

Varsayalım iki siteniz var: birincisi site1.com ve PHP 8.1 istiyor, ikincisi site2.com ve 7.4’te kalacak. İkisinin de kendi Unix kullanıcısı bulunsun: ‘site1’ ve ‘site2’. Kodlar sırasıyla /var/www/site1/current ve /var/www/site2/current altında. Şimdi iki ayrı havuz kuralım.

PHP 8.1 için Havuz (site1)

; /etc/php/8.1/fpm/pool.d/site1.conf
[site1]
user = site1
group = site1

listen = /run/php/site1-php81.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 20s

; Güvenli sınırlar
php_admin_value[open_basedir] = /var/www/site1/current:/tmp
php_admin_value[upload_tmp_dir] = /tmp
php_admin_value[sys_temp_dir] = /tmp
php_admin_value[error_log] = /var/log/php8.1-fpm/site1-error.log
php_admin_value[log_errors] = On
php_admin_value[memory_limit] = 256M
php_admin_value[post_max_size] = 64M
php_admin_value[upload_max_filesize] = 64M

; Yavaş çalışan istekleri yakalayalım
request_slowlog_timeout = 5s
slowlog = /var/log/php8.1-fpm/site1-slow.log

; Durum sayfası
pm.status_path = /php-fpm-status

PHP 7.4 için Havuz (site2)

; /etc/php/7.4/fpm/pool.d/site2.conf
[site2]
user = site2
group = site2

listen = /run/php/site2-php74.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = ondemand
pm.max_children = 8
pm.process_idle_timeout = 20s

php_admin_value[open_basedir] = /var/www/site2/current:/tmp
php_admin_value[upload_tmp_dir] = /tmp
php_admin_value[sys_temp_dir] = /tmp
php_admin_value[error_log] = /var/log/php7.4-fpm/site2-error.log
php_admin_value[log_errors] = On
php_admin_value[memory_limit] = 192M
php_admin_value[post_max_size] = 32M
php_admin_value[upload_max_filesize] = 32M

request_slowlog_timeout = 5s
slowlog = /var/log/php7.4-fpm/site2-slow.log

pm.status_path = /php-fpm-status

Burada havuzun kullanıcı ve grup olarak site sahibini seçtiğine dikkat edin. Kodlara bu kullanıcının sahip olması iyi bir izolasyon sağlar. ‘listen.owner’ ve ‘listen.group’ ile Nginx’in çalıştığı kullanıcıya (çoğu dağıtımda www-data) soket erişimi veriyoruz. Her sürümün logları da kendi klasörlerinde. Böylece bir performans sorunu olduğunda hangi sitenin midesi ağrıyor hemen görülür.

Nginx Sunucu Blokları: Hangi Site Hangi PHP’ye Gidecek?

Şimdi topu Nginx’e atalım. Temel bir sunucu bloğunu, ilgili havuzun soketine işaret edecek şekilde kuruyoruz. Kalıp mantıklı olursa, yeni site eklemek dakikalara düşer.

site1.com (PHP 8.1 soketine gitsin)

# /etc/nginx/sites-available/site1.conf
server {
    server_name site1.com www.site1.com;
    root /var/www/site1/current/public;
    index index.php index.html;

    access_log /var/log/nginx/site1-access.log;
    error_log  /var/log/nginx/site1-error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/site1-php81.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~* \.(jpg|jpeg|png|gif|svg|webp|css|js|ico)$ {
        expires 7d;
        add_header Cache-Control 'public';
    }

    location = /php-fpm-status {
        allow 127.0.0.1;
        deny all;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/site1-php81.sock;
    }
}

site2.com (PHP 7.4 soketine gitsin)

# /etc/nginx/sites-available/site2.conf
server {
    server_name site2.com www.site2.com;
    root /var/www/site2/current/public;
    index index.php index.html;

    access_log /var/log/nginx/site2-access.log;
    error_log  /var/log/nginx/site2-error.log;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/site2-php74.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~* \.(jpg|jpeg|png|gif|svg|webp|css|js|ico)$ {
        expires 7d;
        add_header Cache-Control 'public';
    }

    location = /php-fpm-status {
        allow 127.0.0.1;
        deny all;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/site2-php74.sock;
    }
}

Bu kadar. Dosyaları etkinleştirip Nginx’i yeniden yüklediniz mi, her site kendi havuzunda pişer. FastCGI kısımlarını detaylı keşfetmek isterseniz, Nginx’in kendi kaynağı iyi bir referans: Nginx fastcgi ayarlarının resmi belgeleri. PHP‑FPM havuz parametrelerinin anlamını da tek tek merak ederseniz, PHP‑FPM havuzu seçeneklerinin resmi dökümü hoş bir kılavuzdur.

Güvenlik ve İzolasyon: Her Site Kendi Kum Havuzunda Oynasın

İşin tadı izolasyonda. Her site için ayrı Unix kullanıcısı, ayrı havuz, ayrı log ve ayrı tmp. Böyle yapınca izin hataları biraz artar, kabul, ama riskler ciddi azalır. Kod dizinleri site sahibi kullanıcıya ait olsun, Nginx sadece okuma iznine sahip kalsın. Soket dosyasında dinleyen taraf PHP‑FPM olduğu için, Nginx’e sadece client tarafı yetiyor; bu yüzden ‘listen.owner’ ve ‘listen.group’ kritik.

Bir de küçük ama etkili dokunuşlar var. ‘open_basedir’ ile sitenin yalnızca kendi klasörüne erişeceğini kronikleştirin. ‘upload_tmp_dir’ ve ‘sys_temp_dir’ değerlerini sabitleyin ki sürpriz dosya izinleri çıkmasın. Hata loglarını ayrıştırın; aynı dosyada herkes bağırırsa kimse duyulmaz. İstek bazında sınırlar da güzel bir emniyet kemeri: ‘request_terminate_timeout’ çok uzun scriptleri sonlandırır, ‘request_slowlog_timeout’ yavaşlayanları ayna karşısına çıkarır.

Güvenliği seviyorsanız, giriş kapılarını koruyan küçük çözümler de hayatı kolaylaştırır. WordPress admin girişlerini kontrol altında tutmak için kullandığım bir yolu şöyle anlatmıştım: VPS’te merkezi loglamayı; Grafana Loki + Promtail rehberimiz ile Nginx ve PHP‑FPM loglarını toplayıp uyarılar kurmak, sorunları erken yakalamayı kolaylaştırır. Bir uyarı ‘502 çoğaldı’ dediğinde, hangi havuzun sarsıldığını saniyeler içinde görürsünüz.

Sık Görülen Hatalar: 502’ler, İzinler ve CLI’da Yanlış Sürüm

İlk kurulumda genelde birkaç klasik hata kapıyı çalar. En meşhuru 502 Bad Gateway. Nedenleri tahmin edilebilir. Nginx yanlış sokete bakıyor olabilir, PHP‑FPM havuzu çalışmıyordur, ya da soket dosyasının izinleri Nginx’e kapalıdır. Çözüm için üçlü kontrol iyi gider: Soket yolunu doğru yazdınız mı, servisin durumu ‘active’ mi, ‘listen.mode’ ve ‘listen.owner’ doğru mu?

Bir diğer sıkıntı ‘Permission denied’. Genellikle kod dizini, havuzun kullanıcı ve grubuna ait değildir veya Nginx’e okuma izni verilmemiştir. Sakin kalın; dizinin sahibini site kullanıcısına verip, Nginx grubuna okuma izni tanımlayın. Gerekirse public dizinine özel izinler atayın.

CLI sürümü ise ayrı bir tuzak. Nginx, örneğin 8.1 havuzunu kullanırken siz terminalde ‘php -v’ dediğinizde 7.4 görebilirsiniz. Bu bir sorun değildir, ama cron veya artisan/composer gibi komutları çalıştırırken gereğini yerine getirin. ‘php8.1 artisan migrate’ ya da cronda ‘/usr/bin/php8.1’ gibi tam yol kullanmak, kafa karışıklıklarını önler.

Son olarak OPcache’yi unutmayın. Farklı sürümlerde farklı ini dosyaları var; her sürümün opcache ayarı kendi klasöründe. Kod yayınından sonra ‘reload’ ettiğinizde OPcache’nin ısınması için birkaç dakika tanımak performans için iyidir.

Performans İpuçları: pm Ayarı, OPcache ve Soketin Sesi

Havuz başına süreç sayısı, sunucunuzu ya uçar ya da yorabilir. Trafiği öngörerek ‘pm.max_children’ değerini gerçekçi tutmak önemli. Ben genelde ‘pm = ondemand’ ile başlar, CPU ve RAM davranışını izleyerek çocuk sayısını törpülerim. Yoğun sitelerde ‘pm = dynamic’ ile bir taban ve tavan belirlemek de işe yarar. Temel mesele, hiçbir havuzun kaynakları tek başına yutmamasıdır.

OPcache canlılığın anahtarı. Production’da opcache’yi açık, bellek değerlerini de uygulamanızın büyüklüğüne göre makul tutun. Laravel ve WordPress gibi çatıların dosya sayısı fazladır; gerçekçi bir ‘opcache.memory_consumption’ sizi rahatlatır. Dosya izleme yenileme aralıklarını çok kısaltmayın; gereksiz invalidation performansı taşır.

Soket veya TCP bahisleri ise pratikte kuruluma bakar. Aynı makinede Unix soket kullanmak bana her zaman temiz ve ölçülebilir göründü. İzinleri oturtunca stabil gider. Yürümede bir pürüz görürseniz, TCP’ye geçmek de sorun çözebilir; önemli olan, değişiklik yaptığınızda ölçüp izlemektir. Bu arada FastCGI katmanının detaylarını kurcalamak isterseniz, resmi kaynak iyi açıklıyor: Nginx fastcgi modülü belgesi.

Sürüm Geçişleri: Kırmadan, Korkmadan, Adım Adım

Diyelim ki site2’yi 7.4’ten 8.1’e alacaksınız. Benim izlediğim yol şuna benziyor. Önce 8.1 tarafında yeni bir havuz açarım, örneğin ‘site2-php81.sock’. Aynı sunucu bloğunda yeni bir testing hostu tanımlarım: test.site2.com gibi. Bu test alan adı doğrudan 8.1 soketine gider. QA ve kabul süreçleri burada yapılır, loglar burada akar, OPcache burada ısınır.

Her şey yolundaysa, canlı domainin Nginx bloğunda tek değişiklik yaparım: ‘fastcgi_pass’ satırını yeni sokete çeviririm. Bu değişikliği ‘nginx -t’ ile test eder, ‘systemctl reload nginx’ ile kesintisiz devreye alırım. Gerekirse iki sunucu bloğunu kısa bir süre birlikte tutar, geçişi geri almak için eski bloğu kommentte bekletirim. Bu küçük ritüel, stres seviyesini ciddi azaltır.

Geçişten sonra gözlem süresi kıymetli. Kullanıcı davranışı, hatalar, yavaş logları. Bir iki saatte değil, bir iki günde içiniz rahat eder. Bu dönemde uyarılarınız varsa hayat kolaylaşır; 5xx artarsa haber ver, ortalama yanıt süresi zıplarsa öt gibi basit ama etkili kurallar zaman kazandırır.

Bonus Notlar: Dizin Düzeni, Sertifikalar ve Küçük Dokunuşlar

Dizin düzeni standardı, gelecekteki siz için yapılmış bir iyiliktir. /var/www/site1/current gibi semantik bir simbiyoz, deployment işini de güzelleştirir. ‘current’ sembolünü yeni sürüme alıp, PHP‑FPM veya Nginx’e reload verdiğinizde tertemiz bir devreye alma yaşarsınız. Log dosyalarını site bazlı ayırın; zamanla bir alt klasöre tarih bazlı döndürmek de iyi gider.

SSL sertifikaları tarafında Let’s Encrypt gibi çözümleri devreye almak kolay. Nginx sunucu bloklarına minimal eklemelerle otomatik yenileme rutini sağlanır. Burada anahtar, sertifikayı yenilerken sunucu bloklarını bozmamaktır; staging/live ayrımında acemice bir kopyala‑yapıştır bazen kafa karıştırır.

Bir diğer küçük detay da bakım sayfaları. Geçiş sırasında milisaniyelik reload’larda bile kullanıcıya pürüzsüz bir deneyim vermek istersiniz. Nginx’in ‘try_files’ yaklaşımıyla hafif bir bakım sayfasını devreye almak, acil durumlarda da hayat kurtarır.

Adım Adım Mini Rehber: Baştan Sona Kısa Bir Tur

İşi topyekûn görmek isteyenler için kısaca özetleyeyim. Önce gerekli PHP sürümlerini kurun ve PHP‑FPM servislerini etkinleştirin. Her site için ayrı kullanıcı açın, dizinleri sahipleriyle oluşturun. Havuz dosyalarını doğru sürümün pool.d klasörüne bırakın; soket yollarını not edin. Nginx sunucu bloklarını hazırlayıp ilgili havuza bağlayın. Test edin, reload edin, logları izleyin. Cron ve CLI süreçlerinde doğru PHP sürümünü özellikle çağırın. Her şey yolundayken OPcache ve pm ayarlarıyla küçük tuning’ler yapın.

Yapılandırma seçeneklerinin tamamını merak edenler için dokümantasyon elinizin altında. PHP‑FPM havuz parametrelerinin açıklamalarını okuyup işin mutfağına daha da yaklaşabilirsiniz. Bir parametrenin tek satırlık değişimi bile, yoğun bir sitede büyük fark yaratabilir.

Kapanış: Sunucuda Barış, Sende Huzur

Tek sunucuda birden fazla PHP sürümünü çalıştırmak ilk bakışta göz korkutabilir. Ama mantığı yerleştirdikten sonra, Nginx’in nazik karşılama masasını ve PHP‑FPM’in düzenli mutfağını kurduğunuzda, her şey ritmini buluyor. Her site için ayrı havuz demek, ayrı nefes demek. Logları ayrı, tmp’si ayrı, limiti ayrı. Arıza çıktığında kimin sorunu olduğunu dakikalar içinde anlıyorsunuz; güncelleme gerektiğinde de küçük adımlarla ilerleyip büyük dertlerden kaçınıyorsunuz.

Pratik tavsiye olarak şunları cebinize koyun: Havuzları isimlendirirken anlamlı olun, soket yollarını standardize edin, CLI’da hangi PHP’yi çağırdığınızı bilerek hareket edin. Gözlemi ihmal etmeyin; küçük uyarılar ve izleme panoları birçok sürprizi daha doğmadan çözer. Bu yazı umarım yolunuzu aydınlatır. Takıldığınız yerde geri dönüp bakabileceğiniz bir rehber niyetine dursun. Bir dahaki yazıda daha derin performans dokunuşlarını ve sorunsuz deploy hilelerini konuşuruz. O zamana kadar, sunucunuz sakin, siteleriniz hızlı olsun.

“,
“focus_keyword”: “Tek sunucuda birden fazla PHP sürümü”,
“meta_description”: “Tek sunucuda birden fazla PHP sürümünü Nginx ve PHP‑FPM havuzlarıyla site bazlı, güvenli ve hızlı şekilde kurmayı adım adım, gerçek örneklerle kolayca öğrenin.”,
“faqs”: [
{
“question”: “Tek sunucuda hem PHP 7.4 hem de 8.2’yi aynı anda çalıştırabilir miyim?”,
“answer”: “Evet, mümkün. Her PHP sürümü için ayrı PHP‑FPM servisi kurup, her siteye ayrı havuz ve soket tanımlarsınız. Nginx’te ilgili sitenin .php isteklerini doğru sokete yönlendirince sorunsuzca beraber yaşarlar.”
},
{
“question”: “502 Bad Gateway hatası alıyorum, nereden başlayayım?”,
“answer”: “Genelde üç noktayı kontrol etmek işe yarar: PHP‑FPM havuzu gerçekten çalışıyor mu, Nginx doğru soket yoluna bakıyor mu, soket izinleri Nginx’in kullanıcı ve grubunu kapsıyor mu. Bu üçlü çoğu 502’nin kökünü gösterir.”
},
{
“question”: “Cron ve CLI tarafında hangi PHP sürümünü kullanmalıyım?”,
“answer”: “Nginx’in kullandığı FPM sürümü ile terminaldeki ‘php’ komutu aynı olmak zorunda değil. Cron veya komutlar için ‘php8.1’ gibi tam sürüm binarisini kullanmak en temiz yöntemdir; karışıklık yaşanmaz.”
},
{
“question”: “Soket mi yoksa 127.0.0.1:9000 gibi TCP mi tercih etmeliyim?”,
“answer”: “Aynı sunucuda çalışıyorsanız Unix soket genellikle pratik ve hızlı olur. İzinleri doğru ayarlayın yeter. Ayrı makineye ya da konteynere konuşacaksanız TCP mantıklıdır. Esas olan değişiklikten sonra ölçmek ve izlemektir.”
}
]
}