14 canlı site, tek Git deposu: paylaşımlı çekirdek, izole veritabanları, --delete'siz rsync, bir SSH ban hikâyesi ve 14.616 NUL bayttan doğan CI bekçisi.
jekcms'i ben geliştiriyorum; en yüklü kullanıcısı da yine benim. Şu anda 14 canlı içerik sitesi bu sistemin üzerinde dönüyor — müşteri demosu değil, kendi yayın ağım. En büyük sitede 2.300'den fazla yazı var; filonun toplamı 5.500 yazıyı geçti. Hepsi tek bir Git deposundan çıkıyor ve Cloudflare arkasındaki paylaşımlı LiteSpeed hosting'e dağıtılıyor.
Bu yazı işin cilasız hâli: mimari, deploy hattı ve ikisini de şekillendiren birkaç yara izi.
Tek depo, on dört kapı
Dizin yapısı bilerek sıkıcı. Ortada paylaşılan bir çekirdek var — sınıflar, yardımcılar, admin paneli, API — ve her sitenin yalnızca kendine ait olanı tuttuğu bir sites/ dizini:
jekcms/
├── classes/ # paylaşılan çekirdek — her site için aynı
├── includes/
├── themes/ # 13 bitmiş tema + starter iskeleti
└── sites/
├── site-a/
│ ├── config/environment.php # DB bilgileri, anahtarlar — git'e girmez
│ ├── theme/ # siteye özel görünüm
│ └── uploads/
├── site-b/
└── ...
Çekirdekte düzeltilen bir hata on dört sitede birden düzelir. Satış cümlesi bu ve yıllardır kendi filomda tuttuğunu gördüm. Madalyonun öbür yüzü de aynı derecede doğru: çekirdeğe karışan bir hata da on dört siteye birden gider. Aşağıda anlatacağım deploy hattının varlık sebebi büyük ölçüde bu ikinci cümle.
Her siteye kendi veritabanı
Multi-tenant şema yok, her sorguya iliştirilmiş bir site_id kolonu yok. Her site kendi kimlik bilgileriyle kendi izole MySQL veritabanına bağlanır. Gerekçeler ideolojik değil, gayet pratik: ele geçirilen bir site komşularının verisini okuyamaz, bir şema migrasyonu 2.300 yazılık siteye dokunmadan önce küçük bir sitede prova edilebilir ve bir geri yükleme her zaman tek sitenin dump'ıyla sınırlı kalır.
Temalar da aynı ilkeye uyar. Çekirdek 13 bitmiş tema ve bir starter iskeletiyle gelir; her site ya birini seçer ya da kendi dizininde kendi tema dosyalarını taşır. A sitesinin tasarımına dair hiçbir şey, B sitesinin ayağına takılabileceği bir yerde durmaz.
Deploy: GitHub Actions, rsync ve sinirli bir güvenlik duvarı
main'e push, Actions kontrolleri koşar, rsync sonucu sunucuya iter. Ders kitabı gibi. Hostinger, runner'ın IP'sini deploy'un tam ortasında banlamaya başlayana kadar da gayet güzel çalıştı.
Sebep masum görünüşlü trafikti. İş, her rsync hedefi için sıfırdan bir SSH kimlik doğrulaması açıyordu — on dört site, art arda on dört auth, üstelik bir datacenter IP'sinden. Hostinger'ın saldırı tespiti bunu birebir brute-force denemesi gibi okuyup bağlantıyı kesiyor. Sonuç: filonun yarısı yeni kodda, yarısı eskide. Olabilecek en kötü durum.
Çözüm siteleri azaltmak ya da deploy'u yavaşlatmak olmadı. SSH ControlMaster oldu:
# deploy işinin kullandığı ssh_config
Host deploy
HostName server.example.com
Port 65002
ControlMaster auto
ControlPath /tmp/cm-%r@%h-%p
ControlPersist 10m
İlk bağlantı bir kez kimlik doğrular; sonraki bütün rsync'ler aynı çoklanmış soketin üzerinden akar. Güvenlik duvarının gördüğü tek bir login vardır. Bu ayar girdiği gün banlar bitti.
rsync'in dokunması yasak olan yerler
Hiçbir deploy komutunda --delete yok ve olmayacak:
rsync -az \
--exclude='config/environment.php' \
--exclude='uploads/' \
--exclude='cache/' \
--exclude='logs/' \
./ deploy:~/public_html/
environment.php production kimlik bilgilerini taşır ve yalnızca sunucuda yaşar. uploads/ başka hiçbir yerde kopyası olmayan yılların medyasını tutar. İkisinden birine değen tek bir dikkatsiz --delete, bu bayrağın ömrü boyunca temizleyeceği bütün artık dosyalardan daha pahalıya patlar. Artıkları kabul ediyorum, ara sıra elle temizliyorum. İyi bir takas.
rsync bitti diye deploy bitmiş sayılmaz
Bunu içselleştirmem utanç verici derecede uzun sürdü: PHP'nin OPcache'i diskteki dosyaların değişmesiyle ilgilenmez. Düzeltmeyi deploy edersiniz, sayfayı çekersiniz, hata hâlâ oradadır — çünkü sunucu keyifle geçen haftanın derlenmiş bytecode'unu çalıştırmaktadır. Disk yeni, çalışan kod eski.
Bu yüzden her deploy, her sitede opcache_reset() çağıran token korumalı bir uçla biter; ardından sayfa önbelleği temizlenir. Sayfa önbelleğinin TTL'i 300 saniye; purge atlansa en fazla beş dakikalık bayatlık olur. Ama bayat OPcache'in üstüne bayat sayfa önbelleği bindiğinde, aynı hatayı bir akşamda üç kez "düzeltmeye" başlarsınız. İkisini de sıfırla, sonra bütün siteleri tek tek çek ve 200 bekle.
14.616 NUL bayt: CI bekçisinin doğduğu gün
Haziran 2026. Filodaki dört site bomboş gizlilik sayfaları servis ediyordu. 404 değil — gövdesi boş, tertemiz 200 yanıtları. O temalardaki legal.php dosyalarının tam 14.616 NUL bayttan ibaret olduğu ortaya çıktı. Yolun bir yerinde bir dosya yazımı yarıda kalmış; geriye, PHP'nin include edip "hiçbir şey" olarak render etmeye tamamen razı olduğu sıfır dolu bir kabuk bırakmıştı.
Reklam yayınlayan bir ağ için boş gizlilik sayfası, başvuru reddettirecek ağırlıkta bir kusurdur. Ve sessizce orada durdu; çünkü hattaki hiçbir adım "içi NUL dolu PHP dosyası"nı anormal saymıyordu.
Bekçi aynı gün girdi:
if grep -rlaP '\x00' --include='*.php' .; then
echo "PHP dosyalarında NUL bayt var — deploy reddedildi"
exit 1
fi
Şüpheli derecede boş stub dosyalarını da işaretliyor. Aptal, ucuz ve maaşını çoktan çıkardı.
Actions sunucuya ulaşamazsa?
Hattın bir yedeği var: self-deploy. Sunucunun kendi üzerindeki bir cron işi git pull ve aynı deploy-sonrası adımları koşabiliyor; runner banlanmışsa, limite takılmışsa ya da GitHub kötü bir gün geçiriyorsa filo sunucu tarafından güncellenmeye devam eder. Kural aynı — OPcache sıfırlanmadan pull bitmiş sayılmaz. Bu tür sunucu tarafı işlerin ayrıntıları dokümantasyonda.
Filo ölçeğindeki kaldıraç iki yöne de çalışır
Tek kod tabanını verimli kılan her şey, hataları da aynı oranda büyütür. Kendi kayıt defterimden iki örnek, ikisi de Haziran 2026'dan.
Birincisi: on dört sitede toplam 2.700 civarı yazıya dokunan bir veri onarımı, tek geçişte — üstüne 137 yanlış meta açıklamasının temizliği. On dört ayrı kurulumda bu, on dört ayrı bakım penceresi demekti. Site başına ayrı veritabanı olan tek kod tabanında ise tek script oldu: önce bir sitede prova, sonra kalanlarda döngü. Kaldıracın lehinize çalıştığı an bu.
İkincisi, aleyhinize çalıştığı an: bir dönem otomatik iç-link aracını filonun tamamında agresif çalıştırdım. Verimliydi, tutarlıydı — ve bütün siteleri aynı anda link kataloğuna benzetti. O tekdüzelik bir AdSense reddine katkı verdi. Çözüm otomasyonun tam tersiydi: yazıları elden geçirip linkleri editoryal seçmek. Bazı işler ölçeklenmemeli; hangileri olduğunu bilmek de filo işletmenin parçası.
Peki gerçekten ölçekleniyor mu?
On dört site. 5.500'ü aşmış ve büyümeye devam eden yazı sayısı; en büyük tekil site 2.300+ yazıda. Hepsi paylaşımlı hosting'de — önünde Cloudflare olan LiteSpeed, VPS kümesi falan değil. Bu, CMS'in ucuz hosting'i baştan varsayıp ona göre agresif önbelleklemesi sayesinde mümkün.
"Ölçekleniyor mu?" sorusunun asıl cevabı şu: öğrenmemek gibi bir lüksüm yok. Her jekcms sürümü, başka birinin sitesine değmeden önce benim on dört siteme çarpıyor. Deploy akışı can sıkıcıysa, on dört kez canı sıkılan benim. İsterseniz dogfooding deyin — bu geri besleme döngüsü mimariye, yazdığım her tasarım dokümanından daha çok şey kattı. Bu işi kimin, neden yaptığının uzun hâli de hakkımızda sayfasında.