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.
| 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.
| 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.
| 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 |
| 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ı |
| 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.
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.
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) |
Üç seçenek:
Xpensio kurulum scriptleri her üç seçeneği de destekler.
Seçenek A — Resend (önerilen, Xpensio default)
Seçenek B — Müşteri SMTP (kurumsal mail sunucusu)
BAPI_ACC_DOCUMENT_POST veya custom Z BAPI)Detaylar: memory/SAP_SETUP_GUIDE.md, memory/SAP_S4_FI_SPEC.md
Xpensio iki şekilde kullanıcı yönetebilir:
| 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
Müşteri tarafında bir backup hedefi belirlenmelidir:
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:
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 logsveyakubectl logsyeter, Xpensio yapılandırılmış JSON log üretir.
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.
| 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 |
| İş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) | — |
| Metrik | Hedef |
|---|---|
| First Contentful Paint (FCP) | < 1.5 s |
| Time to Interactive (TTI) | < 2 s |
| Largest Contentful Paint (LCP) | < 2.5 s |
| 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+ |
Beklenenden yavaşsa, sırasıyla kontrol:
docker stats veya kubectl top pods (CPU/RAM doluyorsa upgrade gerek)pg_stat_statements); index eksik olabilirBazı müşteriler (banka, kamu, savunma) internet erişimi olmayan kapalı network'te kurulum ister. Xpensio bu senaryoyu destekler — bazı sınırlamalarla.
| Ö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) |
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
Xpensio yeni versiyon yayınladığında müşteri IT:
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.
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.).
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.
Kurulum başlamadan önce müşteri tarafından sağlanması gerekenler:
Tüm önkoşullar hazırsa:
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).
Başlamadan önce 02-SISTEM-GEREKSINIMLERI.md checklist'inin tamamlandığından emin olun. Özellikle:
xpensio.musteri.com.tr → sunucu IP)ghcr.io erişimi varsudo apt update && sudo apt upgrade -y
sudo apt install -y curl ca-certificates gnupg lsb-release ufw fail2ban
# 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
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
sudo mkdir -p /opt/xpensio/{config,data/postgres,data/uploads,logs,backups}
sudo chown -R $USER:$USER /opt/xpensio
cd /opt/xpensio
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).
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
:latestyerine sabit versiyon kullanın (örn.:v1.4.1). Xpensio teslimatla birlikte size kurulu versiyon numarasını verir.
.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.
docker-compose.ymlcat > /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
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
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
# 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
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:
# 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
İ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.
https://xpensio.musteri.com.tradmin@musteri.com.tr ile giriş yapDetaylı setup adımları: memory/SAP_SETUP_GUIDE.md
# 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
Aşağıdakiler tamamlandıysa kurulum başarılıdır:
https://xpensio.musteri.com.tr üzerinden web giriş ekranı açılıyorhttps://xpensio.musteri.com.tr URL'i girilerek)docker compose logs backend | tail -50
Sık nedenler:
ENCRYPTION_KEY 64 hex char değil → openssl rand -hex 32 ile yeniden üretDATABASE_URL yanlış → host = postgres (compose servis adı), port = 5432 (container içi)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.
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?
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?
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).
kubectl ile cluster admin erişimixpensio.musteri.com.tr → Ingress public IP/hostname# 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'
# 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}"
# 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
# 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
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.
# 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
Eğer kurumsal yönetilen DB kullanılacaksa:
app-secret → database-url'i harici DB connection string ile güncelleyinpostgres.yml'i deploy etmeyin# 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.
# 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
# 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
# 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}
# 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
# 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}]
# 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}
# 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
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
# 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
# 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.
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
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
# 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
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.
| 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ış |
# 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
Ö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
Xpensio yeni versiyon yayınladığında müşteriye e-mail ile bildirim gönderir:
v1.4.2)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
# 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
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.
| 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 |
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"
Backend default info seviyesinde log basar. Debug için:
# .env
LOG_LEVEL=debug # info | warn | error
Restart sonrası geçerli.
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
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.
-- 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;
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:
.env güncelle, backend restartENCRYPTION_KEY değişikliği — DİKKAT:
Xpensio güvenlik açıkları için out-of-band patch yayınlar:
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ı
Belirtiler: Şifre sıfırlama, onay bildirimi mail'leri ulaşmıyor.
Tanı:
docker logs xpensio_backend | grep -i mail
Sık nedenler:
RESEND_API_KEY boş veya yanlış → API key'i kontrol edinNODE_ENV != production → development modda mail sadece DEV_ONLY_EMAIL'e gidercurl https://api.resend.com testiBelirtiler: Masraf FINANCE onayı sonrası SAP'a gitmiyor.
Tanı:
docker logs xpensio_backend | grep -i sap
# Veya: Web → Admin → SAP Queue
Sık nedenler:
Çözüm: Web → Admin → SAP Queue → Failed Items üzerinden hata mesajını oku, SAP destek ile koordine et.
Tanı:
docker stats # CPU/RAM kullanımı
df -h # Disk doluluğu
docker logs xpensio_backend | grep -i 'slow\|timeout'
Sık nedenler:
vacuum full veya disk genişletTanı:
docker compose ps # Backend healthy mi?
docker logs xpensio_backend --tail=50
Sık nedenler:
docker restart xpensio_backendTanı:
du -sh /opt/xpensio/data/*
du -sh /opt/xpensio/backups
docker system df
Çözümler:
docker system prune -aVACUUM FULL (offline işlem!)# 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)
docker exec xpensio_db pg_dump -U xpensio_app expense_management | \
gzip > /opt/xpensio/backups/EMERGENCY_$(date +%Y%m%d_%H%M).sql.gz
docker compose stop backend web
docker exec xpensio_db psql -U xpensio_app expense_management -c \
"COPY audit_logs TO STDOUT" > audit_$(date +%Y%m%d).csv
| 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:
docker exec xpensio_backend cat /app/apps/backend/package.json | grep versiondocker compose ps çıktısı| İş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 |