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.
| Type | Ne zaman "başladı" sayılır | Tipik kullanım |
|---|---|---|
simple | ExecStart süreci çalıştırıldığı an | Ön planda çalışan çoğu uygulama |
exec | Süreç gerçekten çalıştırılabildiğinde | simple'ın daha doğru hâli |
forking | Ana süreç çatallanıp çıktığında | Kendini arka plana atan klasik daemon'lar |
oneshot | Komut tamamlanıp çıktığında | Tek seferlik görevler, timer'lar |
notify | Süreç systemd'ye hazır olduğunu bildirince | sd_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:
- Dosyayı
/etc/systemd/system/altına yaz. sudo systemctl daemon-reload— systemd unit dosyalarını yeniden okur.sudo systemctl enable --now servis.service— açılışta başlat ve hemen çalıştır.systemctl status servis.service— durumu ve son log satırlarını gör.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.