---
title: "Tek VPS Üzerinde Çoklu Proje Mimarisi"
description: "Bir VPS, birden çok bağımsız uygulama. Kubernetes yerine bilinçli minimalizm: kullanıcı izolasyonu, ayrı PHP-FPM pool, paylaşılan PostgreSQL/Redis."
url: https://sade.dev/tr/systems/tek-vps-coklu-proje-mimarisi/
lang: tr
author: "Muhammet Şafak"
published: 2026-04-30
updated: 2026-09-06
section: System
tags: ["vps","architecture","nginx","postgresql","redis"]
summary: "Kubernetes yerine bilinçli minimalizm. Tek VPS üzerinde çoklu Laravel projesi için operasyonel olarak dayanıklı bir mimari."
---

# Tek VPS Üzerinde Çoklu Proje Mimarisi

> Kubernetes yerine bilinçli minimalizm. Tek VPS üzerinde çoklu Laravel projesi için operasyonel olarak dayanıklı bir mimari.

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 kullanıcısı.
- PostgreSQL ve Redis paylaşılıyor: PostgreSQL'de role bazında izole, Redis'te prefix'le ayrılmış.
- Deploy bir komutla, zero-downtime.
- Backup ve monitoring tek yerden.
- Tüm bunlar < $40/aylık bir VPS'te.

## 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 **hatalı davranan 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ı](/tr/notes/nginx-php-fpm-pool-ayrimi) 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**:

```sql
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ı varsayılan olarak per-request açıp kapatıyor — kalıcı bağlantıyla (`PDO::ATTR_PERSISTENT`) bile bağlantı tek bir worker'a bağlı kaldığı için paylaşılan bir havuz yok. PostgreSQL bağlantı kurulumu (~10ms) kümülatifte ciddi. pgBouncer önünde `pool_mode = transaction` ile bunu çözüyoruz. [auth_query yazısı](/tr/notes/pgbouncer-auth-query) detayı veriyor.

## Redis: paylaşılan, namespace'le izole

Tek bir Redis instance. Her Laravel uygulaması farklı bir `prefix`:

```php
'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:',
    ],
],
```

Prefix bir adlandırma sözleşmesi, bir sınır değil: üç uygulama da aynı instance'a, aynı `database 0` üzerine, `requirepass` ve ACL olmadan bağlanıyor — yani biri diğerinin anahtarlarını okuyabilir. Bunun zorlanması gerekiyorsa her uygulamaya kendi Redis ACL kullanıcısını verin.

Detay [namespace izolasyonu yazısında](/tr/notes/shared-redis-namespace-izolasyonu).

## Queue worker'ları

Supervisor her uygulamanın queue worker'ını ayrı bir process grubu olarak yönetiyor:

```
/etc/supervisor/conf.d/
├── billing-worker.conf
├── notify-worker.conf
└── blog-worker.conf
```

Her dosyada uygulama kendi kullanıcısı ile çalışıyor:

```ini
[program:billing-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/billing/current/artisan queue:work rabbitmq --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/
│   └── 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 gönder (opcache invalidation).
6. Supervisor'ı sadece queue worker'ları için yeniden başlat.

Beşinci adımda bir uyarı var: `USR2` FPM master'ına gidiyor ve *bütün* pool'ları graceful biçimde yeniden yüklüyor — tek bir pool'u yeniden başlatan bir sinyal yok. Yani bir projenin deploy'u bütün projelerin worker'larını geri döndürüyor ve ortak opcache'i boşaltıyor; yukarıdaki izolasyon çalışma zamanında geçerli, deploy yolunda değil.

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/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 uygulama kendi PostgreSQL'ine taşınır — 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.** Control plane'i ayrıca ücretlendiren sağlayıcılarda — EKS cluster başına saatlik $0.10, yani tek node eklenmeden ~$73/ay — 3 küçük node'lu managed k8s, eşdeğer VPS'in 3–5 katı tutuyor. Ücretsiz verenlerde fark kapanıyor: DigitalOcean'da 3 × $12/ay node ücreti VPS parasına yakın.
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.

## Sık sorulanlar

**Bir projenin deploy edilmesi aynı VPS üzerindeki diğerlerini etkiler mi?**

Deploy yolunda evet. USR2 sinyali PHP-FPM master'ına gidiyor ve bütün pool'ları graceful biçimde yeniden yüklüyor; tek bir pool'u yeniden başlatan bir sinyal yok. Yani bir projenin deploy'u bütün projelerin worker'larını geri döndürüyor ve ortak opcache'i boşaltıyor. İzolasyon çalışma zamanına ait bir özellik, deploy anına değil.

**Redis prefix uygulamalar arasında gerçek bir izolasyon mu?**

Hayır. Prefix bir adlandırma sözleşmesi. Üç uygulama da aynı instance'a, aynı database 0 üzerine, requirepass ve ACL olmadan bağlanıyor; yani biri diğerinin anahtarlarını okuyabilir. Bu sınırın zorlanması gerekiyorsa her uygulamaya kendi Redis ACL kullanıcısını verin.

**Tek VPS ne zaman doğru cevap olmaktan çıkar?**

Üç sinyalden biri göründüğünde: CPU sürekli %70 üzerindeyse, tek bir uygulama veritabanında baskın hâle geldiyse ya da coğrafi gecikme şikayetleri geliyorsa. İlk ikisi uygulamaları ayrıştırmak demek; üçüncüsü genelde yalnızca statik asset'leri CDN'e taşımak demek.


## Kaynaklar

- [Amazon EKS pricing: $0.10 per cluster per hour under standard Kubernetes version support](https://aws.amazon.com/eks/pricing/) — Amazon Web Services
- [DigitalOcean Kubernetes pricing: free control plane, basic nodes from $12 per month](https://www.digitalocean.com/pricing/kubernetes) — DigitalOcean
- [PDO connections: persistent connections are cached and reused rather than pooled across processes](https://www.php.net/manual/en/pdo.connections.php) — PHP
- [Supervisor program settings: numprocs, user and stopwaitsecs](https://supervisord.org/configuration.html) — Supervisor
