Küçük ve orta SaaS projeler için tek bir VPS, birden fazla uygulamayı barındırmak için fazlasıyla yeterli. Kubernetes, mesh, “cloud native” baskısı pek çok ekibi bu yoldan saptırıyor — ama eğer toplam yük 1 makineye sığıyorsa, dağıtım maliyeti neredeyse her zaman değmez.
Bu yazıda kullandığım örüntüyü adım adım göstereceğim.
Hedef
- 3–5 bağımsız Laravel uygulaması.
- Her uygulama kendi domain’i, kendi süreci, kendi user’ı.
- PostgreSQL ve Redis paylaşılıyor ama izole.
- Deploy bir komutla, zero-downtime.
- Backup ve monitoring tek yerden.
- Tüm bunlar < $40/ay’lık bir VPS’te.
Sistem haritası
┌──────────────────────────────┐
80/443 │ NGINX │
─────────────► │ (TLS termination, vhost'lar) │
└────┬──────────┬──────────┬───┘
│ │ │
unix sockets │ │
│ │ │
┌──────▼───┐ ┌────▼─────┐ ┌─▼──────┐
│ PHP-FPM │ │ PHP-FPM │ │ PHP-FPM│
│ billing │ │ notify │ │ blog │
│ pool │ │ pool │ │ pool │
└──────┬───┘ └────┬─────┘ └─┬──────┘
│ │ │
└──────┬───┴───┬──────┘
│ │
┌────▼───────▼──────┐
│ PostgreSQL 16 │
│ + pgBouncer │
└────────────────────┘
┌────────────────────┐
│ Redis 7 │
│ (namespace prefix) │
└────────────────────┘
┌────────────────────┐
│ Supervisor │
│ queue workers/proj │
└────────────────────┘
Kullanıcı ve dosya yerleşimi
Her uygulama kendi unix kullanıcısını alıyor:
billing:billing /var/www/billing
notify:notify /var/www/notify
blog:blog /var/www/blog
www-data (Nginx) gruba dahil ediliyor, kod dizinine okuma erişimi için. Yazma
sadece kendi uygulamasının kullanıcısında. Bu izolasyon işten çıkan bir uygulama
diğerlerine zarar veremesin diye değil; bir geliştirici hatasının diğer
projeyi etkilemesini önlemek için — ki bu çok daha sık olan senaryo.
PHP-FPM pool’ları
Nginx + PHP-FPM Pool Ayrımı yazısında detaylı.
Burada özet: her uygulama kendi pool.d/*.conf dosyasında pm.max_children,
memory_limit ve unix kullanıcısıyla tanımlanıyor.
Veritabanı: paylaşılan PostgreSQL
Tek bir PostgreSQL cluster, her uygulama için ayrı bir database ve role:
CREATE ROLE billing_app LOGIN PASSWORD '...';
CREATE DATABASE billing OWNER billing_app;
REVOKE ALL ON DATABASE billing FROM PUBLIC;
pg_hba.conf:
host billing billing_app 127.0.0.1/32 scram-sha-256
host notify notify_app 127.0.0.1/32 scram-sha-256
host blog blog_app 127.0.0.1/32 scram-sha-256
Hiçbir uygulama diğerinin DB’sini göremez bile.
pgBouncer’la bağlantı havuzu
PHP-FPM bağlantıyı per-request açıp kapatıyor — uzun süreli bağlantı havuzu yok.
PostgreSQL bağlantı kurulumu (~10ms) kümülatifte ciddi. pgBouncer önünde
pool_mode = transaction ile bunu çözüyoruz. auth_query yazısı
detayı veriyor.
Redis: paylaşılan, namespace’le izole
Tek bir Redis instance. Her Laravel uygulaması farklı bir prefix:
'redis' => [
'cache' => [
'host' => '127.0.0.1',
'database' => 0,
'prefix' => 'prod:billing:cache:',
],
'queue' => [
'host' => '127.0.0.1',
'database' => 0,
'prefix' => 'prod:billing:queue:',
],
],
Detay namespace izolasyonu yazısında.
Queue worker’ları
Supervisor her uygulamanın queue worker’ını ayrı bir process grup olarak yönetiyor:
/etc/supervisor/conf.d/
├── billing-worker.conf
├── notify-worker.conf
└── blog-worker.conf
Her dosyada uygulama kendi user’ı ile çalışıyor:
[program:billing-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/billing/current/artisan queue:work redis --queue=default --sleep=1 --max-time=3600
user=billing
numprocs=4
autostart=true
autorestart=true
stopwaitsecs=3600
Deploy: symlink swap
Her uygulama dizininde:
/var/www/billing/
├── releases/
│ ├── 2026-05-12-1430/
│ ├── 2026-05-13-0900/
│ └── 2026-05-13-1100/ ← yeni release
├── shared/
│ ├── .env
│ ├── storage/
│ └── public-uploads/
└── current → releases/2026-05-13-1100
Deploy adımları:
- Yeni release klasörünü oluştur, git checkout, composer install, build.
shared/içeriklerini sembolik linkle.- Migrate (forward-compatible olmalı).
currentsymlink’i atomik olarak yeni klasöre çevir (ln -sfn).- PHP-FPM’e
kill -USR2ile graceful reload (opcache invalidation). - Supervisor restart sadece queue worker’larına.
Eski release’leri saklamak (örn. son 5) rollback’i bir komuta indirgiyor: symlink’i eski klasöre çevir, PHP-FPM reload.
Backup
- PostgreSQL: pgBackRest, repository S3-uyumlu kovaya yazıyor.
- Redis: AOF kapalı, RDB her saat. Cache-only kullanım için yeterli.
- Uploads (
shared/public-uploads): rclone ile saatlik S3 sync. /etc: günlük tar + S3 (config’in versiyon kontrolü).
Monitoring
Self-hosted bir Prometheus + Grafana abartı olur bu ölçek için. Tercih ettiğim:
- uptime-kuma — her domain’i HTTP check’le izliyor, Slack/Telegram alarmı.
- node_exporter + Cloudflare metrics — sunucu sağlığı.
- PHP-FPM
/statusendpoint — slow request sayısını izle. - logrotate —
/var/log/{nginx,php-fpm,laravel}/*.loggünlük.
Limitler ve büyüme sinyalleri
Bu mimari aşağıdaki sinyallerden biri görülünce kırılır:
- CPU sürekli > %70. Vertical scale (daha büyük VPS) bir kez yapılır, sonra uygulama ayrıştırması.
- Tek bir uygulama DB’de baskın hale gelirse. O uygulamayı kendi PostgreSQL’ine taşıyın — replica veya managed servis.
- Coğrafi gecikme şikayetleri. Bu durumda CDN’e statik asset taşıma yeterli; app server’ı taşımak ekstrem.
Niye Kubernetes değil?
Üç sebep:
- Operasyon yükü. Tek başınıza bir k8s cluster’ı çalıştırmak, üzerinde uygulama yazmaktan daha fazla zaman alır.
- Maliyet. 3 küçük node’lu managed k8s, eşdeğer VPS’in 3–5 katı tutuyor.
- Karmaşıklık ödülü yok. Bu uygulamalar bağımsız ölçeklenmek zorunda değil.
Bu mimari “lazy-eval k8s” değil — başka bir paradigma. Sade, anlaşılır, operasyonel olarak küçük bir mimari. Lazım olduğunda parçalanır, ondan önce değil.