Go Uygulamasını Systemd Servis Olarak Çalıştırma
Üretim ortamında bir Go uygulaması yazdınız, testler geçti, binary hazır. Peki bu uygulamayı sunucuda kalıcı olarak nasıl çalıştıracaksınız? Ekranı kapatınca process ölüyor, sunucu yeniden başlayınca uygulama ayağa kalkmıyor. İşte tam bu noktada systemd devreye giriyor. Systemd ile Go uygulamanızı gerçek bir sistem servisi haline getirip, otomatik başlatma, log yönetimi ve crash recovery gibi özellikleri kazanabilirsiniz.
Go Binary’sini Hazırlamak
Önce sağlam bir binary oluşturmakla başlayalım. Go’nun güzel yanlarından biri statik binary üretebilmesi. Bu sayede hedef sunucuda Go runtime kurulu olmak zorunda değil.
# Üretim için optimize binary derle
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o myapp ./cmd/myapp
# Binary boyutunu kontrol et
ls -lh myapp
# Binary'nin gerçekten statik olduğunu doğrula
file myapp
ldd myapp
CGO_ENABLED=0: C bağımlılıklarını devre dışı bırakır, tam statik binary üretir.
-ldflags=”-w -s”: Debug sembollerini ve DWARF bilgilerini çıkarır, binary boyutunu küçültür.
GOOS=linux GOARCH=amd64: Çapraz derleme için hedef işletim sistemi ve mimariyi belirtir. Geliştirme makineniz macOS veya Windows olsa bile Linux binary üretirsiniz.
Binary’yi sunucuya kopyalayın ve uygun bir dizine yerleştirin:
# Sunucuda dizin yapısını oluştur
sudo mkdir -p /opt/myapp/bin
sudo mkdir -p /opt/myapp/config
sudo mkdir -p /opt/myapp/logs
# Binary'yi kopyala
sudo cp myapp /opt/myapp/bin/myapp
sudo chmod +x /opt/myapp/bin/myapp
# Config dosyasını kopyala
sudo cp config.yaml /opt/myapp/config/config.yaml
Dedicated Kullanıcı Oluşturmak
Uygulamayı root ile çalıştırmak büyük bir güvenlik riski. Her production servis için ayrı bir kullanıcı açın:
# Sistem kullanıcısı oluştur (login shell yok, home dizini yok)
sudo useradd --system --no-create-home --shell /bin/false myapp
# Dizin sahipliğini ayarla
sudo chown -R myapp:myapp /opt/myapp
# Log dizini için özel izinler
sudo chmod 750 /opt/myapp/logs
# Kullanıcının oluşturulduğunu doğrula
id myapp
–system: UID aralığı olarak düşük numaralı sistem UID’lerini kullanır.
–no-create-home: Home dizini oluşturmaz, gereksiz risk azaltır.
–shell /bin/false: Bu kullanıcıyla SSH veya terminal girişi yapılamaz.
Temel Systemd Unit Dosyası
Şimdi asıl konuya gelelim. /etc/systemd/system/myapp.service dosyasını oluşturun:
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Go Uygulamasi
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config/config.yaml
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
Dosyayı kaydettikten sonra servisi aktifleştirin:
# Systemd'yi yeni unit dosyasından haberdar et
sudo systemctl daemon-reload
# Servisi başlat
sudo systemctl start myapp
# Otomatik başlatmayı etkinleştir
sudo systemctl enable myapp
# Durumu kontrol et
sudo systemctl status myapp
Kapsamlı ve Güvenli Unit Dosyası
Temel unit dosyası çalışır ama üretim ortamı için daha fazlasına ihtiyaç var. Güvenlik kısıtlamaları, environment değişkenleri ve kaynak limitleri ekleyelim:
[Unit]
Description=MyApp Go Uygulamasi
Documentation=https://github.com/yourorg/myapp
After=network.target network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# Calistirilacak komut
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
# Environment degiskenleri
Environment=APP_ENV=production
Environment=LOG_LEVEL=info
EnvironmentFile=-/opt/myapp/config/env
# Restart politikasi
Restart=always
RestartSec=10s
StartLimitIntervalSec=60s
StartLimitBurst=3
# Kaynak limitleri
LimitNOFILE=65535
LimitNPROC=4096
# Guvenlik kisitlamalari
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/logs /opt/myapp/data
CapabilityBoundingSet=
AmbientCapabilities=
# Log ayarlari
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Timeout ayarlari
TimeoutStartSec=30s
TimeoutStopSec=30s
[Install]
WantedBy=multi-user.target
Bu dosyadaki kritik parametreleri açıklayalım:
After=postgresql.service: Servis başlamadan önce PostgreSQL’in hazır olmasını bekler.
Requires=postgresql.service: PostgreSQL çökerse bu servis de durur.
ExecReload=/bin/kill -HUP $MAINPID: Go uygulamanız SIGHUP sinyalini dinliyorsa config reload yapabilirsiniz.
Restart=always: Uygulama herhangi bir nedenle çökerse otomatik yeniden başlatır.
StartLimitBurst=3: 60 saniye içinde 3 kezden fazla çökerse systemd yeniden başlatmayı durdurur, böylece crash loop’a girmez.
NoNewPrivileges=true: Process’in yeni ayrıcalıklar kazanmasını engeller.
PrivateTmp=true: /tmp dizinini izole eder, diğer processlerden gizler.
ProtectSystem=strict: Dosya sistemini read-only yapar, sadece ReadWritePaths’te belirtilen yerlere yazabilir.
LimitNOFILE=65535: Açık dosya/bağlantı limitini artırır. Yüksek trafikli HTTP serverlarda çok önemli.
Environment Dosyası ile Gizli Bilgi Yönetimi
Şifreler ve API anahtarlarını unit dosyasına yazmayın. Ayrı bir env dosyası kullanın:
sudo nano /opt/myapp/config/env
DATABASE_URL=postgres://user:secretpassword@localhost:5432/myappdb
REDIS_URL=redis://localhost:6379/0
JWT_SECRET=supersecretjwtkey123
API_KEY=external-api-key-here
SMTP_PASSWORD=emailpassword
# Dosya izinlerini kısıtla, sadece myapp kullanıcısı okuyabilsin
sudo chown myapp:myapp /opt/myapp/config/env
sudo chmod 600 /opt/myapp/config/env
# Root dışında kimse okuyamasın diye kontrol et
sudo -u www-data cat /opt/myapp/config/env # Permission denied vermeli
Unit dosyasındaki EnvironmentFile=-/opt/myapp/config/env satırındaki - işaretine dikkat edin. Bu işaret dosya yoksa hata vermemesini sağlar, servis yine de başlar.
Go Uygulamanızda Graceful Shutdown
Systemd servisi olarak çalışan Go uygulamanızın SIGTERM sinyalini düzgün yakalaması gerekiyor. Aksi halde systemd TimeoutStopSec dolduğunda SIGKILL gönderir ve açık bağlantılar, yarım kalan işlemler sorun çıkarır:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: setupRoutes(),
}
// Serveri goroutine icinde baslat
go func() {
log.Println("Server :8080 portunda dinliyor")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server baslatma hatasi: %v", err)
}
}()
// OS sinyallerini yakala
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Sinyal gelene kadar bekle
sig := <-quit
log.Printf("Sinyal alindi: %v, graceful shutdown basliyor...", sig)
// 30 saniye timeout ile graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Zorla kapama gerekti: %v", err)
}
log.Println("Server temiz sekilde kapandi")
}
Bu yapı sayesinde systemctl stop myapp veya systemctl restart myapp komutlarında uygulama mevcut requestleri tamamlayıp düzgünce kapanır.
Log Yönetimi
Systemd journald ile entegre log yönetimi oldukça güçlü:
# Servis loglarını canlı izle
sudo journalctl -u myapp -f
# Son 100 satır log
sudo journalctl -u myapp -n 100
# Bugünkü loglar
sudo journalctl -u myapp --since today
# Belirli zaman aralığı
sudo journalctl -u myapp --since "2024-01-15 10:00:00" --until "2024-01-15 11:00:00"
# Sadece hata logları
sudo journalctl -u myapp -p err
# JSON formatında çıktı (log analizi için)
sudo journalctl -u myapp -o json | jq .
Eğer uygulamanın log dosyasına da yazmasını istiyorsanız, systemd ile birlikte çalışan bir yapı kurabilirsiniz:
# Log rotation için logrotate konfigürasyonu
sudo nano /etc/logrotate.d/myapp
/opt/myapp/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 myapp myapp
postrotate
systemctl kill --kill-who=main --signal=USR1 myapp
endscript
}
Servis Yönetimi ve Sorun Giderme
Günlük operasyonlarda kullanacağınız komutlar:
# Servis durumu (detaylı)
sudo systemctl status myapp -l
# Başlat / Durdur / Yeniden başlat
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
# Config reload (ExecReload tanımlıysa)
sudo systemctl reload myapp
# Otomatik başlatmayı devre dışı bırak
sudo systemctl disable myapp
# Servisin aktif olup olmadığını kontrol et (script için)
systemctl is-active myapp && echo "Calisiyor" || echo "Durdu"
# Servis dosyasını değiştirdikten sonra
sudo systemctl daemon-reload
sudo systemctl restart myapp
# Tüm failed servisleri listele
systemctl --failed
# Servis bağımlılıklarını görüntüle
systemctl list-dependencies myapp
Bir servis başlamıyorsa ilk bakılacak yer:
# Detaylı başlatma hatası için
sudo journalctl -u myapp -n 50 --no-pager
# Systemd'nin servis hakkındaki son olayları
sudo systemctl status myapp --output=verbose
# Binary'nin çalışıp çalışmadığını manuel test et
sudo -u myapp /opt/myapp/bin/myapp --config /opt/myapp/config/config.yaml
# Port çakışması kontrolü
sudo ss -tlnp | grep 8080
Birden Fazla Instance Çalıştırmak
Aynı uygulamanın birden fazla instance’ını çalıştırmak için template unit dosyaları kullanabilirsiniz. Bu özellikle farklı portlarda veya farklı config’lerle aynı binary’yi çalıştırmanız gerektiğinde işe yarar:
sudo nano /etc/systemd/system/[email protected]
[Unit]
Description=MyApp Go Uygulamasi - Instance %i
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config/config-%i.yaml
Restart=on-failure
RestartSec=5s
Environment=INSTANCE=%i
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp-%i
[Install]
WantedBy=multi-user.target
# Farkli instance'ları baslat
sudo systemctl start myapp@production
sudo systemctl start myapp@staging
sudo systemctl enable myapp@production
# Instance loglarını ayrı takip et
sudo journalctl -u myapp@production -f
Health Check ve Watchdog Entegrasyonu
Systemd’nin watchdog özelliği, uygulamanızın gerçekten sağlıklı çalışıp çalışmadığını periyodik olarak kontrol eder:
[Service]
# ... diger ayarlar ...
WatchdogSec=30s
NotifyAccess=main
Type=notify
Go uygulamanızda watchdog bildirimlerini göndermek için:
package main
import (
"log"
"net"
"os"
"time"
)
// Systemd'ye hazir bildirimi gonder
func sdNotify(state string) error {
socketAddr := os.Getenv("NOTIFY_SOCKET")
if socketAddr == "" {
return nil
}
conn, err := net.Dial("unixgram", socketAddr)
if err != nil {
return err
}
defer conn.Close()
_, err = conn.Write([]byte(state))
return err
}
func main() {
// Uygulamayi baslat
if err := initApp(); err != nil {
log.Fatal(err)
}
// Systemd'ye hazir oldugunu bildir
if err := sdNotify("READY=1"); err != nil {
log.Printf("Systemd bildirim hatasi: %v", err)
}
// Watchdog dongusu
go func() {
interval := 15 * time.Second
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
if isHealthy() {
sdNotify("WATCHDOG=1")
}
}
}()
// Ana uygulama dongusu
runApp()
}
func isHealthy() bool {
// Veritabani baglantisi, kritik servisler vs kontrol et
return true
}
Bu yapıyla uygulama 30 saniye boyunca watchdog sinyali gönderemezse systemd servisi otomatik yeniden başlatır. Gerçek bir hayatta kurtarma mekanizması.
Deployment Workflow
Binary güncelleme sürecini de otomatize etmek için basit bir deploy scripti:
#!/bin/bash
set -e
BINARY_PATH="/opt/myapp/bin/myapp"
NEW_BINARY="$1"
SERVICE_NAME="myapp"
if [ -z "$NEW_BINARY" ]; then
echo "Kullanim: $0 <yeni-binary-yolu>"
exit 1
fi
echo "Yeni binary kontrol ediliyor..."
if ! file "$NEW_BINARY" | grep -q "ELF 64-bit"; then
echo "Hata: Gecersiz binary dosyasi"
exit 1
fi
echo "Eski binary yedekleniyor..."
sudo cp "$BINARY_PATH" "${BINARY_PATH}.backup"
echo "Yeni binary kopyalaniyor..."
sudo cp "$NEW_BINARY" "$BINARY_PATH"
sudo chmod +x "$BINARY_PATH"
sudo chown myapp:myapp "$BINARY_PATH"
echo "Servis yeniden baslatiliyor..."
sudo systemctl restart "$SERVICE_NAME"
echo "Servis durumu kontrol ediliyor..."
sleep 3
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo "Deploy basarili! Servis calisiyor."
else
echo "Hata! Servis baslamadi, geri donuluyor..."
sudo cp "${BINARY_PATH}.backup" "$BINARY_PATH"
sudo systemctl restart "$SERVICE_NAME"
exit 1
fi
sudo chmod +x /usr/local/bin/deploy-myapp
Sonuç
Go uygulamanızı systemd servisi olarak çalıştırmak birkaç adımlık bir süreç olsa da doğru yapılandırıldığında size çok güçlü bir altyapı sunuyor. Özet olarak dikkat etmeniz gereken noktalar şunlar:
- Her zaman dedicated sistem kullanıcısıyla çalıştırın, root’tan kaçının.
ProtectSystem,NoNewPrivilegesvePrivateTmpgibi güvenlik direktiflerini mutlaka ekleyin.- Gizli bilgileri unit dosyasına değil, izinleri kısıtlanmış env dosyasına yazın.
- Go uygulamanızda SIGTERM sinyalini yakalayan graceful shutdown kodunu mutlaka implement edin.
StartLimitBurstile crash loop senaryosuna karşı kendinizi koruyun.- Watchdog entegrasyonu ile sadece process ayakta olması değil, gerçekten sağlıklı çalışması durumunu da izleyin.
Bu yapıyı bir kez kurduğunuzda sunucu restartları, crash’ler ve deployment’lar çok daha az stresli hale geliyor. Systemd’nin log yönetimi, bağımlılık sistemi ve kaynak limitlerinden tam olarak yararlandığınızda Go uygulamanız gerçek anlamda production-ready bir servis haline geliyor.
