Docker & DevOps

    systemd Servis Dosyası Yazma

    Kendi uygulamanı sistem servisine dönüştüren unit dosyasının her direktifi ve güvenli şablonu.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    Uygulamanı nohup node server.js & ile başlattığın gün her şey çalışır. Sonra sunucu bir gece yeniden başlar ve uygulaman geri gelmez; ya da süreç sessizce çöker ve kimse fark etmez. systemd servis dosyası yazma, tam olarak bu iki sorunu kalıcı biçimde kapatan iştir: uygulamanı sistemin bildiği, açılışta başlattığı, çöktüğünde geri getirdiği ve loglarını tek yerde topladığı bir servise dönüştürürsün.

    Bu rehberde bir unit dosyasını satır satır kuracağız. Üç bölümün ([Unit], [Service], [Install]) ne işe yaradığını, Type değerinin neden yanlış seçildiğinde servisi "başlıyor ama hazır değil" hâline soktuğunu, ortam değişkenlerini güvenli biçimde nasıl vereceğini, kaynak sınırlarını ve güvenlik sertleştirme direktiflerini göreceğiz. Sonunda şablon servislerle tek dosyadan çok örnek çalıştırmayı ve en sık yapılan hataları konuşacağız.

    Unit Dosyasının Üç Bölümü#

    Bir .service dosyası /etc/systemd/system/ altında yaşar ve üç bölümden oluşur. Dağıtımın kendi paketleri /lib/systemd/system/ altına yazar; senin yazdıkların /etc/ altına gider ve aynı adlı bir paket dosyasını geçersiz kılar. Bu ayrımı bilmek, bir gün paket güncellemesinin senin ayarını ezip ezmeyeceği sorusunu baştan cevaplar.

    [Unit] bölümü servisin kimliğini ve diğer birimlerle ilişkisini tanımlar. [Service] bölümü sürecin nasıl çalıştırılacağını anlatır ve dosyanın asıl gövdesidir. [Install] bölümü ise systemctl enable çalıştığında servisin hangi hedefe bağlanacağını söyler; bu bölüm yoksa servisi etkinleştiremezsin, yalnızca elle başlatabilirsin.

    # /etc/systemd/system/siparis-api.service
    [Unit]
    Description=Siparis API servisi
    Documentation=https://firmaniz.com/docs/siparis-api
    # Ağ TAM hazır olduktan sonra başla (network.target yeterli değildir)
    After=network-online.target postgresql.service
    Wants=network-online.target
    # Postgres gerçekten şartsa Requires kullan; o düşerse bu da düşer
    Requires=postgresql.service
    
    [Service]
    Type=simple
    User=uygulama
    Group=uygulama
    WorkingDirectory=/opt/siparis-api
    ExecStart=/usr/bin/node /opt/siparis-api/server.js
    Restart=always
    RestartSec=5s
    
    [Install]
    WantedBy=multi-user.target
    

    After= ile Requires= arasındaki fark sık karıştırılır ve önemlidir. After= yalnızca sıra belirtir: "o başladıktan sonra ben başlayayım". Requires= ise bağımlılık kurar: gerekli birim başlatılamazsa bu servis de başlatılmaz, gerekli birim durdurulursa bu servis de durdurulur. Veritabanı olmadan uygulamanın hiçbir anlamı yoksa Requires doğrudur; uygulama veritabanını bekleyip yeniden deniyorsa yalnızca After yeterlidir ve daha esnektir.

    network.target yerine network-online.target kullanmanın sebebi de burada: network.target yalnızca ağ yığınının yapılandırılmaya başladığını, network-online.target ise bir arayüzün gerçekten IP alıp erişilebilir olduğunu ifade eder. Açılışta dış bir adrese bağlanan servisler bu farkı yaşayarak öğrenir.

    Type Direktifi: En Çok Yanlış Seçilen Ayar#

    Type=, systemd'nin "bu servis başladı" kararını nasıl vereceğini belirler. Yanlış seçildiğinde servis çalışır ama bağımlı servisler yanlış anda başlar, systemctl start beklemeden döner ya da hiç dönmez.

    TypeNe zaman "başladı" sayılırTipik kullanım
    simpleExecStart süreci çalıştırıldığı anÖn planda çalışan çoğu uygulama
    execSüreç gerçekten çalıştırılabildiğindesimple'ın daha doğru hâli
    forkingAna süreç çatallanıp çıktığındaKendini arka plana atan klasik daemon'lar
    oneshotKomut tamamlanıp çıktığındaTek seferlik görevler, timer'lar
    notifySüreç systemd'ye hazır olduğunu bildirincesd_notify destekleyen servisler

    Modern uygulamaların çoğu ön planda çalışır ve simple doğru seçimdir. Uygulaman kendini arka plana atıyorsa (daemon moduna geçiyorsa) forking kullanır ve mümkünse PIDFile= verirsin; vermezsen systemd hangi sürecin ana süreç olduğunu tahmin etmeye çalışır ve yanlış tahmin ederse servisi izleyemez.

    oneshot özel bir durumdur: komut biter, servis "başarıyla tamamlandı" sayılır ve süreç ayakta kalmaz. Yedek alma, göç çalıştırma, sertifika yenileme gibi işler bu tiptedir ve genellikle bir timer ile eşleştirilir. RemainAfterExit=yes eklersen, komut bitse bile servis "aktif" görünür — güvenlik duvarı kuralı yükleme gibi durum bırakan işlerde işe yarar.

    # Tek seferlik göç servisi — ana servisten önce çalışsın
    [Unit]
    Description=Veritabani gocleri
    Before=siparis-api.service
    
    [Service]
    Type=oneshot
    RemainAfterExit=yes
    User=uygulama
    WorkingDirectory=/opt/siparis-api
    ExecStart=/usr/bin/node /opt/siparis-api/migrate.js
    
    [Install]
    WantedBy=multi-user.target
    

    Ortam Değişkenleri ve Gizli Bilgiler#

    Uygulaman veritabanı parolası, API anahtarı gibi değerlere ihtiyaç duyar. Bunları Environment= ile doğrudan unit dosyasına yazabilirsin ama yazmamalısın: unit dosyaları genellikle 644 izinlidir ve systemctl cat çıktısında herkes tarafından okunabilir. Doğru yol ayrı bir ortam dosyasıdır.

    [Service]
    # Hassas olmayan değerler doğrudan yazılabilir
    Environment=NODE_ENV=production
    Environment=PORT=3000
    
    # Gizli değerler ayrı dosyadan okunur
    EnvironmentFile=/etc/siparis-api/env
    # Başındaki - işareti: dosya yoksa servis yine de başlar
    EnvironmentFile=-/etc/siparis-api/env.local
    

    Ortam dosyasının izinlerini daraltmayı unutma; bu iki komut olmadan yukarıdaki ayrımın hiçbir anlamı kalmaz:

    sudo install -d -m 0750 -o root -g uygulama /etc/siparis-api
    sudo tee /etc/siparis-api/env >/dev/null <<'EOF'
    DATABASE_URL=postgres://uygulama:[email protected]:5432/siparis
    JWT_SECRET=degistir-beni
    EOF
    sudo chmod 0640 /etc/siparis-api/env
    sudo chown root:uygulama /etc/siparis-api/env
    

    Ortam dosyasının biçimi .env dosyalarına benzer ama kabuk değildir: KEY=value satırları alır, komut ikamesi ($(...)) çalıştırmaz ve değerlerdeki tırnaklar birebir korunur. Bu farkı bilmezsen PATH=$PATH:/opt/bin gibi bir satır beklediğin gibi davranmaz. Güçlü rastgele değerler üretmek için şifre üretici aracımızı kullanabilirsin. Bu tür dosyaların kazara sürüm kontrolüne girip yayınlanması ciddi bir risktir; .git klasörü ve .env dosyası ifşası yazısı bu kazanın nasıl gerçekleştiğini ve nasıl engelleneceğini anlatıyor.

    Yeniden Başlatma, Kaynak Sınırları ve Sağlık#

    Bir servisi yazmanın asıl kazancı, çöktüğünde geri gelmesidir. Ancak bunu sınırsız yaparsan, açılışta hemen çöken bir uygulama saniyede birkaç kez yeniden başlar ve makineyi meşgul eder.

    [Service]
    Restart=always
    RestartSec=5s
    # 5 dakikalık pencerede en fazla 5 deneme; aşarsa servis 'failed' kalır
    StartLimitIntervalSec=300
    StartLimitBurst=5
    
    # Zarif kapanma: önce SIGTERM, 30 sn içinde kapanmazsa SIGKILL
    KillSignal=SIGTERM
    TimeoutStopSec=30s
    
    # Kaynak tavanları — sızıntı tüm makineyi yemesin
    MemoryMax=1G
    MemoryHigh=768M
    CPUQuota=150%
    TasksMax=512
    

    Restart= değerleri arasındaki fark davranışı doğrudan belirler: on-failure yalnızca sıfırdan farklı çıkış kodunda veya sinyalle ölümde yeniden başlatır, always her çıkışta başlatır (normal çıkış dâhil), no hiç başlatmaz. Uzun süre çalışması beklenen bir servis için always, tek seferlik bir görev için no ya da on-failure doğrudur.

    MemoryMax sınırı aşıldığında çekirdeğin OOM mekanizması süreci öldürür ve Restart devreye girer. Bu, tek bir uygulamanın bellek sızıntısıyla tüm sunucuyu kilitlemesini engelleyen en ucuz sigortadır. CPUQuota=150% ise bir buçuk çekirdek anlamına gelir; çok çekirdekli makinelerde tek servisin tüm CPU'yu yemesini önler.

    Servisin gerçekten iş yapabildiğini de kontrol etmek istersen, bunu unit dosyasına gömmek yerine ayrı bir sağlık kontrolü katmanına bırakmak daha temizdir. Liveness ve readiness ayrımının nasıl tasarlandığını ve systemd timer ile nasıl eşleştirileceğini uygulama sağlık kontrolü ve otomatik yeniden başlatma yazısında ayrıntılı anlattım.

    Güvenlik Sertleştirme Direktifleri#

    systemd'nin en az bilinen ama en değerli tarafı, servisi neredeyse ücretsiz biçimde kum havuzuna alabilmesidir. Aşağıdaki blok, uygulaman ele geçirilse bile verebileceği zararı ciddi biçimde daraltır:

    [Service]
    # Ayrıcalık yükseltmeyi tamamen kapat
    NoNewPrivileges=yes
    # /usr ve /boot salt okunur; 'strict' tüm kök dosya sistemini salt okunur yapar
    ProtectSystem=strict
    # /home, /root, /run/user erişilemez
    ProtectHome=yes
    # Servise özel, izole /tmp
    PrivateTmp=yes
    # Çekirdek ayarlarına ve modüllerine dokunamaz
    ProtectKernelTunables=yes
    ProtectKernelModules=yes
    ProtectControlGroups=yes
    # Yalnızca IPv4/IPv6/Unix soketlerine izin ver
    RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
    # Bellek hem yazılabilir hem çalıştırılabilir olamaz
    MemoryDenyWriteExecute=yes
    RestrictRealtime=yes
    LockPersonality=yes
    
    # ProtectSystem=strict altında yazma izni verilecek dizinler:
    ReadWritePaths=/var/lib/siparis-api /var/log/siparis-api
    # systemd'nin oluşturup sahipliğini vereceği dizinler (/var/lib altında):
    StateDirectory=siparis-api
    LogsDirectory=siparis-api
    

    ProtectSystem=strict yazdıktan sonra servis "izin reddedildi" hatalarıyla açılmıyorsa şaşırma; uygulamanın yazdığı her dizini ReadWritePaths ile açman gerekir. Doğru yaklaşım önce ProtectSystem=full ile başlamak, çalıştığını görmek, sonra stricte geçip eksik yolları teker teker eklemektir. Sertleştirmenin ne kadarını uyguladığını systemd'nin kendi analiz aracı puanlar:

    # Servisin güvenlik puanını ve eksik direktifleri listeler
    systemd-analyze security siparis-api.service
    

    Bu komut sana 0–10 arası bir risk puanı ve hangi direktifin eksik olduğunu tek tek söyler. Yeni bir servis yazdıktan sonra çalıştırmayı alışkanlık hâline getirirsen, çoğu sertleştirmeyi beş dakikada eklersin. Uygulaman ayrıca dış dünyaya açık bir port dinliyorsa, sertleştirmenin yanına bir güvenlik duvarı ve gerekirse WAF katmanı koymak mantıklıdır.

    Şablon Servisler ve Devreye Alma#

    Aynı uygulamadan birden fazla örnek çalıştırman gerekiyorsa (üç kuyruk işçisi, iki farklı port), her biri için ayrı dosya yazmak yerine şablon servis kullanırsın. Dosya adının sonuna @ koyarsın ve %i ile örnek adını okursun:

    # /etc/systemd/system/[email protected]
    [Unit]
    Description=Kuyruk iscisi (%i)
    After=network-online.target
    
    [Service]
    Type=simple
    User=uygulama
    WorkingDirectory=/opt/siparis-api
    Environment=WORKER_ID=%i
    EnvironmentFile=/etc/siparis-api/env
    ExecStart=/usr/bin/node /opt/siparis-api/worker.js
    Restart=always
    RestartSec=5s
    
    [Install]
    WantedBy=multi-user.target
    

    Tek dosyayla istediğin kadar işçi çalıştırırsın:

    sudo systemctl daemon-reload
    sudo systemctl enable --now [email protected]
    sudo systemctl enable --now [email protected]
    sudo systemctl enable --now [email protected]
    
    # Hepsini birden yönet
    sudo systemctl restart 'kuyruk-isci@*.service'
    systemctl status 'kuyruk-isci@*.service' --no-pager
    

    Yeni ya da değiştirilmiş bir unit dosyasını devreye alma sırası her zaman aynıdır ve ilk adım atlanınca systemd eski içeriği kullanmaya devam eder:

    1. Dosyayı /etc/systemd/system/ altına yaz.
    2. sudo systemctl daemon-reload — systemd unit dosyalarını yeniden okur.
    3. sudo systemctl enable --now servis.service — açılışta başlat ve hemen çalıştır.
    4. systemctl status servis.service — durumu ve son log satırlarını gör.
    5. journalctl -u servis.service -f — canlı log akışını izle.

    Değişiklikleri elle uygulamak yerine tek komuta bağlamak istersen, kurulum ve devreye alma adımlarını bir Makefile ile görev otomasyonu hedefine taşımak günlük işini ciddi biçimde kısaltır.

    Sık Yapılan Hatalar ve Tuzaklar#

    daemon-reload atlamak. Unit dosyasını değiştirip doğrudan restart yaparsan systemd eski içeriği çalıştırır ve sen "değişiklik neden işe yaramadı" diye dosyaya bakarsın. Değişiklik yaptığın her seferde önce daemon-reload.

    Göreli yol kullanmak. ExecStart=node server.js çalışmaz; systemd bir kabuk başlatmaz, bu yüzden PATH çözümlemesi ve ~ genişletmesi yoktur. Her zaman tam yol yaz: /usr/bin/node. Yolun ne olduğunu command -v node ile bulursun.

    Kabuk sözdizimi beklemek. ExecStart satırında |, >, && ve $(...) çalışmaz. Boru ya da yönlendirme gerekiyorsa komutu bir kabuğa sar: ExecStart=/bin/sh -c 'komut1 | komut2'. Daha temizi, işi bir betiğe alıp betiği çağırmaktır.

    Servisi root olarak çalıştırmak. User= vermezsen servis root olarak çalışır. Uygulamanın root olması gerekmiyorsa (çoğu zaman gerekmez) ayrı bir sistem kullanıcısı aç: sudo useradd --system --no-create-home --shell /usr/sbin/nologin uygulama. 1024 altındaki bir portu dinlemesi gerekiyorsa root yerine AmbientCapabilities=CAP_NET_BIND_SERVICE kullan.

    [Install] bölümünü unutmak. Bu bölüm yoksa systemctl enable "unit dosyasında Install bölümü yok" der ve servis açılışta başlamaz. Servis elle başlatıldığında çalıştığı için hata ancak ilk yeniden başlatmada fark edilir.

    Logları dosyaya yönlendirmeye çalışmak. systemd'nin altında çalışan bir servisin standart çıktısı zaten journald'a gider. Uygulaman kendi log dosyasını açıp yönetmeye çalışırsa iki ayrı log kaynağın olur. En temizi uygulamanın standart çıktıya yazması ve okumayı journalctl -u servis -n 200 --no-pager ile yapmandır.

    Çıkış kodunu önemsememek. Servis başlamıyorsa systemctl status çıktısındaki Main PID: ... (code=exited, status=1/FAILURE) satırı ve hemen altındaki son log satırları neredeyse her zaman sebebi söyler. Dosyayı kurcalamadan önce bu iki satırı oku.

    Sıkça Sorulan Sorular#

    Unit dosyasını nereye koymalıyım#

    Kendi yazdığın servisler /etc/systemd/system/ altına gider; bu dizin yöneticiye aittir ve paket güncellemeleri buraya dokunmaz. Dağıtımla gelen paketlerin dosyaları /lib/systemd/system/ altındadır ve güncellemede değişebilir. Bir paket servisinin yalnızca birkaç ayarını değiştirmek istiyorsan tüm dosyayı kopyalamak yerine systemctl edit servis.service ile bir üst katman parçası (drop-in) oluşturman daha sağlıklıdır.

    Type=simple mi forking mi kullanmalıyım#

    Uygulaman ön planda çalışıyor ve kendini arka plana atmıyorsa simple doğru seçimdir; modern uygulamaların büyük çoğunluğu böyledir. Uygulama başlatıldığında çatallanıp ana süreçten çıkıyorsa forking kullanmalı ve mümkünse PIDFile= ile gerçek süreç kimliğinin yazıldığı dosyayı belirtmelisin. Emin değilsen uygulamayı elle çalıştır: terminali bırakmıyorsa simple, hemen komut istemine dönüyorsa forking.

    Servis başlamıyor, nereden bakmalıyım#

    Önce systemctl status servis.service çıktısındaki çıkış kodunu ve son satırları oku; sebebin çoğu zaman orada yazar. Ardından journalctl -u servis.service -n 100 --no-pager ile daha geniş bir pencereye bak. En sık üç sebep şudur: ExecStart yolunun yanlış olması, servisin çalıştığı kullanıcının dosyalara erişememesi ve gerekli bir ortam değişkeninin tanımsız kalması. sudo -u uygulama /tam/yol/komut ile komutu elle çalıştırmak üçünü de hızlıca ayırt ettirir.

    Ortam değişkenlerini unit dosyasına yazmak güvenli mi#

    Hassas olmayan değerler için sorun yoktur, ancak parola ve API anahtarı gibi gizli değerleri unit dosyasına yazmamalısın. Unit dosyaları genelde herkes tarafından okunabilir izne sahiptir ve systemctl cat çıktısında görünür. Gizli değerleri yalnızca servis kullanıcısının okuyabildiği ayrı bir dosyaya koyup EnvironmentFile= ile yükle; dosyanın iznini 0640 ve grubunu servis kullanıcısı yap.

    Servisim çöktüğünde otomatik nasıl başlar#

    [Service] bölümüne Restart=always (veya yalnızca hatalarda istiyorsan Restart=on-failure) ve makul bir RestartSec değeri eklemen yeterlidir. Bununla birlikte mutlaka StartLimitIntervalSec ve StartLimitBurst ile bir tavan koy; aksi hâlde açılışta hemen çöken bir uygulama sonsuz döngüye girer ve gerçek hatayı log gürültüsünün altına gömer. Sınır aşıldığında servis failed durumunda kalır, bu da sorunu görmeni sağlar.

    systemd timer mı cron mu kullanmalıyım#

    systemd timer'ları, zamanlanmış işleri de aynı servis çerçevesine sokması nedeniyle avantajlıdır: logları journald'a düşer, kaynak sınırları ve güvenlik direktifleri aynen uygulanır, systemctl list-timers ile bir sonraki çalışma zamanını görürsün. Cron ise çok daha kısa yazılır ve her sistemde vardır. Zaten systemd servisleri yazıyorsan timer kullanmak tutarlılık sağlar; tek satırlık basit görevler için cron pratikliğini korur.

    Kapanış#

    Bir unit dosyası yazmak beş dakikalık iştir ama doğru yazmak, uygulamanın yıllarca sessizce ayakta kalmasını sağlar. Aklında tutman gereken dört alışkanlık şu: ExecStart içinde her zaman tam yol kullan, gizli değerleri EnvironmentFile ile ayır ve izinlerini daralt, Restart yanına mutlaka bir başlatma sınırı koy, ve servisi yazdıktan sonra bir kez systemd-analyze security çalıştırıp önerilen sertleştirmeleri ekle.

    Bu tür yapılandırmaları özgürce yapabilmek için tam yetkili bir sunucuya ihtiyacın var. Kendi servislerini kurmak ve sistemi istediğin gibi sertleştirmek için VDS ve bulut sunucu paketlerimize göz atabilir, kurulum ve bakımı bize bırakmak isterseniz sunucu yönetimi hizmetimizden yararlanabilirsiniz. Servislerinizin verisini güvence altına almak içinse yedekleme çözümlerimiz devrede.

    systemdLinuxServis

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.