Spring Boot Uygulamasını Linux Servisi Olarak Çalıştırma
Production ortamında bir Spring Boot uygulaması deploy ettiğinde, onu sadece çalıştırmak yetmez. Sunucu yeniden başladığında otomatik ayağa kalkması, crash durumunda kendini toparlaması, logların düzgün yönetilmesi ve sistem kaynaklarının kontrol altında tutulması gerekir. İşte tam bu noktada uygulamanı bir Linux servisi olarak yapılandırmak hayat kurtarıcı oluyor. Bu yazıda sıfırdan başlayarak production-ready bir Spring Boot servis kurulumu yapacağız.
Ön Hazırlık: Ortam ve Gereksinimler
Başlamadan önce sistemde birkaç şeyin hazır olması gerekiyor. Java kurulumunu kontrol edelim:
java -version
# output: openjdk version "17.0.9" 2023-10-17
# Eğer kurulu değilse Ubuntu/Debian için:
sudo apt update
sudo apt install openjdk-17-jdk -y
# RHEL/CentOS/Rocky Linux için:
sudo dnf install java-17-openjdk-devel -y
# JAVA_HOME ayarı
echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' >> /etc/environment
source /etc/environment
Uygulamayı çalıştıracak dedicated bir sistem kullanıcısı oluşturalım. Root ile çalıştırmak ciddi güvenlik açığı demektir:
# Sistem kullanıcısı oluştur (login shell olmadan)
sudo useradd -r -s /bin/false -d /opt/myapp -m myapp
# Kullanıcının oluştuğunu doğrula
id myapp
# uid=999(myapp) gid=999(myapp) groups=999(myapp)
Spring Boot JAR Dosyasını Hazırlama
Spring Boot uygulamasını executable JAR olarak build etmemiz gerekiyor. pom.xml dosyasında Spring Boot Maven Plugin’in doğru yapılandırıldığından emin ol:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
executable>true ayarı JAR dosyasının başına bir shell script ekler ve bu sayede JAR’ı doğrudan çalıştırabilirsin. Build alalım:
# Testleri atlayarak hızlıca build et
./mvnw clean package -DskipTests
# Target klasöründe JAR'ı kontrol et
ls -lh target/*.jar
# -rw-r--r-- 1 user user 45M Nov 15 14:32 myapp-1.0.0.jar
JAR dosyasını sunucuya kopyalayalım ve gerekli dizin yapısını oluşturalım:
# Dizin yapısını oluştur
sudo mkdir -p /opt/myapp/{bin,config,logs,temp}
# JAR'ı kopyala
sudo cp target/myapp-1.0.0.jar /opt/myapp/bin/myapp.jar
# Symbolic link oluştur (versiyon bağımsız başvuru için)
sudo ln -sf /opt/myapp/bin/myapp.jar /opt/myapp/bin/current.jar
# Sahipliği ayarla
sudo chown -R myapp:myapp /opt/myapp
# JAR'ı çalıştırılabilir yap
sudo chmod 500 /opt/myapp/bin/myapp.jar
Uygulama Konfigürasyonu
Production ortamı için ayrı bir application-prod.properties dosyası oluşturalım:
sudo nano /opt/myapp/config/application-prod.properties
# Server ayarları
server.port=8080
server.tomcat.threads.max=200
server.tomcat.accept-count=100
# Veritabanı bağlantısı
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=dbuser
spring.datasource.password=supersecretpassword
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
# Log ayarları
logging.file.name=/opt/myapp/logs/application.log
logging.level.root=WARN
logging.level.com.mycompany=INFO
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# Actuator (sadece localhost'tan erişim)
management.server.port=8081
management.endpoints.web.exposure.include=health,info,metrics
Konfigürasyon dosyasına hassas bilgiler içerdiği için sıkı izin ver:
sudo chown myapp:myapp /opt/myapp/config/application-prod.properties
sudo chmod 600 /opt/myapp/config/application-prod.properties
systemd Servis Dosyası Oluşturma
Modern Linux dağıtımlarının tamamında systemd kullanılıyor. Servis tanım dosyamızı oluşturalım:
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Spring Boot Application
Documentation=https://wiki.mycompany.com/myapp
After=network.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# Ortam değişkenleri
Environment="JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64"
Environment="SPRING_PROFILES_ACTIVE=prod"
Environment="JAVA_OPTS=-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
# Başlatma komutu
ExecStart=/usr/bin/java
${JAVA_OPTS}
-Dspring.config.location=/opt/myapp/config/
-Djava.io.tmpdir=/opt/myapp/temp
-jar /opt/myapp/bin/current.jar
# Graceful shutdown - 60 saniye bekle
TimeoutStopSec=60
KillSignal=SIGTERM
KillMode=mixed
# Yeniden başlatma politikası
Restart=on-failure
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=5
# Güvenlik kısıtlamaları
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/myapp/logs /opt/myapp/temp
# Standart output/error yönetimi
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
Bu servis dosyasındaki kritik parametreleri açıklayalım:
- After=postgresql.service: PostgreSQL başlamadan uygulama başlamasın
- Restart=on-failure: Sadece hata durumunda yeniden başlat, elle durdurduğunda başlatma
- StartLimitBurst=5: 300 saniye içinde 5 kez başarısız olursa servisi durdur
- NoNewPrivileges=true: Process’in yetki yükseltmesini engelle
- PrivateTmp=true: Izole geçici dizin kullan
- ProtectSystem=strict: Sistem dosyalarına yazma erişimini kısıtla
- KillMode=mixed: Ana process’e SIGTERM gönder, yanıt vermezse tüm gruba SIGKILL
Servisi Aktif Etme ve Başlatma
# systemd daemon'ı yeniden yükle
sudo systemctl daemon-reload
# Servisi etkinleştir (boot'ta otomatik başlasın)
sudo systemctl enable myapp
# Servisi başlat
sudo systemctl start myapp
# Durum kontrolü
sudo systemctl status myapp
Başarılı bir çıktı şöyle görünür:
● myapp.service - MyApp Spring Boot Application
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2024-11-15 14:45:23 UTC; 2min ago
Main PID: 12345 (java)
Tasks: 42 (limit: 4915)
Memory: 387.2M
CPU: 8.543s
CGroup: /system.slice/myapp.service
└─12345 /usr/bin/java -Xms512m -Xmx1024m ...
Log Yönetimi: journald ve Logrotate
systemd ile birlikte journald otomatik olarak logları toplar. Logları takip etmek için:
# Canlı log takibi
sudo journalctl -u myapp -f
# Son 100 satır
sudo journalctl -u myapp -n 100
# Belirli zaman aralığı
sudo journalctl -u myapp --since "2024-11-15 10:00" --until "2024-11-15 12:00"
# Sadece hata logları
sudo journalctl -u myapp -p err
# JSON formatında
sudo journalctl -u myapp -o json-pretty | head -50
Uygulama kendi log dosyasına da yazıyorsa logrotate yapılandırması ekleyelim:
sudo nano /etc/logrotate.d/myapp
/opt/myapp/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
copytruncate
dateext
dateformat -%Y%m%d
su myapp myapp
}
- copytruncate: Dosyayı kopyalayıp sıfırlar, uygulamayı yeniden başlatmaya gerek kalmaz
- rotate 30: 30 günlük log sakla
- compress: Eski logları gzip ile sıkıştır
- delaycompress: En yeni rotate edilmiş dosyayı sıkıştırma (bir sonraki rotasyona bırak)
JVM Ayarlarını Production İçin Optimize Etme
Spring Boot uygulamaları için JVM parametreleri doğru ayarlanmazsa memory leak’ler ve GC duraksamaları ciddi sorun çıkarır. Daha kapsamlı bir JVM konfigürasyonu:
sudo nano /opt/myapp/config/jvm.options
# Heap ayarları - sistemin %40-50'si
-Xms512m
-Xmx2g
# GC: G1GC genellikle web uygulamaları için iyi seçim
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:+G1UseAdaptiveIHOP
# GC Logging (troubleshooting için kritik)
-Xlog:gc*:file=/opt/myapp/logs/gc.log:time,uptime:filecount=5,filesize=20m
# OOM durumunda heap dump al
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/myapp/temp/heapdump.hprof
# OOM'da JVM'i yeniden başlat
-XX:+ExitOnOutOfMemoryError
# Metaspace limiti
-XX:MaxMetaspaceSize=256m
# DNS cache (cloud ortamlar için önemli)
-Djava.security.egd=file:/dev/./urandom
-Dnetworkaddress.cache.ttl=60
Servis dosyasını bu dosyayı okuyacak şekilde güncelleyelim:
sudo nano /etc/systemd/system/myapp.service
ExecStart satırını şöyle değiştirelim:
EnvironmentFile=-/opt/myapp/config/jvm.options
ExecStart=/usr/bin/java
@/opt/myapp/config/jvm.options
-Dspring.config.location=/opt/myapp/config/
-Dspring.profiles.active=prod
-Djava.io.tmpdir=/opt/myapp/temp
-jar /opt/myapp/bin/current.jar
Health Check ve Monitoring Entegrasyonu
Spring Boot Actuator’ı aktif ettiğimize göre, sistemin sağlığını düzenli kontrol eden bir script yazalım:
sudo nano /opt/myapp/bin/healthcheck.sh
#!/bin/bash
APP_NAME="myapp"
HEALTH_URL="http://localhost:8081/actuator/health"
LOG_FILE="/opt/myapp/logs/healthcheck.log"
ALERT_EMAIL="[email protected]"
check_health() {
local response
local http_code
response=$(curl -sf -o /dev/null -w "%{http_code}"
--max-time 10
--connect-timeout 5
"$HEALTH_URL" 2>/dev/null)
http_code=$?
if [ "$response" = "200" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') [OK] $APP_NAME healthy" >> "$LOG_FILE"
return 0
else
echo "$(date '+%Y-%m-%d %H:%M:%S') [FAIL] $APP_NAME unhealthy - HTTP: $response" >> "$LOG_FILE"
# Servisi yeniden başlatmayı dene
echo "$(date '+%Y-%m-%d %H:%M:%S') [ACTION] Restarting $APP_NAME service..." >> "$LOG_FILE"
sudo systemctl restart "$APP_NAME"
# Mail gönder (mailutils kurulu olmalı)
echo "$APP_NAME servisi yeniden başlatıldı. Kontrol et." |
mail -s "[$APP_NAME] Service Restart Alert" "$ALERT_EMAIL"
return 1
fi
}
check_health
sudo chmod 750 /opt/myapp/bin/healthcheck.sh
sudo chown myapp:myapp /opt/myapp/bin/healthcheck.sh
# Her 5 dakikada bir çalışacak cron job
sudo crontab -u myapp -e
# Şunu ekle:
# */5 * * * * /opt/myapp/bin/healthcheck.sh
Deployment Script: Sıfır Kesintili Güncelleme
Yeni versiyon deploy etmek için bir script hazırlayalım:
sudo nano /opt/myapp/bin/deploy.sh
#!/bin/bash
set -euo pipefail
APP_NAME="myapp"
APP_DIR="/opt/myapp"
BACKUP_DIR="/opt/myapp/backup"
NEW_JAR="${1:?'Yeni JAR dosyasini belirtin: deploy.sh <jar_path>'}"
DEPLOY_DATE=$(date +%Y%m%d_%H%M%S)
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Backup dizini oluştur
mkdir -p "$BACKUP_DIR"
# Mevcut JAR'ı yedekle
if [ -f "$APP_DIR/bin/current.jar" ]; then
log "Mevcut JAR yedekleniyor..."
cp -p "$APP_DIR/bin/current.jar" "$BACKUP_DIR/myapp_${DEPLOY_DATE}.jar"
log "Yedek: $BACKUP_DIR/myapp_${DEPLOY_DATE}.jar"
fi
# Yeni JAR'ı kopyala
log "Yeni JAR kopyalaniyor: $NEW_JAR"
cp "$NEW_JAR" "$APP_DIR/bin/myapp-new.jar"
chown myapp:myapp "$APP_DIR/bin/myapp-new.jar"
chmod 500 "$APP_DIR/bin/myapp-new.jar"
# Servisi durdur
log "Servis durduruluyor..."
systemctl stop "$APP_NAME"
sleep 5
# Yeni JAR'ı aktif et
mv "$APP_DIR/bin/myapp-new.jar" "$APP_DIR/bin/myapp.jar"
ln -sf "$APP_DIR/bin/myapp.jar" "$APP_DIR/bin/current.jar"
# Servisi başlat
log "Servis baslatiliyor..."
systemctl start "$APP_NAME"
# 30 saniye bekle ve health check yap
log "Health check bekleniyor (30 saniye)..."
sleep 30
if systemctl is-active --quiet "$APP_NAME"; then
log "Deployment basarili! $APP_NAME calisiyor."
# 7 günden eski yedekleri temizle
find "$BACKUP_DIR" -name "*.jar" -mtime +7 -delete
log "Eski yedekler temizlendi."
else
log "HATA: Deployment basarisiz! Rollback yapiliyor..."
# Rollback
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.jar 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
cp "$LATEST_BACKUP" "$APP_DIR/bin/myapp.jar"
systemctl start "$APP_NAME"
log "Rollback tamamlandi: $LATEST_BACKUP"
else
log "Yedek bulunamadi! Manuel mudahale gerekiyor."
exit 1
fi
fi
sudo chmod 750 /opt/myapp/bin/deploy.sh
# Kullanım:
# sudo /opt/myapp/bin/deploy.sh /tmp/myapp-2.0.0.jar
Güvenlik Duvarı ve Network Ayarları
# UFW ile (Ubuntu/Debian)
sudo ufw allow from 10.0.0.0/8 to any port 8080 proto tcp comment "MyApp - Internal"
sudo ufw allow from 10.0.0.0/8 to any port 8081 proto tcp comment "MyApp Actuator - Internal"
sudo ufw deny 8080
sudo ufw deny 8081
# firewalld ile (RHEL/Rocky Linux)
sudo firewall-cmd --permanent --new-zone=myapp
sudo firewall-cmd --permanent --zone=myapp --add-source=10.0.0.0/8
sudo firewall-cmd --permanent --zone=myapp --add-port=8080/tcp
sudo firewall-cmd --permanent --zone=myapp --add-port=8081/tcp
sudo firewall-cmd --reload
Actuator endpoint’lerine sadece internal network’ten erişilmeli. 8081 portu dışarıya asla açılmamalı.
Sorun Giderme: Sık Karşılaşılan Durumlar
Servis başlamıyorsa önce şunu kontrol et:
# Detaylı hata mesajları
sudo journalctl -u myapp -n 50 --no-pager
# Process'in çalışıp çalışmadığı
sudo systemctl status myapp -l
# Port kullanımda mı?
sudo ss -tlnp | grep 8080
# Java process durumu
sudo -u myapp jps -lvm
# Dosya izinlerini kontrol et
sudo -u myapp ls -la /opt/myapp/bin/
sudo -u myapp cat /opt/myapp/config/application-prod.properties
Sonuç
Spring Boot uygulamasını systemd servisi olarak yapılandırmak tek seferlik bir iş gibi görünse de aslında her adım production güvenilirliği açısından önem taşıyor. Dedicated kullanıcı, doğru JVM parametreleri, güvenlik kısıtlamaları, log rotasyonu ve otomatik health check birlikte çalıştığında gece 3’te telefona bakma ihtiyacın ciddi ölçüde azalıyor.
Özellikle şu noktalara dikkat etmeni öneririm: StartLimitBurst değerini ortamına göre ayarla, başarısız başlatmalar sayısı sisteme göre değişir. GC log’larını mutlaka aktif tut, production’da bellek sorunlarını retrospektif analiz edemezsen çok zor durumda kalırsın. Deploy script’ini pipline’ına entegre et ve manuel deploy yapmayı mümkün olduğunca azalt. Son olarak Actuator endpoint’lerini sadece internal network’e aç, bu detay production ortamında sık atlanan ama güvenlik açısından kritik bir noktadır.
