Python Uygulamasını Systemd Servis Olarak Çalıştırma

Bir Python uygulamasını elle çalıştırmak başlarda makul görünür. Terminali açarsın, sanal ortamı aktive edersin, scripti başlatırsın. Sonra sunucu yeniden başlar, uygulaman durur, sen de saat 03:00’te telefon alırsın. İşte tam bu noktada systemd devreye giriyor. Python uygulamalarını systemd servisi olarak çalıştırmak, hem güvenilirlik hem de yönetilebilirlik açısından production ortamında olmazsa olmaz bir pratiktir.

Neden Systemd?

Systemd, modern Linux dağıtımlarının neredeyse tamamında init sistemi olarak kullanılıyor. Uygulamamızı systemd ile yönetmenin bize sağladığı avantajları sıralayalım:

  • Otomatik başlatma: Sunucu açıldığında uygulama otomatik ayağa kalkar
  • Çökme sonrası yeniden başlatma: Uygulama hata verip dursa bile systemd onu tekrar başlatır
  • Merkezi log yönetimi: journald entegrasyonu sayesinde tüm loglar tek yerden takip edilir
  • Bağımlılık yönetimi: Ağ, veritabanı gibi servislerin hazır olmasını bekletebilirsiniz
  • Kaynak sınırlama: CPU ve bellek limitlerini servis dosyasında tanımlayabilirsiniz

Şimdi her şeyi baştan kuralım.

Örnek Senaryo: Flask API Servisi

Bu yazıda gerçek dünyaya yakın bir senaryo kullanacağız. Diyelim ki bir Flask tabanlı REST API geliştirdiniz ve bunu production sunucusunda sürekli çalışır halde tutmak istiyorsunuz. Uygulamanın adı inventory-api, çalışacağı kullanıcı appuser.

Kullanıcı ve Dizin Yapısı Oluşturma

Önce uygulamayı çalıştıracak sistem kullanıcısını oluşturalım. Production ortamında uygulamaları root ile çalıştırmak büyük bir güvenlik riskidir.

# Sistem kullanıcısı oluştur (login shell olmadan)
sudo useradd --system --no-create-home --shell /bin/false appuser

# Uygulama dizinini oluştur
sudo mkdir -p /opt/inventory-api
sudo mkdir -p /opt/inventory-api/logs

# Dizin sahipliğini ayarla
sudo chown -R appuser:appuser /opt/inventory-api

Uygulama dosyalarımızı bu dizine kopyalıyoruz. Basit bir Flask uygulaması örneği:

# /opt/inventory-api/app.py
from flask import Flask, jsonify
import logging
import os

app = Flask(__name__)

# Loglama ayarları
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

@app.route('/health')
def health():
    logger.info("Health check endpoint called")
    return jsonify({"status": "healthy", "service": "inventory-api"})

@app.route('/api/items')
def get_items():
    items = [
        {"id": 1, "name": "Laptop", "stock": 15},
        {"id": 2, "name": "Monitor", "stock": 8}
    ]
    return jsonify(items)

if __name__ == '__main__':
    port = int(os.environ.get('APP_PORT', 8080))
    app.run(host='0.0.0.0', port=port)

Sanal Ortam Oluşturma

Bu adım kritik. Systemd servisi olarak çalışırken hangi Python yorumlayıcısını ve hangi kütüphaneleri kullanacağımızı net olarak belirtmemiz gerekiyor. Sanal ortam bu noktada kurtarıcımız.

# appuser olarak sanal ortam oluştur
sudo -u appuser python3 -m venv /opt/inventory-api/venv

# Bağımlılıkları yükle
sudo -u appuser /opt/inventory-api/venv/bin/pip install flask gunicorn

# requirements.txt varsa
sudo -u appuser /opt/inventory-api/venv/bin/pip install -r /opt/inventory-api/requirements.txt

# Kurulumu doğrula
sudo -u appuser /opt/inventory-api/venv/bin/python -c "import flask; print(flask.__version__)"

Sanal ortamı doğrudan kullanıcı ile oluşturmak, servis dosyasında hak sorunları yaşamamanızı sağlar. Bu detayı atlayan pek çok sysadmin “Permission denied” hataları ile uğraşmak zorunda kalır.

Systemd Servis Dosyası Oluşturma

Asıl konumuza geldik. Systemd servis dosyaları /etc/systemd/system/ dizinine yerleştirilir ve .service uzantısı taşır.

sudo nano /etc/systemd/system/inventory-api.service

Servis dosyasının içeriği:

[Unit]
Description=Inventory API - Flask REST Servisi
Documentation=https://github.com/sirketiniz/inventory-api
After=network.target
Wants=network-online.target

[Service]
Type=exec
User=appuser
Group=appuser
WorkingDirectory=/opt/inventory-api

# Sanal ortamdaki Gunicorn ile çalıştır
ExecStart=/opt/inventory-api/venv/bin/gunicorn 
    --workers 4 
    --bind 0.0.0.0:8080 
    --timeout 120 
    --access-logfile /opt/inventory-api/logs/access.log 
    --error-logfile /opt/inventory-api/logs/error.log 
    app:app

# Yeniden başlatma ayarları
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3

# Ortam değişkenleri
Environment="FLASK_ENV=production"
Environment="APP_PORT=8080"
EnvironmentFile=-/opt/inventory-api/.env

# Güvenlik ayarları
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/inventory-api/logs

# Log ayarları
StandardOutput=journal
StandardError=journal
SyslogIdentifier=inventory-api

[Install]
WantedBy=multi-user.target

Bu dosyayı biraz açalım çünkü her satır önemli.

[Unit] Bölümü

  • After=network.target: Ağ hazır olmadan başlamaz
  • Wants=network-online.target: Ağ bağlantısı tam olarak kurulana kadar bekler, harici API çağrıları yapan uygulamalar için şart

[Service] Bölümü

  • Type=exec: ExecStart ile başlatılan process doğrudan main process olur, fork etmez
  • WorkingDirectory: Uygulamanın başlangıç dizini, görece dosya yolları için kritik
  • Restart=on-failure: Sadece hata durumunda yeniden başlatır, manuel durdurmalarda başlatmaz
  • RestartSec=5s: Yeniden başlatmalar arasında 5 saniye bekler
  • StartLimitBurst=3: 60 saniye içinde en fazla 3 kez yeniden başlatmayı dener
  • EnvironmentFile: Hassas bilgileri (veritabanı şifresi vb.) ayrı dosyada tutmanızı sağlar. Baştaki - işareti dosya yoksa hata vermemesini söyler
  • NoNewPrivileges=true: Process yeni ayrıcalıklar kazanamaz
  • PrivateTmp=true: Geçici dizinler izole edilir
  • ProtectSystem=strict: Sistem dizinleri salt okunur olur

Ortam Değişkeni Dosyası

Hassas bilgileri servis dosyasına yazmak yerine ayrı bir .env dosyasına taşıyın:

sudo nano /opt/inventory-api/.env
# /opt/inventory-api/.env
DATABASE_URL=postgresql://user:gizlisifre@localhost:5432/inventory
SECRET_KEY=cok-gizli-bir-anahtar-buraya
REDIS_URL=redis://localhost:6379/0
SENTRY_DSN=https://[email protected]/yyyy
# .env dosyasının izinlerini kısıtla
sudo chown appuser:appuser /opt/inventory-api/.env
sudo chmod 600 /opt/inventory-api/.env

Servisi Aktive Etme ve Yönetme

Servis dosyasını oluşturduktan sonra systemd’ye bildirmemiz gerekiyor:

# Systemd daemon'ı yenile (yeni/değişen servis dosyalarını okur)
sudo systemctl daemon-reload

# Servisi etkinleştir (boot'ta otomatik başlatma)
sudo systemctl enable inventory-api

# Servisi hemen başlat
sudo systemctl start inventory-api

# Durumu kontrol et
sudo systemctl status inventory-api

systemctl status çıktısı şöyle görünmeli:

● inventory-api.service - Inventory API - Flask REST Servisi
     Loaded: loaded (/etc/systemd/system/inventory-api.service; enabled)
     Active: active (running) since Mon 2024-01-15 14:32:11 UTC; 5s ago
   Main PID: 12345 (gunicorn)
      Tasks: 5 (limit: 4915)
     Memory: 87.2M
        CPU: 1.234s
     CGroup: /system.slice/inventory-api.service
             ├─12345 /opt/inventory-api/venv/bin/python /opt/...

Log Takibi

Systemd ile logları takip etmek son derece kolaydır:

# Son logları göster
sudo journalctl -u inventory-api

# Canlı log takibi
sudo journalctl -u inventory-api -f

# Son 100 satır
sudo journalctl -u inventory-api -n 100

# Belirli zaman aralığı
sudo journalctl -u inventory-api --since "2024-01-15 14:00:00" --until "2024-01-15 15:00:00"

# Sadece hataları göster
sudo journalctl -u inventory-api -p err

# Boot'tan bu yana
sudo journalctl -u inventory-api -b

SyslogIdentifier=inventory-api sayesinde loglar düzgün etiketlenir ve filtreleme çok kolaylaşır.

Gelişmiş Senaryo: Celery Worker Servisi

Sadece web uygulaması değil, arka plan worker’ları da aynı şekilde yönetebilirsiniz. Celery worker için örnek:

[Unit]
Description=Inventory API Celery Worker
After=network.target redis.service
Requires=redis.service

[Service]
Type=forking
User=appuser
Group=appuser
WorkingDirectory=/opt/inventory-api
EnvironmentFile=/opt/inventory-api/.env

ExecStart=/opt/inventory-api/venv/bin/celery 
    -A app.celery 
    worker 
    --loglevel=info 
    --concurrency=4 
    --pidfile=/opt/inventory-api/celery.pid 
    --logfile=/opt/inventory-api/logs/celery.log 
    --detach

ExecStop=/opt/inventory-api/venv/bin/celery 
    -A app.celery 
    control shutdown

ExecReload=/bin/kill -s HUP $MAINPID
PIDFile=/opt/inventory-api/celery.pid

Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target

Requires=redis.service satırı önemli. Celery worker’ınız Redis olmadan çalışamaz, bu yüzden Redis başlamadan bu servis de başlamaz.

Sorun Giderme

Production’da karşılaşılan yaygın problemleri ve çözümlerini paylaşayım.

“Failed to start” Hatası

# Detaylı hata mesajını görmek için
sudo journalctl -u inventory-api -n 50 --no-pager

# Servis dosyasını doğrula
sudo systemd-analyze verify /etc/systemd/system/inventory-api.service

Çoğu zaman hata ya yanlış Python/Gunicorn yolu ya da eksik izinlerden kaynaklanır.

İzin Sorunları

# appuser olarak manuel test et
sudo -u appuser /opt/inventory-api/venv/bin/gunicorn 
    --workers 1 
    --bind 0.0.0.0:8080 
    app:app

# Log dizini yazılabilir mi?
sudo -u appuser touch /opt/inventory-api/logs/test.log

Servis Sürekli Yeniden Başlıyorsa

# Son başarısız nedeni gör
sudo journalctl -u inventory-api -p err -n 30

# StartLimitBurst aşıldıysa sıfırla
sudo systemctl reset-failed inventory-api
sudo systemctl start inventory-api

Servis Güncellemesi ve Deployment

Yeni kod deploy ettiğinizde servisi nasıl güncelleyeceğiniz de önemli. Basit bir deployment scripti:

#!/bin/bash
# /opt/scripts/deploy-inventory-api.sh

set -e

APP_DIR="/opt/inventory-api"
REPO_URL="https://github.com/sirketiniz/inventory-api"
SERVICE_NAME="inventory-api"

echo "Deployment basliyor..."

# Yeni kodu çek
sudo -u appuser git -C $APP_DIR pull origin main

# Bağımlılıkları güncelle
sudo -u appuser $APP_DIR/venv/bin/pip install -r $APP_DIR/requirements.txt --quiet

# Veritabanı migrasyonlarını çalıştır
sudo -u appuser $APP_DIR/venv/bin/python $APP_DIR/manage.py db upgrade

# Servisi yeniden başlat
sudo systemctl restart $SERVICE_NAME

# Sağlık kontrolü
sleep 3
if sudo systemctl is-active --quiet $SERVICE_NAME; then
    echo "Deployment basarili! Servis calisiyor."
    curl -sf http://localhost:8080/health && echo " - Health check gecti"
else
    echo "HATA: Servis baslatılamadi!"
    sudo journalctl -u $SERVICE_NAME -n 20 --no-pager
    exit 1
fi

Kaynak Limitleri

Production ortamında Python uygulamalarının ne kadar bellek ve CPU kullanabileceğini kısıtlamak akıllıca bir pratiktir:

[Service]
# Bellek limiti (2GB)
MemoryLimit=2G
MemoryHigh=1.5G

# CPU limiti (2 çekirdek eşdeğeri)
CPUQuota=200%

# Açık dosya sayısı limiti
LimitNOFILE=65536

# Process sayısı limiti
LimitNPROC=512

MemoryHigh değeri aşıldığında systemd uyarı üretir ama durdurmaz. MemoryLimit kesin sınırdır ve aşılırsa process öldürülür.

Birden Fazla Ortam: Template Servisler

Aynı uygulamanın staging ve production versiyonlarını aynı sunucuda çalıştırıyorsanız template servisler kullanın:

sudo nano /etc/systemd/system/[email protected]
[Unit]
Description=Inventory API - %i ortami
After=network.target

[Service]
Type=exec
User=appuser
WorkingDirectory=/opt/inventory-api-%i
EnvironmentFile=/opt/inventory-api-%i/.env
ExecStart=/opt/inventory-api-%i/venv/bin/gunicorn 
    --workers 2 
    --bind 0.0.0.0:%I 
    app:app
Restart=on-failure

[Install]
WantedBy=multi-user.target

Kullanımı:

# Staging başlat (8081 portunda)
sudo systemctl start inventory-api@8081

# Production başlat (8080 portunda)
sudo systemctl start inventory-api@8080

# Her ikisini de etkinleştir
sudo systemctl enable inventory-api@8081 inventory-api@8080

%i servis adındaki @ sonrasını temsil eder. Bu sayede tek bir servis dosyasıyla birden fazla instance yönetebilirsiniz.

Sonuç

Python uygulamalarını systemd servisi olarak çalıştırmak, “elle başlatıp unutma” döneminden “gerçek production yönetimi” dönemine geçişin temel adımıdır. Doğru yapılandırılmış bir systemd servisi size şunları kazandırır: Sunucu yeniden başladığında uygulamanız otomatik ayağa kalkar. Çökmeler otomatik olarak ele alınır. Loglar merkezi ve aranabilir halde tutulur. Güvenlik ihlallerinin etkisi izole edilir.

Başlangıçta servis dosyası yazmak biraz uğraştırıcı görünebilir ama bir kez doğru kurguladıktan sonra gece yarısı uyandırılma ihtimali ciddi ölçüde düşer. Benim kendi pratiğimde her yeni Python projesi için servis dosyası şablonu hazır tutuyorum ve yeni deployment’larda sadece birkaç parametreyi değiştiriyorum. Bu alışkanlığı edinmenizi şiddetle tavsiye ederim.

Son olarak: Servis dosyasını production’a taşımadan önce mutlaka systemd-analyze verify ile doğrulayın ve mümkünse staging ortamında test edin. Küçük bir yazım hatası uygulamanızın hiç başlamamasına neden olabilir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir