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

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ı:

  1. Yeni release klasörünü oluştur, git checkout, composer install, build.
  2. shared/ içeriklerini sembolik linkle.
  3. Migrate (forward-compatible olmalı).
  4. current symlink’i atomik olarak yeni klasöre çevir (ln -sfn).
  5. PHP-FPM’e kill -USR2 ile graceful reload (opcache invalidation).
  6. 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 /status endpoint — slow request sayısını izle.
  • logrotate/var/log/{nginx,php-fpm,laravel}/*.log gü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:

  1. Operasyon yükü. Tek başınıza bir k8s cluster’ı çalıştırmak, üzerinde uygulama yazmaktan daha fazla zaman alır.
  2. Maliyet. 3 küçük node’lu managed k8s, eşdeğer VPS’in 3–5 katı tutuyor.
  3. 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.