Özel systemd Servisi Oluşturarak Uygulama Başlatma

Bir uygulamayı sunucuda çalıştırmanın en kolay yolu nohup ./uygulama & gibi bir komut vermek olabilir, ama bu yaklaşımın ne kadar kırılgan olduğunu ilk sistem yeniden başlatmasında ya da beklenmedik bir çöküşte anlıyorsunuz. Systemd, Linux dünyasının servis yönetim standardı haline geleli epey zaman oldu ve özel servis dosyaları yazmayı öğrenmek her sysadmin’in temel becerileri arasında olmalı. Bu yazıda gerçek dünya senaryoları üzerinden giderek kendi systemd servislerinizi nasıl oluşturacağınızı, yöneteceğinizi ve sorun gidereceğinizi ele alacağız.

Neden Özel Systemd Servisi?

Bir Python API’si çalıştırıyorsunuz, bir Go binary’si deploy ettiniz, ya da belki bir Node.js uygulaması var. Bu uygulamaların ortak ihtiyaçları şunlar:

  • Sunucu yeniden başladığında otomatik olarak ayağa kalkmaları
  • Çöktüklerinde otomatik olarak yeniden başlamaları
  • Log çıktılarının merkezi bir yerde toplanması
  • Belirli bir kullanıcı yetkileriyle çalışmaları
  • Diğer servislerden sonra (örneğin veritabanından sonra) başlamaları

Bunların hepsini systemd servis dosyaları ile sağlayabilirsiniz. Eski usul init.d scriptleri yazmak ya da supervisor gibi ek araçlar kurmak yerine, işletim sisteminizin yerleşik mekanizmasını kullanmak hem daha temiz hem de daha güvenilir bir yaklaşım.

Servis Dosyası Nereye Yazılır?

Systemd servis dosyaları .service uzantısıyla birkaç farklı konumda bulunabilir. Sistem genelinde geçerli olacak özel servisler için kullanmanız gereken dizin /etc/systemd/system/ dizinidir.

ls /etc/systemd/system/
# Mevcut özel servisleri görmek için
ls /lib/systemd/system/
# Sistem tarafından yüklenen servisleri görmek için

Kendi oluşturduğunuz servis dosyaları her zaman /etc/systemd/system/ altına gitmelidir. /lib/systemd/system/ dizinindeki dosyalar paket yöneticisi tarafından yönetilir ve sistem güncellemelerinde üzerine yazılabilir.

Temel Servis Dosyası Yapısı

Bir systemd servis dosyası INI formatına benzer bir yapıya sahip. Üç ana bölümden oluşur: [Unit], [Service] ve [Install].

[Unit]
Description=Benim Uygulamamın Açıklaması
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Bu kadar. En basit haliyle çalışan bir servis dosyası bu yapıya sahip. Şimdi her bölümü ve önemli parametreleri detaylı inceleyelim.

[Unit] Bölümü

Bu bölüm servisin meta verilerini ve bağımlılıklarını tanımlar.

Description: Servisin ne yaptığını açıklayan kısa metin. systemctl status çıktısında görünür.

After: Bu servis hangi servislerden sonra başlayacak. Örneğin bir veritabanı bağlantısına ihtiyaç duyuyorsanız After=postgresql.service yazmalısınız. network.target ise ağ arayüzlerinin hazır olmasını bekler.

Requires: After ile benzer ama daha katı. Belirtilen servis çalışmazsa bu servis de başlamaz ve durur.

Wants: Requires‘ın daha yumuşak versiyonu. Belirtilen servis başlamazsa bu servis yine de çalışmaya devam eder.

Before: Belirtilen servislerden önce başlaması gerektiğini söyler.

Örneğin bir uygulama hem PostgreSQL hem de Redis’e bağımlıysa:

[Unit]
Description=Python FastAPI Uygulaması
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service

[Service] Bölümü

İşin özü burada. Servisin nasıl çalışacağını bu bölümde tanımlıyorsunuz.

Type Parametresi

Type=simple: Varsayılan. ExecStart ile belirtilen process servisin ana process’i olarak kabul edilir.

Type=forking: Uygulama başladıktan sonra kendini fork edip arka plana geçiyorsa bu tipi kullanın. Eski tarz daemon’lar genellikle bu şekilde çalışır.

Type=oneshot: Bir kez çalışıp biten işler için. Cron benzeri görevler gibi.

Type=notify: Uygulama systemd’ye hazır olduğunda sd_notify() ile sinyal gönderiyorsa kullanılır.

Type=idle: Diğer tüm servisler başladıktan sonra başlar.

Çalıştırma Parametreleri

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/environment
ExecStart=/usr/bin/python3 /opt/myapp/main.py
ExecStartPre=/opt/myapp/pre-start.sh
ExecStopPost=/opt/myapp/cleanup.sh

User ve Group: Uygulamanın hangi kullanıcı/grup yetkileriyle çalışacağını belirtir. Root olarak çalıştırmaktan kaçının. Her uygulama için ayrı bir sistem kullanıcısı oluşturun.

WorkingDirectory: Uygulamanın çalışma dizini. Göreli dosya yolları kullanıyorsanız bu önemli.

EnvironmentFile: Ortam değişkenlerini ayrı bir dosyadan yükler. Hassas bilgileri (şifre, API anahtarı) servis dosyasına yazmak yerine bu yöntemi kullanın.

ExecStartPre: Ana process başlamadan önce çalıştırılacak komut. Veritabanı migration’ı ya da config dosyası kontrolü için idealdir.

ExecStopPost: Servis durduğunda çalışacak temizlik komutu.

Yeniden Başlatma Politikaları

Bu parametreler production ortamlarında kritik öneme sahip:

Restart=no: Varsayılan. Hiç yeniden başlatmaz.

Restart=on-failure: Sadece başarısız çıkışlarda (sıfır dışı exit code) yeniden başlatır.

Restart=always: Her durumda, normal çıkışta bile yeniden başlatır.

Restart=on-abnormal: Sinyal ile öldürüldüğünde veya timeout’ta yeniden başlatır.

RestartSec: Yeniden başlatmadan önce kaç saniye bekleyeceği.

StartLimitIntervalSec ve StartLimitBurst: Belirli süre içinde kaç kez yeniden başlatma denemesi yapılacağını sınırlar. Sürekli çöken bir servisin sistemi yormasını engeller.

[Service]
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=60
StartLimitBurst=3

Bu konfigürasyonla: 60 saniye içinde 3 kez başarısız olursa systemd vazgeçer ve servisi başarısız olarak işaretler.

[Install] Bölümü

WantedBy=multi-user.target: Servisi enable ettiğinizde hangi target altına eklenecek. Çoğu sunucu servisi için multi-user.target doğru seçim. Grafik arayüzlü sistemlerde graphical.target kullanılabilir.

Gerçek Dünya Örnek 1: Python FastAPI Uygulaması

Diyelim ki /opt/apiserver altında çalışan bir FastAPI uygulamanız var ve uvicorn ile ayağa kaldırıyorsunuz.

Önce uygulama kullanıcısını oluşturun:

sudo useradd --system --no-create-home --shell /bin/false apiserver
sudo chown -R apiserver:apiserver /opt/apiserver

Environment dosyasını hazırlayın:

sudo nano /etc/apiserver/environment
DATABASE_URL=postgresql://user:password@localhost/mydb
SECRET_KEY=supersecretkey123
ENVIRONMENT=production
PORT=8000
sudo chmod 640 /etc/apiserver/environment
sudo chown root:apiserver /etc/apiserver/environment

Servis dosyasını oluşturun:

# /etc/systemd/system/apiserver.service

[Unit]
Description=FastAPI Uygulama Sunucusu
Documentation=https://github.com/sirket/apiserver
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=apiserver
Group=apiserver
WorkingDirectory=/opt/apiserver
EnvironmentFile=/etc/apiserver/environment
ExecStart=/opt/apiserver/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=120
StartLimitBurst=5

# Güvenlik sertleştirme
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/apiserver/logs

# Log yönetimi
StandardOutput=journal
StandardError=journal
SyslogIdentifier=apiserver

[Install]
WantedBy=multi-user.target

Servisi etkinleştirin ve başlatın:

sudo systemctl daemon-reload
sudo systemctl enable apiserver.service
sudo systemctl start apiserver.service
sudo systemctl status apiserver.service

Gerçek Dünya Örnek 2: Go Binary Servisi

Go ile derlediğiniz bir binary’yi servis olarak çalıştırmak çok daha basit çünkü harici bağımlılık yok.

# /etc/systemd/system/goapp.service

[Unit]
Description=Go Mikro Servis - Ödeme İşlemleri
After=network.target

[Service]
Type=simple
User=goapp
WorkingDirectory=/opt/goapp
Environment="GIN_MODE=release"
Environment="LOG_LEVEL=info"
EnvironmentFile=-/etc/goapp/environment
ExecStart=/opt/goapp/payment-service -config /etc/goapp/config.yaml
Restart=always
RestartSec=3
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target

Burada dikkat çeken birkaç nokta var. EnvironmentFile=-/etc/goapp/environment ifadesindeki - işareti, eğer bu dosya yoksa hata vermeden devam etmesini sağlar. KillMode=mixed ile SIGTERM sinyali ana process’e gönderilir, belirtilen süre içinde kapanmazsa tüm alt process’ler sonlandırılır.

Servis Yönetimi Komutları

Servisi yönetmek için kullanacağınız temel komutlar:

# Servis dosyalarını yeniden yükle (yeni servis veya değişiklik sonrası zorunlu)
sudo systemctl daemon-reload

# Servisi başlat
sudo systemctl start myapp.service

# Servisi durdur
sudo systemctl stop myapp.service

# Servisi yeniden başlat
sudo systemctl restart myapp.service

# Config değişikliği sonrası graceful reload
sudo systemctl reload myapp.service

# Sistem başlangıcında otomatik başlatmayı etkinleştir
sudo systemctl enable myapp.service

# Otomatik başlatmayı devre dışı bırak
sudo systemctl disable myapp.service

# Enable + hemen başlat
sudo systemctl enable --now myapp.service

# Servis durumunu kontrol et
sudo systemctl status myapp.service

# Servisin aktif olup olmadığını kontrol et (script içinde kullanışlı)
systemctl is-active myapp.service

# Servisin enabled olup olmadığını kontrol et
systemctl is-enabled myapp.service

Log Yönetimi ve Sorun Giderme

Systemd journal, servis loglarını merkezi olarak tutar. Log okuma komutları:

# Servisin tüm loglarını göster
sudo journalctl -u myapp.service

# Son 100 satırı göster
sudo journalctl -u myapp.service -n 100

# Canlı log akışı (tail -f gibi)
sudo journalctl -u myapp.service -f

# Belirli bir zaman aralığındaki loglar
sudo journalctl -u myapp.service --since "2024-01-15 10:00" --until "2024-01-15 11:00"

# Sadece bu boot'taki loglar
sudo journalctl -u myapp.service -b

# Hata seviyesindeki loglar
sudo journalctl -u myapp.service -p err

# Birden fazla servisin loglarını birlikte göster
sudo journalctl -u myapp.service -u postgresql.service -f

Servis neden başlamıyor sorusuna yanıt ararken şu adımları izleyin:

# Önce duruma bak
sudo systemctl status myapp.service

# Tam hata mesajları için
sudo journalctl -u myapp.service -n 50 --no-pager

# Systemd'nin servis hakkında bildiklerini gör
sudo systemctl show myapp.service

# Servisin hangi bağımlılıklara sahip olduğunu görmek için
sudo systemctl list-dependencies myapp.service

Güvenlik Sertleştirme Direktifleri

Özel servisler yazarken güvenlik direktiflerini ihmal etmemek gerekiyor. Bu direktifler uygulamanızın sistem üzerindeki etkisini sınırlar.

[Service]
# Yeni ayrıcalıklar edinmeyi engelle
NoNewPrivileges=true

# /tmp dizinini izole et
PrivateTmp=true

# /usr ve /boot'u salt okunur yap
ProtectSystem=strict

# /home dizinine erişimi engelle
ProtectHome=true

# Sadece belirli dizinlere yazma izni ver
ReadWritePaths=/opt/myapp/data /var/log/myapp

# Gereksiz sistem çağrılarını filtrele
SystemCallFilter=@system-service

# Network namespace izolasyonu (sadece belirli servisler için)
# PrivateNetwork=true

# Sadece belirli network adreslerine bağlanmaya izin ver
IPAddressAllow=localhost
IPAddressDeny=any

ProtectSystem=strict ile birlikte ReadWritePaths kullanımına dikkat edin. Strict mod kök dosya sistemini salt okunur yapar, dolayısıyla uygulamanızın yazması gereken dizinleri açıkça belirtmeniz gerekir.

Zamanlayıcı ile Servis Çalıştırma (Systemd Timer)

Cron kullanmak yerine systemd timer’ları kullanabilirsiniz. Bunun için bir .service ve bir .timer dosyası oluşturmanız gerekiyor.

# /etc/systemd/system/backup.service

[Unit]
Description=Veritabanı Yedekleme İşi
After=postgresql.service

[Service]
Type=oneshot
User=backup
EnvironmentFile=/etc/backup/environment
ExecStart=/opt/backup/run-backup.sh
# /etc/systemd/system/backup.timer

[Unit]
Description=Veritabanı Yedekleme Zamanlayıcısı

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
Unit=backup.service

[Install]
WantedBy=timers.target
sudo systemctl enable --now backup.timer
sudo systemctl list-timers --all

Persistent=true ile sistem kapalıyken kaçırılan çalışmalar, sistem açıldığında hemen çalıştırılır.

Drop-in Konfigürasyon Dosyaları

Bazen bir sistem servisinin konfigürasyonunu değiştirmek istersiniz ama kaynak dosyayı düzenlemek istemezsiniz. Drop-in dosyaları bu iş için biçilmiş kaftan.

# Servis için override dizini oluştur
sudo systemctl edit nginx.service

# Ya da manuel olarak:
sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo nano /etc/systemd/system/nginx.service.d/override.conf
# /etc/systemd/system/nginx.service.d/override.conf

[Service]
# Mevcut değerin üzerine yeni değer eklemek için önce boş ata, sonra yeni değer ver
LimitNOFILE=
LimitNOFILE=65536
Restart=always
RestartSec=5

Bu yaklaşımla orijinal servis dosyasına dokunmadan özelleştirme yapabiliyorsunuz. Sistem güncellemesi geldiğinde orijinal dosya güncellense bile override ayarlarınız korunur.

Servis Şablonları

Aynı uygulamayı birden fazla instance olarak çalıştırmanız gerekiyorsa template servisler kullanabilirsiniz. Dosya adındaki @ işareti template olduğunu belirtir.

# /etc/systemd/system/[email protected]

[Unit]
Description=İşçi Süreci %i
After=network.target redis.service

[Service]
Type=simple
User=worker
WorkingDirectory=/opt/worker
Environment="WORKER_ID=%i"
EnvironmentFile=/etc/worker/environment
ExecStart=/opt/worker/worker --id %i --queue default
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
# 3 farklı worker instance başlat
sudo systemctl enable --now [email protected]
sudo systemctl enable --now [email protected]
sudo systemctl enable --now [email protected]

# Tüm worker instance'larını gör
sudo systemctl status 'worker@*.service'

%i ifadesi @ işaretinden sonra gelen parametreyi temsil eder. Loglarda da bu ID görünür, bu da hangi instance’ın sorun yaşadığını anlamayı kolaylaştırır.

Deployment Senaryosu: Sıfır Kesinti ile Güncelleme

Uygulamanızı güncellerken servis kesintisini minimize etmek için şu akışı kullanabilirsiniz:

#!/bin/bash
# /opt/myapp/deploy.sh

set -e

APP_DIR="/opt/myapp"
NEW_VERSION=$1

echo "Yeni versiyon deploy ediliyor: $NEW_VERSION"

# Yeni binary'yi kopyala
cp "/tmp/myapp-${NEW_VERSION}" "${APP_DIR}/myapp.new"
chmod +x "${APP_DIR}/myapp.new"

# Eski binary'yi yedekle
cp "${APP_DIR}/myapp" "${APP_DIR}/myapp.old"

# Yeni binary'yi aktif et
mv "${APP_DIR}/myapp.new" "${APP_DIR}/myapp"

# Graceful reload dene, olmazsa restart et
if ! sudo systemctl reload myapp.service 2>/dev/null; then
    sudo systemctl restart myapp.service
fi

# Servisin ayakta olduğunu kontrol et
sleep 3
if systemctl is-active --quiet myapp.service; then
    echo "Deploy başarılı!"
    rm -f "${APP_DIR}/myapp.old"
else
    echo "Deploy başarısız! Rollback yapılıyor..."
    mv "${APP_DIR}/myapp.old" "${APP_DIR}/myapp"
    sudo systemctl restart myapp.service
    exit 1
fi

Sonuç

Systemd servis dosyaları ilk bakışta karmaşık görünebilir ama bir kez mantığını kavradığınızda vazgeçilmez bir araç haline geliyor. Özetlemek gerekirse:

  • Her uygulama için ayrı bir sistem kullanıcısı oluşturun, root olarak çalıştırmaktan kaçının
  • EnvironmentFile kullanarak hassas bilgileri servis dosyasından ayrı tutun
  • Restart ve StartLimitBurst direktifleriyle kendini toparlayan servisler oluşturun
  • NoNewPrivileges, PrivateTmp, ProtectSystem gibi güvenlik direktiflerini mutlaka kullanın
  • Sistem servislerini değiştirirken drop-in dosyalarını tercih edin
  • Birden fazla instance için template servisleri kullanın

Cron job’lar için de systemd timer’larını değerlendirin; daha iyi log yönetimi ve bağımlılık yönetimi sunuyorlar. Production’da journalctl -u servisadi -f hayat kurtarır, logları düzenli takip edin.

En önemlisi, servis dosyalarınızı versiyon kontrolüne (git) ekleyin. Bir sunucuyu sıfırdan kurmak zorunda kaldığınızda ya da aynı yapıyı başka bir sunucuya taşımak istediğinizde ne kadar doğru bir karar olduğunu anlayacaksınız.

Yorum yapın