Dörd maşın idarə edirəm və onlar üç işə bölünür: pul qazandıran bir production serveri, eyni dəmir üzərində yaşayan bir staging klonu, bir multi-tenant QA/demo hostu (bu sındırıldı — aşağıda ona toxunacağam) və avtonom-agent yükü üçün daim işləyən bir droplet. Aşağıdakı hər şey real konfiqurasiyadır, səliqəyə salınmış ideallaşdırılmış versiya deyil. Boşluqlar bilərəkdən adı ilə qeyd olunub.
15 GB tavanı altında tənzimlənmiş production serveri
example — 15 GB RAM-a malik Ubuntu 24.04 serveridir (203.0.113.10), üzərində Virtualmin + nginx + PHP-FPM 8.3 + MariaDB 10.11 işləyir. example.com, site-a, site-b, site-c və bir Booknetic SaaS quraşdırmasını host edir. 15 GB-ı bu qədər pool bölüşəndə defolt parametrlər səni öldürər.
Əsas dəyişiklik PHP-FPM-i pm = dynamic-dən pm = ondemand-a keçirmək oldu. Bundan sonra boşdayanan pool-lar sıfır xərc tutur — child proseslər yalnız real sorğu gələndə yaranır. pm.max_children-i 24-dən 15-ə endirdim (24 bu serverdə OOM riski idi), yaddaş sızmalarını cilovlamaq üçün worker-ləri təzələyən pm.max_requests = 500 və boş child-ləri tez ləğv etmək üçün pm.process_idle_timeout = 10s əlavə etdim. Pool-başına memory_limit hər yerdə 256M-dir — site-c istisna olmaqla, onun qiymət-siyahısı generatoru 256M-də həqiqətən OOM verirdi, ona görə həmin bir pool 512M alır. Bu, qlobal artım deyil, pool-başına override-dır; hər pool-u onun ən pis halını ört-basdır etmək üçün şişirtməkdənsə, bir acgöz sayta daha çox hava verməyi üstün tuturam.
Ən böyük verilənlər bazası qələbəsi utandırıcı dərəcədə sadə idi. /etc/mysql/mariadb.conf.d/99-tuning.cnf faylında innodb_buffer_pool_size-ı defolt 128M-dən 3G-yə qaldırdım. WooCommerce yükü üçün 128M absurddur — isti indekslərinin çoxu ora sığmır belə, ona görə RAM-a düşməli olan hər sorğu diskə gedir. /etc/php/8.3/fpm/conf.d/99-opcache-tuning.ini faylındakı OPcache opcache.memory_consumption = 256 və opcache.max_accelerated_files = 32000 alır, çünki WordPress + WooCommerce + Booknetic birlikdə heyrətamiz sayda PHP faylı daşıyır və defolt fayl limiti onların yarısını səssizcə keşsiz buraxır. Həmçinin vm.swappiness = 10 ilə 4 GB-lıq /swapfile (/etc/fstab-da sabitlənib) əlavə etdim ki, kernel trafik artımı zamanı swap-a ilk çıxış yolu kimi yox, təhlükəsizlik şəbəkəsi kimi yanaşsın.
Obyekt keşi Redis-dir, maxmemory 512mb və allkeys-lru eviction ilə, və ən sevdiyim hiylə budur: hər sayt üçün bir Redis DB, hər wp-config.php-də WP_REDIS_DATABASE ilə təyin olunur. site-a=0, site-b=1, example=2, site-c=3, staging=4, növbəti sayt=5. Tək paylaşılan Redis bir saytın FLUSHDB-sinin bütün digər saytların keşini partlatmasına imkan verərdi; nömrələnmiş DB-lər beş ayrı Redis nüsxəsi işlətmənin yükü olmadan ucuz namespace verir.
Daemon-u ifşa etmədən monitorinq
Netdata quraşdırılıb, amma yalnız 127.0.0.1:19999-a bağlıdır — xam port heç vaxt açıq internetdə deyil. Onu əlçatan etmək üçün monitor.example.com-u öz Virtualmin domeni kimi yaratdım: nginx loopback-dakı Netdata-ya reverse-proxy edir, HTTP Basic Auth (istifadəçi monitor) arxasında, wildcard Cloudflare Origin sertifikatı ilə. Bu loopback-a-bağla-sonra-önünə-qoy nümunəsi bütün park boyu mənim üslubumdur. Dürüst status: Cloudflare DNS qeydi və Telegram bildirişləri (~80% sağlamlıq alarmları üçün bot token + chat ID) hələ gözləmədədir.
Dürüst olarkən: bu serverdə hələ UFW yoxdur, SSH hələ də parol authentifikasiyasına icazə verir və origin-in 80/443-ü Cloudflare IP-lərinə məhdudlaşdırılmayıb, yəni çılpaq IP-yə vuran istənilən kəs CF-i tamamilə atlayır. Bunlar unutduğum şeylər deyil, təxirə salınmış kompromislərdir. Yerində olan şey isə fail2ban (sshd/webmin/usermin/mail jail-ləri) və saatda bir dəfə Dropbox-a işləyən Virtualmin backup-ıdır. Və parol-auth “boşluğu” bir dəfə məni xilas etdi: ~/.ssh/id_ed25519-u təzə açarla üzərinə yazıb özümü kilidlədikdən sonra ssh-copy-id yalnız parol auth hələ də aktiv olduğu üçün işlədi. Bərpa qapısı ilə təhlükəsizlik dəliyi eyni qapı oldu.
Cloudflare planlı işləri səssizcə sındıranda
Bu mənim ən sevdiyim döyüş hekayəmdir, çünki aşkar yerdə heç nə “sınmamışdı”. 2026-05-18-də təxminən 20:25-də example.com-da vaxt-əsaslı WhatsApp və e-poçt xatırlatmaları işləməyi dayandırdı. Twilio məlumatları etibarlı idi — əvvəlki həftədə 886 uğurlu göndərmə — ona görə zahirən Twilio və ya WhatsApp problemi kimi görünürdü. Amma deyildi.
Bir çox WordPress steki kimi, bu saytın planlı tərəfi də server-tərəfi loopback sorğusuna söykənir — wp-cron və Action Scheduler-in istifadə etdiyi nümunədir, burada PHP gözləyən işi işlətmək üçün öz wp-cron.php / admin-ajax.php-na geri HTTP zəngi vurur. Həmin tarix ətrafında Cloudflare-in Bot Fight Mode-u aktivləşdirildi və həmin endpoint-lərin hər ikisində cf-mitigated: challenge ilə HTTP 403 qaytarmağa başladı. cURL loopback JS challenge-i həll edə bilməz, ona görə özünə-HTTP-zəng-vuran nə varsa, sadəcə heç vaxt işləmədi. Hadisə-əsaslı iş işləməyə davam etdi — çünki o, challenge-i keçən real ziyarətçinin brauzer sorğusunun içində sinxron işləyir. Məhz bu asimmetriya — planlı işlər ölü, ani işlər diri — onu mesajlaşma xətasının donuna saldı.
Düzəliş ümumiyyətlə planlaşdırma üçün Cloudflare-proxy HTTP loopback-ına söykənməyi dayandırmaqdır. WordPress-in HTTP ilə işə düşən cron-unu söndürdüm (define('DISABLE_WP_CRON', true)) və gözləyən işi example istifadəçisinin crontab-ındakı real dəqiqəlik sistem cron-undan işə salıram, birbaşa WP-CLI çağıraraq, beləcə heç nə HTTP üzərindən serveri tərk etmir:
* * * * * /usr/bin/flock -n /tmp/wpcron.lock /usr/local/bin/wp --path=/home/example/public_html cron event run --due-now >/dev/null 2>&1
flock -n qoruyucusu üst-üstə düşən dəqiqəlik işləmələrin bir-birinin üstünə yığılmasının qarşısını alır. Keçid edəndə hər hansı tək “son işləmə” zaman möhürünə güvənmək əvəzinə işin həqiqətən işlədiyini yoxla — CLI ilə işə salınan işləmə tətbiqin UI-yönəlik “işlədi” markerini dəyişməmiş qoya bilər, ona görə tətbiqin öz log cədvəlini id üzrə yoxlayıram. Əsl kök-səbəb düzəlişi — origin egress IP-yə məhdudlaşdırılmış */wp-cron.php və */admin-ajax.php üçün Cloudflare WAF skip qaydası — hələ siyahıdadır. Ümumiləşdirilə bilən dərs: loopback wp-cron və ya Action Scheduler-ə söykənən istənilən Cloudflare-proxy saytı Bot Fight Mode aktivləşən an səssizcə dayana bilər. Köçürülmüş WooCommerce saytının planlı işləri ilişib qalmış görünürsə, əvvəlcə buna bax.
Eyni serverdə hər qatda izolyasiya edilmiş staging
staging.example.com eyni fiziki serverdə, amma ayrıca Virtualmin domeni kimi yaşayır, bu da hər qatda tam izolyasiya verir: öz unix istifadəçisi staging (/home/staging), öz FPM pool-u (/etc/php/8.3/fpm/pool.d/17792676891117477.conf, 256M, 64M upload/post), öz MySQL DB və məlumatları, öz Redis DB 4 və öz nginx vhost-u. Giriş /etc/nginx/monitor.htpasswd-ə qarşı auth_basic ilə server boyu məhdudlaşdırılıb — monitor domeni ilə eyni monitor məlumatları — .well-known/ isə auth_basic off ilə ayrılıb ki, ACME TLS yeniləməsi üçün hələ də təsdiq edə bilsin.
Klon dörd addımlıq runbook-dur: docroot-u rsync et, mysqldump ... | mysql staging, yeni məlumatlar plus WP_REDIS_DATABASE=4 üçün wp config set, sonra wp search-replace https://example.com https://staging.example.com --skip-columns=guid (son işləmədə 127 əvəzləmə), sonra chown -R staging:staging.
Və budur mənə real vaxt itkisinə mal olan tələ: wp search-replace verilənlər bazasını yeniləyir, amma Redis-i etibarsız ETMİR. Klondan sonra DB-də siteurl/home staging.example.com oxuyurdu, amma home_url() hələ də keşlənmiş example.com dəyərini qaytarırdı, ona görə redirect_canonical hər bir staging sorğusunu 301 ilə düz production-a geri yönləndirirdi. Düzəliş bir sətirdir — redis-cli -n 4 FLUSHDB — və indi bu sərt qaydadır: search-replace-dən sonra həmişə klonlanmış saytın Redis DB-sini flush et.
Ən vacib sərtləşdirmə e-poçt kill-switch-idir, çünki staging-in səni biabır etməyin bir nömrəli yolu real müştərilərə e-poçt göndərməkdir. /home/staging/public_html/wp-content/mu-plugins/staging-disable-email.php-dəki mu-plugin pre_wp_mail filtrindən true qaytarır (bütün göndərməni qısa qapayır) və, ehtiyat tədbiri olaraq, phpmailer_init-də bütün alıcıları və əlavələri təmizləyir. wp option update blog_public 0 və Disallow: / robots.txt ilə birlikdə staging heç kimə e-poçt göndərə və ya indekslənə bilməz. Çətinlik budur ki, hər yenidən-klonlama rsync-i mu-plugin-i, Redis DB parametrini və blog_public-i üzərinə yazır — ona görə hər üçü hər dəfə yenidən tətbiq edilməli (və FLUSHDB yenidən işlədilməlidir), yoxsa klon səssizcə özünü yenidən production kimi davranmağa kökləyir.
İzolyasiyanı sübut edən sındırma
panel.itahir.com — OVH VPS-dir (213.32.21.187), Webmin/Virtualmin altında Ubuntu, Booknetic QA saytları, demo mühitləri və n8n avtomatlaşdırması host edir — hər tenant öz Linux istifadəçisi altında (n8n, sizinzaman, dev, saas, bktest, qatest1/2, env1/2…). Webmin (:10000) və Usermin (:20000) hələ də açıqdır, bu da burada əhəmiyyət kəsb etdiyi üzə çıxan qeyd edilmiş boşluqdur.
2026-05-20-də /home/test/ saytı zəif ElementsKit page-builder plagini vasitəsilə sındırıldı — məlum RCE vektoru. Hücumçu kompilyasiya edilmiş binar faylları phpseclib-i təqlid edən saxta ikili-vendor yolunda gizlətdi: wp-content/plugins/elementskit/libs/composer/vendor/build/vendor/src/phpseclib/.../Reductions/.tmp. İmtiyazsız test istifadəçisi kimi işləyərək, WordPress credential-stuffing edən 600-dən çox çıxış HTTPS bağlantısı açdı, bu da OVH abuse hesabatını işə saldı (bilet +CQBQBDPPTK.1fb6). Cilovlama kobud oldu:
sudo pkill -9 -u test -f "elementskit/libs/composer/vendor/build"
find /home -type d -path "*vendor/build/vendor*"
# sonra: /home/test/ və test istifadəçisi tamamilə silindi
Server boyu audit təmiz çıxdı: root sındırılması yox, digər tenantlara yayılma yox, rootkit yox, backdoor SSH açarları yox. Cilovlanmış qalmasının səbəbi məhz hər-istifadəçi Virtualmin izolyasiyasıdır — partlama radiusu bir sayt idi, çünki hücumçu yalnız test ola bilərdi. Bu, hər-tenant Linux istifadəçiləri üçün arxitektura arqumentidir, çətin yolla təsdiqlənmiş. Dərslər cəlbedici deyil: WP plaginlərində patch gigiyenası narahat olduğun OS sərtləşdirməsindən daha vacibdir, açıq Webmin/Usermin bağlamaq istədiyin hücum səthidir və phpseclib-i təqlid edən vendor/build/vendor istənilən paylaşılan hostda grep etməyə dəyər konkret IOC-dir.
Təhlükəsizliyi düzgün etdiyim droplet
cortex droplet-i (DigitalOcean, 159.89.9.22, Ubuntu 24.04.3, 2 vCPU / 3.8 GB / 116 GB) qəsdən qoyulmuş qarşı nöqtədir. Onu xüsusi olaraq ona görə provizioned etdim ki, eksperimental avtonom-agent + Docker yükü production tenantları ilə eyni serveri PAYLAŞMASIN — və panel sındırılmasından sonra bu mülahizə paranoik yox, özünü doğrultmuş hiss olunurdu.
O, yalnız SSH-açarlıdır. UFW SSH-ə icazə verir, başqa heç nəyə yox. Board UI 127.0.0.1-ə bağlanır və ona tunel vasitəsilə çatırsan (ssh -L 7878:127.0.0.1:7878) — Netdata ilə eyni loopback-əvvəl instinkti, amma nginx+Basic-Auth əvəzinə tunel. fail2ban aktivdir. Bootstrap (scripts/install-droplet.sh) Docker 29.x və 2 GB swap quraşdırır. Worker konteynerləri claude-u non-root node istifadəçisi kimi işlədir, nəticələri /work/task-<id>-ə yazır; ən önəmlisi, host-un özündə claude CLI belə yoxdur — claude yalnız sandbox-lanmış konteynerin içində işləyir, ~/.config/cortex/worker-token-dən inyeksiya edilmiş CLAUDE_CODE_OAUTH_TOKEN ilə authentifikasiya olunur (Mac-də generasiya edilir və SSH üzərindən ötürülür, heç yerə yapışdırılmır).
Dürüst qəribəlik: /root/cortex-dəki repo git clone ilə yox, tar-over-ssh ilə deploy edildi, ona görə serverdə .git yoxdur — yeniləmələr docs/DEPLOY.md-də təsvir edilmiş deploy açarını quranadək dəyişdirilmiş faylları yenidən tar etmək deməkdir. Yan-yana qoyduqda, park bir təhlükəsizlik-yetkinlik qradiyentidir: droplet düzgün edir (UFW, yalnız-açar, loopback UI, non-root sandbox, minimal host); example adlandırılmış boşluqlarla praqmatikdir; panel isə bahasını ödədiyim dərsdir. Self-hosting hər serveri eyni səviyyəyə dartmaqdan ibarət deyil — hansı serverin hansı səviyyəyə layiq olduğunu bilmək və qalanı barədə dürüst olmaqdan ibarətdir.