Xpensio

Kurulum & Operasyon Rehberi

Sistem Gereksinimleri · Docker Compose · Kubernetes · Operasyon Runbook
Müşteri Sistem Yöneticisi & DevOps
Sürüm: v1.0  |  Tarih: 9 Mayıs 2026  |  Gizlilik: Müşteri IT & yetkili personel

İçindekiler

  1. Xpensio — Sistem Gereksinimleri (BYOC)
    1. 1. Donanım Gereksinimleri
    2. 2. İşletim Sistemi & Container Runtime
    3. 3. Network & Firewall
    4. 4. DNS & Domain
    5. 5. Harici Servis Hesapları (Opsiyonel ama Önerilen)
    6. 6. SAP Entegrasyonu Önkoşulları
    7. 7. Identity Provider (HR Sync) — Opsiyonel
    8. 8. Backup & Storage
    9. 9. Monitoring (Opsiyonel ama Önerilen)
    10. 10. Performans Beklentileri (Benchmark)
    11. 11. Air-Gapped / Kapalı Network Kurulumu
    12. 12. Önkoşullar Checklist
    13. Sonraki Adım
  2. Xpensio — BYOC Kurulum (Docker Compose)
    1. Önkoşul Doğrulama
    2. 1. Sunucu Hazırlığı
    3. 2. Xpensio Image'larını İndirme
    4. 3. Konfigürasyon Dosyaları
    5. 4. SSL Sertifika
    6. 5. Kurulum & Başlatma
    7. 6. İlk Kullanım — Setup Wizard
    8. 7. Backup Kurulumu
    9. 8. Doğrulama Testleri
    10. 9. Sorun Giderme
    11. 10. Sonraki Adımlar
  3. Xpensio — Kubernetes Kurulum (BYOC, HA Cluster)
    1. Önkoşullar
    2. 1. Namespace ve Image Pull Secret
    3. 2. Secrets
    4. 3. PostgreSQL
    5. 4. Backend Deployment
    6. 5. Web Deployment
    7. 6. Ingress (TLS dahil)
    8. 7. Network Policies (Önerilen)
    9. 8. Pod Disruption Budget (HA Korunumu)
    10. 9. Doğrulama
    11. 10. Backup (K8s Ortamında)
    12. 11. Güncelleme (Rolling Update)
    13. 12. İzleme & Log
    14. 13. Sorun Giderme
  4. Xpensio — Operasyon Runbook (BYOC)
    1. 1. Günlük Operasyon Checklist
    2. 2. Backup & Restore
    3. 3. Güncelleme (Yeni Versiyon)
    4. 4. Log Yönetimi
    5. 5. Performance Tuning
    6. 6. Güvenlik İşlemleri
    7. 7. Sık Karşılaşılan Sorunlar
    8. 8. Acil Durum Prosedürü
    9. 9. Destek İletişim
    10. 10. Periyodik Bakım

Xpensio — Sistem Gereksinimleri (BYOC)

Bu doküman, müşteri sistem yöneticisinin (SysAdmin/DevOps) BYOC modelinde Xpensio kurulumuna başlamadan önce hazırlaması gereken donanım, network, OS ve harici servis gereksinimlerini tanımlar.

BYOC notu: Xpensio yazılımı Linux + Docker veya Kubernetes çalıştıran herhangi bir ortamda kurulabilir. Müşteri kendi datacenter'ı, herhangi bir TR cloud sağlayıcısı (Turkcell, Veriforce, IBM TR), AB hyperscaler (AWS, Azure, GCP) veya başka bir cloud sağlayıcısı seçebilir. Xpensio bu sağlayıcılarla doğrudan partner sözleşmesi yapmaz; müşteri sözleşmeyi kendi yapar.


1. Donanım Gereksinimleri

1.1 Tek Sunucu (Docker Compose) — Küçük/Orta Müşteri

Kullanıcı sayısı vCPU RAM Disk (SSD) Notlar
1–50 4 8 GB 100 GB POC / pilot
50–250 8 16 GB 200 GB Standart
250–500 16 32 GB 500 GB Yoğun kullanım

Disk hesaplaması: Aylık aktif kullanıcı × 5 MB (fiş foto + DB satırı) × 36 ay (yasal saklama) + %30 büyüme payı.

Örnek: 200 aktif kullanıcı × 5 MB × 36 ay = ~36 GB veri + 100 GB OS/log/temp = 150 GB önerilir, 200 GB güvenli.

1.2 Kubernetes Cluster (HA) — Büyük Kurumsal

Bileşen Pod sayısı Pod başına CPU/RAM Toplam
Backend 3 (HA) 500m CPU / 512Mi RAM 1.5 vCPU / 1.5 GB
Web 2 (HA) 300m CPU / 256Mi RAM 0.6 vCPU / 512 MB
PostgreSQL (primary + 2 replica) 3 2 vCPU / 4 GB 6 vCPU / 12 GB
Ingress controller 2 200m / 256Mi 0.4 vCPU / 512 MB
Toplam (minimum) ~9 vCPU / 15 GB

Önerilen cluster boyutu: 3 worker node × 8 vCPU / 16 GB RAM (HA için).

Storage: 200 GB başlangıç (PVC), büyüme için thin-provision veya genişletilebilir StorageClass.


2. İşletim Sistemi & Container Runtime

Desteklenen OS (Tek Sunucu)

Container Runtime

Kubernetes (Cluster)


3. Network & Firewall

3.1 İç Trafik (cluster içi / sunucu içi)

Servis Port Protokol Açıklama
PostgreSQL 5432 TCP Sadece backend container/pod erişir
Backend API 3001 TCP Web ve mobil → backend
Web (Next.js) 3000 TCP Reverse proxy → web

3.2 Dış Trafik (Internet'ten Müşteri Sunucusuna)

Port Protokol Açıklama Zorunlu?
443 HTTPS Kullanıcı erişimi (web + mobil) Evet
80 HTTP Let's Encrypt ACME challenge + 443'e redirect Evet (cert için)
22 SSH Sistem yönetimi (sadece IT ekip IP'leri) İsteğe bağlı

3.3 Müşteri Sunucusundan Dışarıya (Outbound)

Hedef Port Açıklama Zorunlu?
ghcr.io 443 Docker image pull (Xpensio güncellemeleri) Evet
registry-1.docker.io 443 Base image (postgres, node) pull Evet
Müşteri SAP sunucusu 8000 / 443 SAP RFC / OData API SAP entegrasyonu için
Müşteri AD/LDAP sunucusu 389 / 636 LDAP HR sync Opsiyonel
api.resend.com 443 Mail gönderimi Mail için (alternatif: müşteri SMTP)
fcm.googleapis.com 443 Mobil push notification Opsiyonel
generativelanguage.googleapis.com 443 Gemini AI (OCR + chatbot + fraud) Opsiyonel
letsencrypt.org 443 SSL sertifika yenileme Let's Encrypt kullanılırsa

Kapalı network (air-gapped) müşteriler için: Mail için müşteri kurumsal SMTP sunucusu kullanılır; Gemini AI özellikleri devre dışı bırakılır (OCR Tesseract fallback'e düşer); SSL için müşteri kurum sertifikası kullanılır.

3.4 SAP → Xpensio (PULL Modu için)

SAP ABAP, Xpensio'yu çağıracaksa SAP sunucusundan Xpensio sunucusuna outbound 443 izni gerekir. Eğer SAP DMZ'de değilse, ters tünel (Cloudflare Tunnel, ngrok kurumsal) önerilir.


4. DNS & Domain

4.1 Gerekli DNS Kayıtları

Müşteri kendi domaini altında bir A veya CNAME kaydı oluşturmalıdır:

Kayıt Tip Hedef Açıklama
xpensio.musteri.com.tr A Müşteri sunucu public IP Web + API tek domain altında

Alternatif (ayrık):

Kayıt Tip Hedef
xpensio.musteri.com.tr A Web sunucu IP
xpensio-api.musteri.com.tr A API sunucu IP (farklı sunucu ise)

4.2 SSL/TLS Sertifika

Üç seçenek:

  1. Let's Encrypt (ücretsiz, otomatik yenilenir — internet erişimi gerekli)
  2. Müşteri kurum sertifikası (kapalı network, internal CA)
  3. Ticari sertifika (DigiCert, Comodo, vb.)

Xpensio kurulum scriptleri her üç seçeneği de destekler.


5. Harici Servis Hesapları (Opsiyonel ama Önerilen)

5.1 Mail Gönderimi

Seçenek A — Resend (önerilen, Xpensio default)

Seçenek B — Müşteri SMTP (kurumsal mail sunucusu)

5.2 Mobil Push Notification (Opsiyonel)

5.3 AI Özellikler (Opsiyonel)


6. SAP Entegrasyonu Önkoşulları

6.1 SAP Tarafı (Müşteri SAP BASIS ekibi sağlar)

Detaylar: memory/SAP_SETUP_GUIDE.md, memory/SAP_S4_FI_SPEC.md

6.2 Network Bağlantısı SAP ↔ Xpensio


7. Identity Provider (HR Sync) — Opsiyonel

Xpensio iki şekilde kullanıcı yönetebilir:

Mod A — Standalone (Default, En Basit)

Mod B — Otomatik HR Sync (Önerilen)

Kaynak Önkoşullar
SAP HCM (PA0001, PA0002) RFC user + PA tablo okuma yetkisi
SuccessFactors OAuth client (User API)
Active Directory (LDAP) LDAP servis hesabı, search base, filtre
Azure AD / Entra ID Tenant ID + App registration (Client ID/Secret)
Harici DB DB connection string (read-only user)

Detaylar: memory/integrations/06-AZURE-AD-LDAP.md


8. Backup & Storage

8.1 Backup Hedefi

Müşteri tarafında bir backup hedefi belirlenmelidir:

8.2 Önerilen Politika

8.3 DR / RTO / RPO Hedefleri

Bu hedefler müşterinin seçeceği konfigürasyona göre elde edilir. BYOC modelinde nihai DR/RTO/RPO sorumluluğu müşteri IT'sine aittir; aşağıdaki tablo Xpensio yazılımının desteklediği konfigürasyonları gösterir.

Konfigürasyon RPO (kabul edilebilir veri kaybı) RTO (kurtarma süresi) Notlar
Tek sunucu (Compose) — günlük backup 24 saat 1–2 saat DB restore + container restart; pilot/POC için yeterli
Tek sunucu + saatlik backup 1 saat 1–2 saat Cron sıklığı artırılır; disk dolma riski göz önüne alın
Tek sunucu + PostgreSQL WAL archiving (PITR) 5–15 dakika 1–2 saat WAL'lar S3/NFS'e sürekli arşivlenir; PITR ile point-in-time restore
K8s HA cluster + PostgreSQL streaming replication < 1 dakika (sync) Dakikalar (otomatik failover) Patroni / Crunchy operator; primary çökünce replica promote edilir
K8s HA + cross-region replica < 1 dakika Dakikalar Coğrafi DR; bölge çökmesinde diğer bölge devreye girer

Öneriler (kullanıcı/kritiklik bazında):

Müşteri IT'nin yapması gerekenler:


9. Monitoring (Opsiyonel ama Önerilen)

Xpensio metrik export eder. Müşteri mevcut monitoring stack'ine entegre edebilir:

Müşteri Sistemi Entegrasyon
Prometheus + Grafana /api/v1/metrics endpoint'i scrape edilir
Zabbix HTTP healthcheck + custom UserParameter scriptleri
Datadog / New Relic APM SDK ile (Xpensio Node.js Sentry/OTel destekler)
Splunk / ELK Container log driver → Splunk HEC veya Filebeat

Yoksa: docker logs veya kubectl logs yeter, Xpensio yapılandırılmış JSON log üretir.


10. Performans Beklentileri (Benchmark)

Aşağıdaki değerler Xpensio production ortamından (Hetzner CX31, 4 vCPU / 8 GB) alınan referans benchmarklardır. Müşteri ortamında donanım, network, SAP yanıt süresi ve eş zamanlı kullanıcı sayısına göre değişir.

10.1 API Yanıt Süresi (p95 — 100 eş zamanlı kullanıcı)

Endpoint Türü Tek Sunucu (Compose) K8s HA (3 backend pod)
Login / authentication < 200 ms < 150 ms
Masraf listesi (sayfalı, 50 kayıt) < 300 ms < 200 ms
Masraf detay + fiş < 250 ms < 180 ms
Onay aksiyonu (state geçiş) < 200 ms < 150 ms
Rapor (3 ay, departman bazında) 800 ms – 2 s 500 ms – 1.5 s
Rapor (12 ay, tüm org) 2 – 5 s 1.5 – 3 s
Excel export 3 – 8 s 2 – 5 s

10.2 Özel İşlemler

İşlem Süre Bağımlılık
OCR (Gemini API) 3 – 8 s/fiş İnternet + Gemini API hızı
OCR (Tesseract — air-gapped) 15 – 25 s/fiş CPU bound
SAP FI posting (PUSH) 1 – 3 s/masraf SAP yanıt süresi
SAP HR sync (1.000 kullanıcı) 30 – 60 s SAP yanıt süresi + network
Fraud analizi (Gemini AI) 2 – 4 s/masraf Gemini API
Fraud analizi (kural tabanlı, AI yok) < 50 ms
Mail gönderimi (Resend) < 500 ms (kuyruğa al)
Push notification (FCM) < 200 ms (kuyruğa al)

10.3 Web Sayfa Yüklenme

Metrik Hedef
First Contentful Paint (FCP) < 1.5 s
Time to Interactive (TTI) < 2 s
Largest Contentful Paint (LCP) < 2.5 s

10.4 Throughput Kapasitesi

Konfigürasyon Sürekli istek/saniye (RPS) Eş zamanlı aktif kullanıcı
Tek sunucu — 4 vCPU / 8 GB 50 – 80 RPS 50
Tek sunucu — 8 vCPU / 16 GB 150 – 250 RPS 250
Tek sunucu — 16 vCPU / 32 GB 300 – 500 RPS 500
K8s 3-pod HA — 4 vCPU/pod 500 – 1.000 RPS 1.000+

10.5 Performans Gerçekleşmedi mi?

Beklenenden yavaşsa, sırasıyla kontrol:

  1. Sunucu kaynak kullanımıdocker stats veya kubectl top pods (CPU/RAM doluyorsa upgrade gerek)
  2. PostgreSQL — yavaş sorgu logu (pg_stat_statements); index eksik olabilir
  3. Network latency — SAP, mail, AI servisleri uzaktaysa response time'ı şişirir
  4. Disk I/O — uploads veya DB diski IOPS yetersizse fişler yavaş açılır
  5. Müşteri özel raporlar — büyük tarih aralığı sorgular her zaman yavaşlar; zaman aralığı kısıtlama önerilir

11. Air-Gapped / Kapalı Network Kurulumu

Bazı müşteriler (banka, kamu, savunma) internet erişimi olmayan kapalı network'te kurulum ister. Xpensio bu senaryoyu destekler — bazı sınırlamalarla.

11.1 Kapalı Network'te Devre Dışı Kalan Özellikler

Özellik Etki Çözüm
Gemini AI (OCR + fraud + chatbot) Çalışmaz OCR Tesseract'a düşer; fraud kural tabanlı çalışır; chatbot devre dışı
Firebase push notification Çalışmaz Web push (browser notification) veya devre dışı; mail bildirim alternatif
Resend mail Çalışmaz Müşteri kurumsal SMTP sunucusu
Let's Encrypt SSL Çalışmaz Müşteri kurum CA sertifikası
GHCR'den otomatik image güncelleme Çalışmaz Manuel image transfer (aşağıda)

11.2 Image Teslim Prosedürü (Air-Gapped)

Xpensio image'larını internet erişimli bir bilgisayarda hazırlayıp, kapalı network sunucusuna USB / DVD / dosya transfer aracı ile aktarın.

Adım 1 — Internet erişimli bilgisayardan image çek:

# Backend image
docker pull ghcr.io/xpensio/expense-backend:v1.4.1
docker pull ghcr.io/xpensio/expense-web:v1.4.1
docker pull pgvector/pgvector:pg16
docker pull nginx:1.27-alpine

Adım 2 — Image'ları tarball'a çıkart:

mkdir -p xpensio-airgapped
docker save ghcr.io/xpensio/expense-backend:v1.4.1 | gzip > xpensio-airgapped/backend-v1.4.1.tar.gz
docker save ghcr.io/xpensio/expense-web:v1.4.1     | gzip > xpensio-airgapped/web-v1.4.1.tar.gz
docker save pgvector/pgvector:pg16                 | gzip > xpensio-airgapped/postgres.tar.gz
docker save nginx:1.27-alpine                      | gzip > xpensio-airgapped/nginx.tar.gz

# Toplam boyut: ~1.5 GB
du -sh xpensio-airgapped/

Adım 3 — USB / DVD / SFTP ile kapalı network sunucusuna transfer:

# Örnek: USB'ye kopyala
cp xpensio-airgapped/*.tar.gz /media/usb/

# Veya: SCP ile bastion host üzerinden
scp xpensio-airgapped/*.tar.gz bastion:/tmp/ # → bastion'dan kapalı network'e

Adım 4 — Kapalı network sunucusunda import:

cd /opt/xpensio-airgapped/
gunzip -c backend-v1.4.1.tar.gz  | docker load
gunzip -c web-v1.4.1.tar.gz      | docker load
gunzip -c postgres.tar.gz        | docker load
gunzip -c nginx.tar.gz           | docker load

# Doğrula
docker images | grep -E 'xpensio|pgvector|nginx'

Adım 5 — docker-compose.yml'de image referansını sabitle:

backend:
  image: ghcr.io/xpensio/expense-backend:v1.4.1   # GHCR pull etmez, lokal image kullanılır
  pull_policy: never                              # Kapalı network için kritik

11.3 Güncelleme Prosedürü (Air-Gapped)

Xpensio yeni versiyon yayınladığında müşteri IT:

  1. Yeni versiyon e-mail bildirimi alır (release notes + versiyon no)
  2. İnternet erişimli bilgisayardan yeni image'ı indirir (Adım 1–2 tekrar)
  3. Tarball'ı kapalı network'e transfer eder
  4. docker load + docker compose up -d backend web ile günceller

Önemli: Sıklık önerisi 3 ayda bir veya kritik güvenlik yaması yayınlandığında. Xpensio güvenlik patch SLA'sı (24 saat) air-gapped'te otomatik geçerli değildir — müşteri IT manuel yapmalıdır.

11.4 npm / OS Paket Güncellemeleri (Air-Gapped)

Xpensio Docker image'ı tüm npm bağımlılıklarını içerir, OS paketine ihtiyacı yoktur. Müşteri sadece OS güvenlik yamaları için kendi internal repo mirror'ını kullanır (Red Hat Satellite, Ubuntu APT mirror, vb.).

11.5 Air-Gapped'te SAP Entegrasyonu

SAP ile bağlantı kapalı network içinde kalır (aynı VLAN veya iç DMZ). İnternet erişimi gerekmez. PUSH ve PULL modu her ikisi de çalışır.


12. Önkoşullar Checklist

Kurulum başlamadan önce müşteri tarafından sağlanması gerekenler:

Sunucu / Cluster

Network

Harici Servisler

SAP

Identity

Backup

Operasyon


Sonraki Adım

Tüm önkoşullar hazırsa:


Xpensio — BYOC Kurulum (Docker Compose)

Hedef: Tek sunucu üzerinde Docker Compose ile Xpensio kurulumu. 50–500 kullanıcılı ortam için uygundur. BYOC modeli — müşterinin kendi seçtiği herhangi bir Linux sunucu üzerinde çalışır (kendi DC, TR cloud sağlayıcısı, AB hyperscaler, vb.).

Hedef kitle: Müşteri sistem yöneticisi (Linux + Docker temel bilgisi gerekli). Kurulum müşteri IT tarafından yapılır; Xpensio uzaktan e-mail rehberlik sağlar.

Süre: 2–4 saat (önkoşullar tamamsa).


Önkoşul Doğrulama

Başlamadan önce 02-SISTEM-GEREKSINIMLERI.md checklist'inin tamamlandığından emin olun. Özellikle:


1. Sunucu Hazırlığı

1.1 OS Güncelleme (Ubuntu 22.04 örneği)

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl ca-certificates gnupg lsb-release ufw fail2ban

1.2 Docker Kurulumu

# Docker resmi repo
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Servisi başlat ve sistem açılışında otomatik başlat
sudo systemctl enable --now docker

# Doğrula
docker --version          # Docker version 24.x.x
docker compose version    # Docker Compose version v2.x.x

1.3 Firewall (UFW)

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp     # SSH (sadece IT IP'leriyle sınırlandırın)
sudo ufw allow 80/tcp     # HTTP (Let's Encrypt + 443 redirect)
sudo ufw allow 443/tcp    # HTTPS
sudo ufw enable
sudo ufw status verbose

1.4 Dizin Yapısı

sudo mkdir -p /opt/xpensio/{config,data/postgres,data/uploads,logs,backups}
sudo chown -R $USER:$USER /opt/xpensio
cd /opt/xpensio

2. Xpensio Image'larını İndirme

2.1 GHCR Login

Xpensio size ghcr_pat_xxxxxxxxxxxx formatında bir token verecek.

echo "ghcr_pat_xxxxxxxxxxxx" | docker login ghcr.io -u xpensio-customer --password-stdin

Güvenlik: Bu token yalnızca read-only image pull yetkisine sahiptir. Token'ı güvenli saklayın (vault, secret manager).

2.2 Image Pull

docker pull ghcr.io/xpensio/expense-backend:latest
docker pull ghcr.io/xpensio/expense-web:latest
docker pull pgvector/pgvector:pg16

Versiyon sabitleme (önerilen): Production'da :latest yerine sabit versiyon kullanın (örn. :v1.4.1). Xpensio teslimatla birlikte size kurulu versiyon numarasını verir.


3. Konfigürasyon Dosyaları

3.1 .env Dosyası

cd /opt/xpensio/config
cat > .env <<'EOF'
# =====================================================
# XPENSIO ENVIRONMENT — PRODUCTION
# =====================================================
# Bu dosyayı GİT'e commit etmeyin. İzinler 600 olmalı.

# --- POSTGRESQL ---
POSTGRES_DB=expense_management
POSTGRES_USER=xpensio_app
POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD_32_CHARS

# --- BACKEND (NestJS) ---
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://xpensio_app:CHANGE_ME_STRONG_PASSWORD_32_CHARS@postgres:5432/expense_management?schema=public

# Encryption key for sensitive data (SAP credentials, etc.) — 32 byte hex (64 char)
# Generate: openssl rand -hex 32
ENCRYPTION_KEY=CHANGE_ME_64_HEX_CHARS_FROM_OPENSSL_RAND_HEX_32

# JWT signing key — minimum 32 chars random
# Generate: openssl rand -base64 48
JWT_SECRET=CHANGE_ME_RANDOM_BASE64_AT_LEAST_32_CHARS
JWT_EXPIRATION=15m

# --- WEB ---
WEB_URL=https://xpensio.musteri.com.tr
NEXT_PUBLIC_API_URL=https://xpensio.musteri.com.tr/api/v1
APP_BASE_URL=https://xpensio.musteri.com.tr
CORS_ORIGINS=https://xpensio.musteri.com.tr

# --- SAP (kurulum sonrası web setup wizard'dan da girilebilir) ---
SAP_TYPE=ECC
SAP_BASE_URL=https://sap.musteri.local:8000
SAP_USERNAME=XPENSIO_RFC
SAP_PASSWORD=SAP_RFC_PASSWORD
SAP_CLIENT=100
SAP_COMPANY_CODE=1000

# --- IDENTITY (HR Sync) ---
# Seçenekler: NONE | SAP_HCM | LDAP | AZURE_AD | EXTERNAL_DB | MOCK
IDENTITY_PROVIDER=NONE

# --- MAIL ---
# Seçenek 1: Resend
RESEND_API_KEY=re_xxxxxxxxxxxxx

# Seçenek 2: Müşteri SMTP (Resend yerine)
# MAIL_HOST=smtp.musteri.local
# MAIL_PORT=587
# MAIL_USER=xpensio@musteri.local
# MAIL_PASS=xxx
# MAIL_FROM=xpensio@musteri.local

# --- AI (OPSİYONEL) ---
# Boş bırakırsanız OCR Tesseract'a düşer, chatbot devre dışı.
GEMINI_API_KEY=

# --- PUSH NOTIFICATION (OPSİYONEL) ---
# Firebase service account JSON dosyası /opt/xpensio/config/firebase-key.json'a koyulmalı
FIREBASE_CREDENTIALS_PATH=/run/secrets/firebase-key.json
EOF

# Dosya izinlerini sıkılaştır
chmod 600 /opt/xpensio/config/.env

Önemli — şifre üretme:

# POSTGRES_PASSWORD ve JWT_SECRET için:
openssl rand -base64 32

# ENCRYPTION_KEY için (tam 64 hex char olmalı):
openssl rand -hex 32

ENCRYPTION_KEY'i kaybetmeyin! Bu key ile DB'deki SAP şifreleri şifrelenir. Kaybedilirse tüm SAP yapılandırması kaybolur.

3.2 docker-compose.yml

cat > /opt/xpensio/docker-compose.yml <<'EOF'
version: '3.8'

services:
  postgres:
    image: pgvector/pgvector:pg16
    container_name: xpensio_db
    restart: unless-stopped
    env_file: ./config/.env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
      - ./backups:/backups
    networks:
      - xpensio
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 4G

  backend:
    image: ghcr.io/xpensio/expense-backend:latest
    container_name: xpensio_backend
    restart: unless-stopped
    env_file: ./config/.env
    volumes:
      - ./data/uploads:/app/apps/backend/uploads
      - ./config/firebase-key.json:/run/secrets/firebase-key.json:ro
    networks:
      - xpensio
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ['CMD-SHELL', 'wget -q -O- http://localhost:3001/api/v1/health || exit 1']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          memory: 2G

  web:
    image: ghcr.io/xpensio/expense-web:latest
    container_name: xpensio_web
    restart: unless-stopped
    env_file: ./config/.env
    networks:
      - xpensio
    depends_on:
      - backend
    deploy:
      resources:
        limits:
          memory: 512M

  nginx:
    image: nginx:1.27-alpine
    container_name: xpensio_nginx
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./config/ssl:/etc/nginx/ssl:ro
      - ./logs/nginx:/var/log/nginx
    networks:
      - xpensio
    depends_on:
      - web
      - backend

networks:
  xpensio:
    driver: bridge
EOF

3.3 NGINX Reverse Proxy Konfigürasyonu

cat > /opt/xpensio/config/nginx.conf <<'EOF'
user nginx;
worker_processes auto;
events { worker_connections 1024; }

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;

    client_max_body_size 25M;  # Fiş upload (max 20MB)

    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript;

    # Log format
    log_format combined_real '$remote_addr - $remote_user [$time_local] '
                             '"$request" $status $body_bytes_sent '
                             '"$http_referer" "$http_user_agent" '
                             'rt=$request_time uct="$upstream_connect_time"';

    access_log /var/log/nginx/access.log combined_real;
    error_log /var/log/nginx/error.log warn;

    upstream xpensio_backend { server backend:3001; }
    upstream xpensio_web     { server web:3000; }

    # HTTP → HTTPS yönlendirme
    server {
        listen 80;
        server_name xpensio.musteri.com.tr;

        # Let's Encrypt için ACME challenge (opsiyonel)
        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location / {
            return 301 https://$host$request_uri;
        }
    }

    # HTTPS
    server {
        listen 443 ssl http2;
        server_name xpensio.musteri.com.tr;

        ssl_certificate     /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;

        # Security headers
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Frame-Options "DENY" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;

        # API
        location /api/ {
            proxy_pass http://xpensio_backend;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_read_timeout 120s;
        }

        # Web (Next.js)
        location / {
            proxy_pass http://xpensio_web;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
        }
    }
}
EOF

4. SSL Sertifika

Seçenek A — Let's Encrypt (İnternet erişimli ortam)

sudo apt install -y certbot

# Önce nginx olmadan certbot standalone modda çalıştır
sudo certbot certonly --standalone -d xpensio.musteri.com.tr \
  --email it@musteri.com.tr --agree-tos --no-eff-email

# Sertifikaları nginx'in bekleyebileceği yere kopyala
sudo mkdir -p /opt/xpensio/config/ssl
sudo cp /etc/letsencrypt/live/xpensio.musteri.com.tr/fullchain.pem /opt/xpensio/config/ssl/
sudo cp /etc/letsencrypt/live/xpensio.musteri.com.tr/privkey.pem   /opt/xpensio/config/ssl/

# Otomatik yenileme cron (her 12 saatte bir kontrol eder, gerekirse yeniler)
echo "0 0,12 * * * root certbot renew --quiet --post-hook 'docker restart xpensio_nginx'" | \
  sudo tee /etc/cron.d/certbot-renew

Seçenek B — Müşteri Kurum Sertifikası

# Müşteri sertifika ve private key dosyalarını /opt/xpensio/config/ssl/ altına koyar:
# - fullchain.pem  (sertifika + intermediate chain)
# - privkey.pem    (private key)

sudo chmod 600 /opt/xpensio/config/ssl/privkey.pem
sudo chmod 644 /opt/xpensio/config/ssl/fullchain.pem

5. Kurulum & Başlatma

5.1 İlk Başlatma

cd /opt/xpensio
docker compose up -d

# Logları izle (DB migration ve seed çalışacak)
docker compose logs -f backend

Backend başlangıçta otomatik:

  1. Prisma migration çalıştırır (tüm DB şeması oluşturulur)
  2. Seed verisi yükler (rol şablonları, default kategoriler)
  3. NestJS başlar (port 3001)

5.2 Sağlık Kontrolü

# Tüm container'lar healthy olmalı
docker compose ps

# Backend healthcheck
curl https://xpensio.musteri.com.tr/api/v1/health
# Beklenen: {"status":"ok","timestamp":"...","database":"connected"}

# Web erişim
curl -I https://xpensio.musteri.com.tr
# Beklenen: HTTP/2 200

6. İlk Kullanım — Setup Wizard

6.1 Süper Admin Oluşturma

İlk kurulumda Xpensio veritabanında tek bir super admin kullanıcı yaratılmalıdır. Bu kullanıcı tüm tenantları yönetir.

docker exec -it xpensio_backend node -e "
const bcrypt = require('bcrypt');
bcrypt.hash('CHANGE_ME_STRONG_PASSWORD', 10).then(h => console.log(h));
"

Bu komut bir bcrypt hash döner. Şimdi DB'ye ekleyin:

docker exec -it xpensio_db psql -U xpensio_app -d expense_management <<EOF
INSERT INTO users (id, email, password, first_name, last_name, role, is_super_admin, created_at, updated_at)
VALUES (
  gen_random_uuid()::text,
  'admin@musteri.com.tr',
  '\$2b\$10\$BURAYA_BCRYPT_HASH_GELECEK',
  'Sistem',
  'Yöneticisi',
  'ADMIN',
  true,
  NOW(),
  NOW()
);
EOF

Alternatif (önerilen): Xpensio destek ekibi ilk kurulumda bu adımı uzaktan yapar.

6.2 Setup Wizard'a Giriş

  1. Tarayıcıda: https://xpensio.musteri.com.tr
  2. admin@musteri.com.tr ile giriş yap
  3. Admin → Setup Wizard menüsünden 4 adımlı kurulum:
    • Adım 1: Organizasyon bilgisi (şirket adı, logo, vergi numarası)
    • Adım 2: Şirket(ler) ve cost center yapısı
    • Adım 3: ERP konfigürasyonu (SAP bağlantı + GL haritası)
    • Adım 4: Identity provider (HR sync — opsiyonel)

Detaylı setup adımları: memory/SAP_SETUP_GUIDE.md

6.3 İlk Kullanıcıları Ekleme


7. Backup Kurulumu

# Backup script kopyala
cat > /opt/xpensio/scripts/daily-backup.sh <<'EOF'
#!/bin/bash
set -e
BACKUP_DIR="/opt/xpensio/backups"
DATE=$(date +%Y-%m-%d_%H-%M)
KEEP_DAYS=30

# PostgreSQL dump
docker exec xpensio_db pg_dump -U xpensio_app expense_management \
  | gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz"

# Uploads tarball
tar -czf "${BACKUP_DIR}/uploads_${DATE}.tar.gz" -C /opt/xpensio/data uploads

# Eskileri temizle
find "${BACKUP_DIR}" -name "*.gz" -mtime +${KEEP_DAYS} -delete

# Müşteri backup hedefine sync (örnek: NFS, S3)
# rsync -av "${BACKUP_DIR}/" /mnt/nfs-backup/xpensio/
# aws s3 sync "${BACKUP_DIR}/" s3://musteri-backup/xpensio/

echo "[$(date)] Backup OK"
EOF

chmod +x /opt/xpensio/scripts/daily-backup.sh

# Cron — her gece 02:00
echo "0 2 * * * root /opt/xpensio/scripts/daily-backup.sh >> /opt/xpensio/logs/backup.log 2>&1" | \
  sudo tee /etc/cron.d/xpensio-backup

8. Doğrulama Testleri

Aşağıdakiler tamamlandıysa kurulum başarılıdır:


9. Sorun Giderme

Backend başlamıyor

docker compose logs backend | tail -50

Sık nedenler:

NGINX 502 Bad Gateway

docker compose logs nginx
docker compose ps  # backend ve web "healthy" mi?

Sık nedenler: Backend henüz hazır değil (start_period 60sn) veya healthcheck başarısız.

SSL hatası

openssl x509 -in /opt/xpensio/config/ssl/fullchain.pem -text -noout | head -20

Kontrol: Sertifika domain'i xpensio.musteri.com.tr ile eşleşiyor mu? Süresi dolmuş mu?

SAP bağlantı hatası

docker exec -it xpensio_backend wget -O- --timeout=10 http://sap.musteri.local:8000/sap/bc/ping

Kontrol: Sunucudan SAP'a network erişimi var mı? Firewall, VLAN, NAT?

Daha fazla → 05-OPERASYON-RUNBOOK.md


10. Sonraki Adımlar


Xpensio — Kubernetes Kurulum (BYOC, HA Cluster)

Hedef: Mevcut müşteri Kubernetes cluster'ında Xpensio'yu yüksek erişilebilirlik (HA) ile çalıştırmak. BYOC modeli — müşterinin kendi K8s cluster'ında namespace olarak çalışır. 500+ kullanıcı, kurumsal müşteri için.

Hedef kitle: Müşteri DevOps / Platform ekibi (Kubernetes deneyimi gerekli). Kurulum müşteri tarafından yapılır; Xpensio uzaktan e-mail rehberlik sağlar.

Süre: 1–2 gün (cluster hazırsa).


Önkoşullar


1. Namespace ve Image Pull Secret

# Namespace
kubectl create namespace xpensio

# GHCR pull secret (Xpensio image'larını çekmek için)
kubectl create secret docker-registry ghcr-pull-secret \
  --namespace=xpensio \
  --docker-server=ghcr.io \
  --docker-username=xpensio-customer \
  --docker-password='ghcr_pat_xxxxxxxxxxxxx' \
  --docker-email='it@musteri.com.tr'

2. Secrets

2.1 Database Secret

# Güçlü şifreler üret
DB_PASSWORD=$(openssl rand -base64 32)
JWT_SECRET=$(openssl rand -base64 48)
ENCRYPTION_KEY=$(openssl rand -hex 32)

# Saklayın! Özellikle ENCRYPTION_KEY kaybedilirse SAP yapılandırması erişilemez.
echo "DB_PASSWORD=${DB_PASSWORD}"
echo "JWT_SECRET=${JWT_SECRET}"
echo "ENCRYPTION_KEY=${ENCRYPTION_KEY}"

2.2 Secret Manifest

# secrets.yml
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
  namespace: xpensio
type: Opaque
stringData:
  username: xpensio_app
  password: REPLACE_WITH_DB_PASSWORD
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: xpensio
type: Opaque
stringData:
  database-url: "postgresql://xpensio_app:REPLACE_WITH_DB_PASSWORD@postgres:5432/expense_management?schema=public"
  jwt-secret: REPLACE_WITH_JWT_SECRET
  encryption-key: REPLACE_WITH_ENCRYPTION_KEY
  resend-api-key: re_xxxxxxxxxx           # opsiyonel
  gemini-api-key: ""                       # opsiyonel
---
apiVersion: v1
kind: Secret
metadata:
  name: sap-secret
  namespace: xpensio
type: Opaque
stringData:
  username: XPENSIO_RFC
  password: REPLACE_WITH_SAP_PASSWORD
# Secret değerlerini düzenle, sonra apply et
kubectl apply -f secrets.yml

2.3 ConfigMap (Hassas olmayan konfigürasyon)

# configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: xpensio
data:
  NODE_ENV: "production"
  PORT: "3001"
  SAP_TYPE: "ECC"
  SAP_BASE_URL: "https://sap.musteri.local:8000"
  SAP_CLIENT: "100"
  SAP_COMPANY_CODE: "1000"
  IDENTITY_PROVIDER: "NONE"
  WEB_URL: "https://xpensio.musteri.com.tr"
  APP_BASE_URL: "https://xpensio.musteri.com.tr"
  CORS_ORIGINS: "https://xpensio.musteri.com.tr"
  NEXT_PUBLIC_API_URL: "https://xpensio.musteri.com.tr/api/v1"
kubectl apply -f configmap.yml

3. PostgreSQL

Not: Üretim ortamında harici yönetilen PostgreSQL önerilir (kurumsal HA: Patroni, Crunchy PostgreSQL Operator, AWS RDS, Azure Database). Aşağıdaki manifest sadece basit/POC için tek replica gösterir.

3.1 PVC + Deployment + Service

# postgres.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: xpensio
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 100Gi
  # storageClassName: <müşteri storage class>
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: xpensio
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels: {app: postgres}
  template:
    metadata:
      labels: {app: postgres}
    spec:
      containers:
        - name: postgres
          image: pgvector/pgvector:pg16
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: expense_management
            - name: POSTGRES_USER
              valueFrom: {secretKeyRef: {name: db-secret, key: username}}
            - name: POSTGRES_PASSWORD
              valueFrom: {secretKeyRef: {name: db-secret, key: password}}
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec: {command: [pg_isready, -U, xpensio_app]}
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests: {memory: 1Gi, cpu: 500m}
            limits:   {memory: 4Gi, cpu: 2000m}
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: xpensio
spec:
  selector: {app: postgres}
  ports:
    - port: 5432
      targetPort: 5432
kubectl apply -f postgres.yml

# DB hazır olana kadar bekle
kubectl wait --for=condition=ready pod -l app=postgres -n xpensio --timeout=120s

3.2 Harici PostgreSQL Kullanımı (Önerilen)

Eğer kurumsal yönetilen DB kullanılacaksa:


4. Backend Deployment

4.1 Backend Pod Sayısı (Horizontal Pod Autoscaling)

# backend.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: xpensio
spec:
  replicas: 3
  selector:
    matchLabels: {app: backend}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels: {app: backend}
    spec:
      imagePullSecrets:
        - name: ghcr-pull-secret
      containers:
        - name: backend
          image: ghcr.io/xpensio/expense-backend:v1.4.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3001
          envFrom:
            - configMapRef: {name: app-config}
          env:
            - name: DATABASE_URL
              valueFrom: {secretKeyRef: {name: app-secret, key: database-url}}
            - name: JWT_SECRET
              valueFrom: {secretKeyRef: {name: app-secret, key: jwt-secret}}
            - name: ENCRYPTION_KEY
              valueFrom: {secretKeyRef: {name: app-secret, key: encryption-key}}
            - name: RESEND_API_KEY
              valueFrom: {secretKeyRef: {name: app-secret, key: resend-api-key}}
            - name: GEMINI_API_KEY
              valueFrom: {secretKeyRef: {name: app-secret, key: gemini-api-key}}
            - name: SAP_USERNAME
              valueFrom: {secretKeyRef: {name: sap-secret, key: username}}
            - name: SAP_PASSWORD
              valueFrom: {secretKeyRef: {name: sap-secret, key: password}}
          volumeMounts:
            - name: uploads
              mountPath: /app/apps/backend/uploads
          readinessProbe:
            httpGet: {path: /api/v1/health, port: 3001}
            initialDelaySeconds: 30
            periodSeconds: 10
          livenessProbe:
            httpGet: {path: /api/v1/health, port: 3001}
            initialDelaySeconds: 60
            periodSeconds: 30
          resources:
            requests: {memory: 512Mi, cpu: 250m}
            limits:   {memory: 2Gi,   cpu: 1000m}
      volumes:
        - name: uploads
          persistentVolumeClaim:
            claimName: uploads-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: uploads-pvc
  namespace: xpensio
spec:
  accessModes: [ReadWriteMany]    # Çoklu pod erişimi için RWX gerekli
  resources:
    requests: {storage: 200Gi}
  # storageClassName: <NFS, CephFS, EFS gibi RWX destekleyen>
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: xpensio
spec:
  selector: {app: backend}
  ports:
    - port: 3001
      targetPort: 3001

Önemli — Uploads PVC: Backend pod'ları arası fiş dosyalarını paylaşmak için ReadWriteMany (RWX) destekleyen storage gerekir (NFS, CephFS, AWS EFS, Azure Files). Eğer sadece RWO varsa, fiş upload'larını object storage'a (MinIO, S3) taşımak gerekir — bu özel ayar Xpensio destek ile koordine edilmelidir.

4.2 HPA (Horizontal Pod Autoscaler)

# hpa-backend.yml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: backend-hpa
  namespace: xpensio
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: backend
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: {type: Utilization, averageUtilization: 70}
    - type: Resource
      resource:
        name: memory
        target: {type: Utilization, averageUtilization: 80}
kubectl apply -f backend.yml -f hpa-backend.yml
kubectl rollout status deployment/backend -n xpensio

5. Web Deployment

# web.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: xpensio
spec:
  replicas: 2
  selector:
    matchLabels: {app: web}
  template:
    metadata:
      labels: {app: web}
    spec:
      imagePullSecrets:
        - name: ghcr-pull-secret
      containers:
        - name: web
          image: ghcr.io/xpensio/expense-web:v1.4.1
          ports:
            - containerPort: 3000
          envFrom:
            - configMapRef: {name: app-config}
          env:
            - name: GEMINI_API_KEY
              valueFrom: {secretKeyRef: {name: app-secret, key: gemini-api-key}}
          readinessProbe:
            httpGet: {path: /, port: 3000}
            initialDelaySeconds: 15
            periodSeconds: 10
          resources:
            requests: {memory: 256Mi, cpu: 100m}
            limits:   {memory: 512Mi, cpu: 500m}
---
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: xpensio
spec:
  selector: {app: web}
  ports:
    - port: 3000
      targetPort: 3000
kubectl apply -f web.yml
kubectl rollout status deployment/web -n xpensio

6. Ingress (TLS dahil)

6.1 cert-manager ile Let's Encrypt

# cluster-issuer.yml (cluster genelinde tek sefer)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: it@musteri.com.tr
    privateKeySecretRef: {name: letsencrypt-prod-key}
    solvers:
      - http01:
          ingress: {class: nginx}

6.2 Ingress Manifest

# ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: xpensio-ingress
  namespace: xpensio
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "25m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts: [xpensio.musteri.com.tr]
      secretName: xpensio-tls
  rules:
    - host: xpensio.musteri.com.tr
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: backend
                port: {number: 3001}
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port: {number: 3000}
kubectl apply -f cluster-issuer.yml -f ingress.yml

# TLS sertifika hazır olana kadar bekle (~1-2 dakika)
kubectl get certificate -n xpensio -w

7. Network Policies (Önerilen)

# netpol.yml — sadece izin verilen trafiğe geçit
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-policy
  namespace: xpensio
spec:
  podSelector:
    matchLabels: {app: backend}
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - podSelector: {matchLabels: {app: web}}
        - namespaceSelector:
            matchLabels: {name: ingress-nginx}
      ports:
        - {protocol: TCP, port: 3001}
  egress:
    # PostgreSQL
    - to:
        - podSelector: {matchLabels: {app: postgres}}
      ports: [{protocol: TCP, port: 5432}]
    # DNS
    - to:
        - namespaceSelector: {}
          podSelector: {matchLabels: {k8s-app: kube-dns}}
      ports: [{protocol: UDP, port: 53}]
    # SAP, harici servisler — müşteri ortamına göre güncelleyin
    - to:
        - ipBlock: {cidr: 10.0.0.0/8}    # SAP iç IP aralığı
      ports: [{protocol: TCP, port: 8000}]

8. Pod Disruption Budget (HA Korunumu)

# pdb.yml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: backend-pdb
  namespace: xpensio
spec:
  minAvailable: 2          # 3 replica'dan en az 2'si her zaman ayakta kalsın
  selector:
    matchLabels: {app: backend}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
  namespace: xpensio
spec:
  minAvailable: 1
  selector:
    matchLabels: {app: web}

9. Doğrulama

# Tüm pod'lar Running + Ready
kubectl get pods -n xpensio

# Servisler
kubectl get svc -n xpensio

# Ingress IP/hostname
kubectl get ingress -n xpensio

# Backend log
kubectl logs -n xpensio -l app=backend --tail=50

# Healthcheck
curl https://xpensio.musteri.com.tr/api/v1/health

# Web
curl -I https://xpensio.musteri.com.tr

İlk kullanım (super admin oluşturma + setup wizard) için: 03-ON-PREMISE-KURULUM.md §6


10. Backup (K8s Ortamında)

Seçenek A — Velero (Önerilen)

Cluster genelinde tutarlı backup için:

# Velero kurulu olduğu varsayılarak
velero schedule create xpensio-daily \
  --schedule="0 2 * * *" \
  --include-namespaces xpensio \
  --ttl 720h0m0s   # 30 gün

Seçenek B — pg_dump + S3

# cronjob-backup.yml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup
  namespace: xpensio
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: pgdump
              image: pgvector/pgvector:pg16
              command:
                - /bin/sh
                - -c
                - |
                  DATE=$(date +%Y-%m-%d_%H-%M)
                  PGPASSWORD=$DB_PASSWORD pg_dump -h postgres -U xpensio_app expense_management | \
                    gzip > /backup/db_${DATE}.sql.gz
                  # S3'e yükle
                  # aws s3 cp /backup/db_${DATE}.sql.gz s3://musteri-backup/xpensio/
              env:
                - name: DB_PASSWORD
                  valueFrom: {secretKeyRef: {name: db-secret, key: password}}
              volumeMounts:
                - name: backup
                  mountPath: /backup
          volumes:
            - name: backup
              persistentVolumeClaim:
                claimName: backup-pvc

11. Güncelleme (Rolling Update)

# Yeni versiyon image'ı (Xpensio yayınladığında)
kubectl set image deployment/backend -n xpensio backend=ghcr.io/xpensio/expense-backend:v1.4.2
kubectl set image deployment/web     -n xpensio web=ghcr.io/xpensio/expense-web:v1.4.2

# Rollout izle
kubectl rollout status deployment/backend -n xpensio
kubectl rollout status deployment/web     -n xpensio

# Sorun varsa rollback
kubectl rollout undo deployment/backend -n xpensio

DB migration: Yeni versiyon backend pod ayağa kalktığında otomatik prisma migrate deploy çalıştırır. Migration uyumsuzluğu için her major upgrade öncesi Xpensio destek mühendisi koordinasyonu gereklidir.


12. İzleme & Log

Prometheus Metrik

Backend /api/v1/metrics endpoint'ini expose eder. Service monitor:

# servicemonitor.yml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: backend-metrics
  namespace: xpensio
spec:
  selector:
    matchLabels: {app: backend}
  endpoints:
    - port: http
      path: /api/v1/metrics
      interval: 30s

Log Aggregation

Container log'ları otomatik stdout/stderr'e gider. Müşteri log altyapısı (Loki, ELK, Splunk) bunları toplar.

# Manuel
kubectl logs -n xpensio -l app=backend --tail=100 -f

13. Sorun Giderme

# Pod CrashLoopBackOff
kubectl describe pod -n xpensio <pod-name>
kubectl logs -n xpensio <pod-name> --previous

# Image pull hatası
kubectl describe pod -n xpensio <pod-name> | grep -i 'pull\|secret'
# Çözüm: ghcr-pull-secret doğru namespace'te mi?

# Backend ↔ DB bağlantı hatası
kubectl exec -n xpensio -it deploy/backend -- sh -c 'wget -O- postgres:5432'

# Ingress 502
kubectl get endpoints -n xpensio
# Backend service'in endpoint listesi boşsa: backend pod'lar henüz hazır değil

# TLS sertifika alınmadı
kubectl describe certificate -n xpensio xpensio-tls
kubectl describe order,challenge -n xpensio

Daha fazla → 05-OPERASYON-RUNBOOK.md


Xpensio — Operasyon Runbook (BYOC)

Müşteri sistem yöneticisinin günlük operasyon, izleme, backup, güncelleme ve sorun giderme için başvuracağı kılavuz.

BYOC notu: Operasyonel sorumluluk (sunucu sağlığı, backup, monitoring, OS patch, SSL yenileme, 7/24 müdahale) müşteri IT'sindedir. Xpensio yazılım için e-mail destek sağlar (8 iş saati yanıt SLA), sunucu/altyapı operasyonuna müdahale etmez.


1. Günlük Operasyon Checklist

Kontrol Sıklık Komut / Yöntem Beklenen
Container'lar çalışıyor mu Günlük docker compose ps veya kubectl get pods -n xpensio Tümü Up (healthy) / Running
Disk doluluğu Günlük df -h /opt/xpensio < %80
Backup başarılı Günlük ls -la /opt/xpensio/backups/ | tail Bugünkü dosya var
Backend response Günlük curl -f https://xpensio.musteri.com.tr/api/v1/health {"status":"ok"}
Memory kullanımı Haftalık docker stats Limit'in %70'i altında
Log boyutu Haftalık du -sh /var/lib/docker/containers/*/*-json.log < 1 GB / container
SSL süresi Aylık openssl x509 -in fullchain.pem -noout -enddate > 30 gün kalmış

2. Backup & Restore

2.1 Manuel Backup

# PostgreSQL dump
docker exec xpensio_db pg_dump -U xpensio_app expense_management \
  | gzip > /opt/xpensio/backups/manual_$(date +%Y%m%d_%H%M).sql.gz

# Uploads
tar -czf /opt/xpensio/backups/uploads_$(date +%Y%m%d_%H%M).tar.gz \
  -C /opt/xpensio/data uploads

2.2 Restore Prosedürü

ÖNEMLİ: Restore yapmadan önce mevcut DB'yi yedekleyin (geri dönüş için).

# 1. Backend'i durdur (uploads ve DB ile uğraşırken)
docker compose stop backend web

# 2. PostgreSQL Restore
gunzip -c /opt/xpensio/backups/db_2026-05-09_02-00.sql.gz | \
  docker exec -i xpensio_db psql -U xpensio_app expense_management

# 3. Uploads Restore
tar -xzf /opt/xpensio/backups/uploads_2026-05-09_02-00.tar.gz \
  -C /opt/xpensio/data

# 4. Backend'i başlat
docker compose start backend web

# 5. Doğrula
docker compose ps
curl https://xpensio.musteri.com.tr/api/v1/health

2.3 Backup Doğrulama Testi (3 ayda bir)

  1. Test sunucusuna en son backup'ı restore edin
  2. Web'e giriş, raporlar açılıyor mu kontrol edin
  3. SAP test bağlantısı çalışıyor mu kontrol edin
  4. Sonuç dokümante edilir, başarısızsa Xpensio destek bilgilendirilir

3. Güncelleme (Yeni Versiyon)

Xpensio yeni versiyon yayınladığında müşteriye e-mail ile bildirim gönderir:

3.1 Docker Compose Ortamında Güncelleme

cd /opt/xpensio

# 1. Backup al (zorunlu!)
./scripts/daily-backup.sh

# 2. Yeni image'ları çek
docker pull ghcr.io/xpensio/expense-backend:v1.4.2
docker pull ghcr.io/xpensio/expense-web:v1.4.2

# 3. docker-compose.yml'de versiyon güncelle (latest yerine sabit kullanılıyorsa)
sed -i 's|expense-backend:v1.4.1|expense-backend:v1.4.2|g' docker-compose.yml
sed -i 's|expense-web:v1.4.1|expense-web:v1.4.2|g' docker-compose.yml

# 4. Önce backend'i recreate (DB migration otomatik çalışır)
docker compose up -d backend
docker compose logs -f backend  # "Migration completed" mesajını bekle

# 5. Web'i recreate
docker compose up -d web

# 6. Doğrulama
curl https://xpensio.musteri.com.tr/api/v1/health
docker compose ps

3.2 Kubernetes Ortamında Güncelleme

# 1. Backup
kubectl exec -n xpensio postgres-0 -- pg_dump -U xpensio_app expense_management | \
  gzip > db_pre_upgrade_$(date +%Y%m%d).sql.gz

# 2. Image güncelle (rolling update)
kubectl set image deployment/backend -n xpensio backend=ghcr.io/xpensio/expense-backend:v1.4.2

# 3. Migration için ilk pod'u izle
kubectl logs -n xpensio -l app=backend -f --tail=50

# 4. Web güncelle
kubectl set image deployment/web -n xpensio web=ghcr.io/xpensio/expense-web:v1.4.2

# 5. Rollout izle
kubectl rollout status deployment/backend -n xpensio
kubectl rollout status deployment/web -n xpensio

3.3 Geri Alma (Rollback)

Docker Compose:

# docker-compose.yml'de eski versiyona dön
sed -i 's|expense-backend:v1.4.2|expense-backend:v1.4.1|g' docker-compose.yml
docker compose up -d backend web

# DB migration geri alındıysa: DB restore gerekebilir!

Kubernetes:

kubectl rollout undo deployment/backend -n xpensio
kubectl rollout undo deployment/web -n xpensio

DİKKAT: Major versiyon (v1.x → v2.x) güncellemelerinde DB schema değişiklikleri geri alınamaz olabilir. Bu durumda DB restore zorunludur. Major upgrade öncesi mutlaka Xpensio destek ile koordinasyon yapın.


4. Log Yönetimi

4.1 Log Konumları

Bileşen Konum
Backend (NestJS) docker logs xpensio_backend veya kubectl logs ...
Web (Next.js) docker logs xpensio_web
PostgreSQL docker logs xpensio_db
NGINX /opt/xpensio/logs/nginx/access.log, error.log
Backup cron /opt/xpensio/logs/backup.log

4.2 Log Rotation

Docker default JSON log driver sınırsız yazar — disk dolar. Sınırlandırın:

# docker-compose.yml — her servise ekle:
    logging:
      driver: json-file
      options:
        max-size: "100m"
        max-file: "5"

4.3 Log Seviyesi Değiştirme

Backend default info seviyesinde log basar. Debug için:

# .env
LOG_LEVEL=debug   # info | warn | error

Restart sonrası geçerli.


5. Performance Tuning

5.1 PostgreSQL

Standart yapılandırma 4 GB RAM'e göre. Daha büyük sunucu için:

docker exec -it xpensio_db psql -U xpensio_app -d expense_management -c "
  ALTER SYSTEM SET shared_buffers = '2GB';
  ALTER SYSTEM SET effective_cache_size = '6GB';
  ALTER SYSTEM SET maintenance_work_mem = '512MB';
  ALTER SYSTEM SET work_mem = '32MB';
  ALTER SYSTEM SET max_connections = 200;
"
docker restart xpensio_db

5.2 Backend Worker Sayısı

Default tek Node.js process. CPU sayısı kadar process için (Docker Compose):

# Yeni service ekle, port farklı, NGINX upstream'e ekle
backend2:
  <<: *backend-template
  container_name: xpensio_backend2

Veya K8s'te replicas: 3+ zaten otomatik dağıtım yapar.

5.3 Yavaş Sorgu Tespiti

-- En yavaş 10 sorgu
SELECT query, calls, mean_exec_time, max_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

6. Güvenlik İşlemleri

6.1 Şifre Değiştirme

PostgreSQL kullanıcı şifresi:

docker exec -it xpensio_db psql -U xpensio_app -d expense_management -c "
  ALTER USER xpensio_app PASSWORD 'YENI_GUCLU_SIFRE';
"
# Sonra .env'de POSTGRES_PASSWORD ve DATABASE_URL güncelle, backend restart et

JWT_SECRET değişikliği:

ENCRYPTION_KEY değişikliği — DİKKAT:

6.2 Güvenlik Yamaları

Xpensio güvenlik açıkları için out-of-band patch yayınlar:

6.3 Erişim Logları (Audit)

Xpensio tüm yönetici işlemlerini DB'ye loglar:

SELECT created_at, user_email, action, target_type, target_id, ip_address
FROM audit_logs
ORDER BY created_at DESC
LIMIT 100;

Web'den: Admin → Denetim Logları


7. Sık Karşılaşılan Sorunlar

Sorun: Mail gönderilmiyor

Belirtiler: Şifre sıfırlama, onay bildirimi mail'leri ulaşmıyor.

Tanı:

docker logs xpensio_backend | grep -i mail

Sık nedenler:

  1. RESEND_API_KEY boş veya yanlış → API key'i kontrol edin
  2. NODE_ENV != production → development modda mail sadece DEV_ONLY_EMAIL'e gider
  3. Resend domain doğrulanmamış → Resend dashboard'da DNS SPF/DKIM kontrolü
  4. Outbound 443 kapalı → curl https://api.resend.com testi

Sorun: SAP entegrasyonu çalışmıyor

Belirtiler: Masraf FINANCE onayı sonrası SAP'a gitmiyor.

Tanı:

docker logs xpensio_backend | grep -i sap
# Veya: Web → Admin → SAP Queue

Sık nedenler:

  1. SAP RFC user şifresi süresi dolmuş
  2. SAP IP/firewall değişti
  3. ENCRYPTION_KEY değişti → kayıtlı SAP şifresi okunamıyor (UI'dan yeniden gir)
  4. SAP company code tanımlı değil
  5. GL hesap haritası eksik (cost center, expense GL, tax GL)

Çözüm: Web → Admin → SAP Queue → Failed Items üzerinden hata mesajını oku, SAP destek ile koordine et.

Sorun: Yavaşlama

Tanı:

docker stats          # CPU/RAM kullanımı
df -h                 # Disk doluluğu
docker logs xpensio_backend | grep -i 'slow\|timeout'

Sık nedenler:

  1. PostgreSQL disk %90+ → vacuum full veya disk genişlet
  2. Çok büyük rapor sorgusu → kullanıcıya tarih aralığı küçültme önerisi
  3. SAP yavaş → SAP ekibi ile koordine et (Xpensio değil SAP yavaşlığı)
  4. RAM yetersiz → resource limit artır veya sunucu büyüt

Sorun: "502 Bad Gateway"

Tanı:

docker compose ps   # Backend healthy mi?
docker logs xpensio_backend --tail=50

Sık nedenler:

  1. Backend henüz başlamadı (60sn warm-up)
  2. DB bağlantı kopmuş → docker restart xpensio_backend
  3. OOM (Out of Memory) kill → resource limit artır

Sorun: Disk doluyor

Tanı:

du -sh /opt/xpensio/data/*
du -sh /opt/xpensio/backups
docker system df

Çözümler:

  1. Eski backup'ları temizle (cron zaten 30 gün retention yapıyor)
  2. Docker image cache temizle: docker system prune -a
  3. PostgreSQL VACUUM FULL (offline işlem!)
  4. Uploads R2/S3'e taşı (Xpensio destek ile koordinasyon)

8. Acil Durum Prosedürü

8.1 Sistem Tamamen Erişilemiyor

# 1. Container durumu
docker compose ps

# 2. Hepsini durdurup başlat
docker compose down
docker compose up -d

# 3. Hala çalışmıyorsa: en son backup'tan restore (Bölüm 2.2)

# 4. Xpensio destek hattı
# E-mail: destek@xpensioapp.com
# Acil: +90 XXX XXX XX XX (sözleşme dahilinde)

8.2 Veri Kaybı Şüphesi

  1. HEMEN backup al (kötü duruma yenisi yazmadan):
    docker exec xpensio_db pg_dump -U xpensio_app expense_management | \
      gzip > /opt/xpensio/backups/EMERGENCY_$(date +%Y%m%d_%H%M).sql.gz
    
  2. Container'ları DURDURUN (yazmaya devam etmesin):
    docker compose stop backend web
    
  3. Xpensio destek ile koordine et (forensic + restore desteği için).

8.3 Güvenlik İhlali Şüphesi

  1. Sistemi internet erişiminden izole et (firewall'da 443 kapat)
  2. Tüm secret'ları rotate et (DB pass, JWT, ENCRYPTION_KEY)
  3. Audit log'ları arşivle:
    docker exec xpensio_db psql -U xpensio_app expense_management -c \
      "COPY audit_logs TO STDOUT" > audit_$(date +%Y%m%d).csv
    
  4. Xpensio destek + KVKK kurum temsilcisi ile koordine et (KVKK 72 saat içinde bildirim zorunluluğu).

9. Destek İletişim

Kanal Hedef SLA
destek@xpensioapp.com Standart soru, log analizi, küçük sorun 8 iş saati
Acil destek hattı (sözleşmesel) Sistem down, veri kaybı 4 saat müdahale
Aylık review toplantısı Performans, kullanım, yeni özellik talebi Önceden planlanır

Destek talebi açarken şunları paylaşın:


10. Periyodik Bakım

İşlem Sıklık Kim
Backup test restore 3 ayda Müşteri SysAdmin
OS güvenlik update Aylık Müşteri SysAdmin
SSL sertifika yenileme 60 günde (Let's Encrypt otomatik) Cron
Disk kullanım raporu Aylık Müşteri SysAdmin
Audit log arşivleme 6 ayda Müşteri (KVKK gereği)
Xpensio versiyon güncelleme Çeyrekte (en az) Müşteri SysAdmin
Güvenlik yaması Yayınlandığında Müşteri SysAdmin
Disaster Recovery tatbikat Yılda 1 Müşteri + Xpensio