Binary yang dijalankan lewat SSH mati begitu sesi terminal ditutup — dan tidak bangkit saat prosesnya crash atau server reboot. Aplikasi production harus hidup tanpa campur tangan manusia: jalan saat boot, bangkit saat crash, mencatat log di satu tempat yang bisa ditelusuri. Di distro Linux modern pekerjaan ini diserahkan kepada systemd, service manager bawaan server. Chapter ini membungkus binary sapa-server menjadi unit systemd lengkap dengan kebijakan restart dan pengerasan keamanan.
Mengapa Aplikasi Perlu Service Manager #
Menjalankan proses latar belakang secara manual (./sapa-server &, lalu nohup) punya tiga kelemahan fatal. Pertama, proses menjadi anak sesi shell — saat sesi berakhir, proses ikut mati. Kedua, saat aplikasi crash dini hari, tidak ada yang memulai ulang sampai seseorang sadar. Ketiga, log tersebar di nohup.out tanpa stempel waktu. systemd menyelesaikan ketiganya: unit berjalan di cgroup sendiri sejak boot, kebijakan Restart membangkitkannya saat mati, dan output stdout/stderr mengalir ke journald.
Anatomi Unit File #
systemd mengenali layanan lewat berkas unit di /etc/systemd/system/sapa.service. Berikut versi beranotasi untuk aplikasi kita:
# /etc/systemd/system/sapa.service
[Unit]
Description=Layanan API sapa
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=sapa
Group=sapa
WorkingDirectory=/opt/sapa
ExecStart=/opt/sapa/sapa-server
Restart=always
RestartSec=2
Environment=APP_PORT=8080
EnvironmentFile=-/etc/sapa/env
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
[Install]
WantedBy=multi-user.targetSeksi [Unit] menjelaskan metadata dan urutan: After/Wants memastikan jaringan siap sebelum aplikasi binding port. Seksi [Service] adalah inti pengaturan proses, dan [Install] menentukan target aktivasi — multi-user.target berarti ikut menyala pada boot normal. Directive penting di [Service]:
Type=simple: ExecStart dianggap proses utama tanpa protokol kesiapan tambahan; varianType=notifydibahas di akhir chapter.User=sapa/Group=sapa: proses berjalan sebagai user khusus tanpa hak istimewa — bukan root.WorkingDirectory=/opt/sapa: direktori kerja proses; path relatif resol ke sini.ExecStart=/opt/sapa/sapa-server: perintah yang dijalankan, memakai path absolut.Restart=always+RestartSec=2: systemd memulai ulang proses dua detik setiap kali mati, apa pun penyebabnya.Environment/EnvironmentFile: menyuntikkan environment variable — dibedah tuntas di chapter Environment Production .NoNewPrivileges,ProtectSystem,PrivateTmp: hardening yang membatasi apa yang bisa dilakukan proses — rinciannya pada tabel di bawah.
Menghasilkan Unit dari Program #
Menyalin-tempel unit seperti di atas ke banyak server rawan melenceng: user dan direktori bisa berbeda tiap mesin. Program systemd-demo.go pada bagian bawah halaman menghasilkan unit dari satu templat text/template — nilai spesifik server diisi lewat struct konfigurasi:
package main
import (
"fmt"
"os"
"text/template"
)
// unitSystemd adalah templat berkas unit systemd untuk layanan sapa.
const unitSystemd = `[Unit]
Description={{ .Deskripsi }}
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ .Pengguna }}
WorkingDirectory={{ .Direktori }}
ExecStart={{ .Direktori }}/sapa-server
Restart=always
RestartSec=2
Environment=APP_PORT=8080
EnvironmentFile=-/etc/sapa/env
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
`
// konfigurasi menyimpan nilai yang diisikan ke templat unit.
type konfigurasi struct {
Deskripsi string
Pengguna string
Direktori string
}
func main() {
kfg := konfigurasi{
Deskripsi: "Layanan API sapa",
Pengguna: "sapa",
Direktori: "/opt/sapa",
}
tmpl := template.Must(template.New("unit").Parse(unitSystemd))
if err := tmpl.Execute(os.Stdout, kfg); err != nil {
fmt.Println("gagal mengeksekusi templat:", err)
os.Exit(1)
}
}Output yang Dihasilkan:
[Unit]
Description=Layanan API sapa
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=sapa
WorkingDirectory=/opt/sapa
ExecStart=/opt/sapa/sapa-server
Restart=always
RestartSec=2
Environment=APP_PORT=8080
EnvironmentFile=-/etc/sapa/env
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetBerkas sapa-service.conf pada bagian bawah halaman adalah hasil cetak program tersebut — deploy script cukup memipakan output ke berkas tujuan. Bagian penting programnya:
- Baris 11 (
const unitSystemd = ...): templat unit disimpan sebagai konstanta string; aksi{{ .Deskripsi }}akan digantikan nilai field struct saat eksekusi. - Baris 29 (
type konfigurasi struct): field struct (Deskripsi,Pengguna,Direktori) menjadi sumber nilai templat — nilai per server diatur di satu tempat. - Baris 38 (
template.Must(...)): mem-parse templat saat inisialisasi; kesalahan sintaks langsung panic saat pengembangan, bukan kejutan saat deploy. - Baris 39 (
tmpl.Execute(os.Stdout, kfg)): mengeksekusi templat dengan datakfgke standard output sehingga bisa dipipa ke berkas tujuan.
Mengaktifkan dan Mengamati Layanan #
Setelah binary dan unit berada di posisinya, urutan perintah berikut mendaftarkan dan menyalakan layanan (systemctl butuh hak administrator di server tujuan):
sudo install -m 0755 sapa-server /opt/sapa/sapa-server
sudo install -m 0644 sapa-service.conf /etc/systemd/system/sapa.service
sudo systemctl daemon-reload
sudo systemctl enable --now sapa
systemctl status sapa
journalctl -u sapa -n 50 --no-pager
journalctl -u sapa -fPerintah daemon-reload wajib setiap kali berkas unit berubah — systemd membaca unit ke memori dan tidak memeriksa perubahan otomatis. Flag enable --now membuat symlink agar layanan mulai saat boot lalu menyalakannya sekarang juga. Log dibaca lewat journalctl -u sapa: -n 50 menampilkan 50 baris terakhir, -f mengikuti log secara live seperti tail -f.
Kebijakan Restart dan Hardening #
Pilihan Restart menentukan kapan systemd membangkitkan kembali proses yang mati:
| Nilai | Bangkit ketika | Cocok untuk |
|---|---|---|
no (bawaan) | Tidak pernah | Proses sekali jalan (batch job) |
on-failure | Exit status ≠ 0 | Layanan yang exit 0 berarti sengaja berhenti |
always | Mati karena apa pun | API server yang harus selalu hidup |
Kombinasi Restart=always dengan aplikasi yang keluar bersih via graceful shutdown tetap aman: systemd menaikkan ulang prosesnya — restart cepat seperti ini justru bagian dari strategi deploy (chapter Rollback dan Recovery
). Direktif hardening membatasi dampak bila aplikasi disusupi:
| Direktif | Efek |
|---|---|
NoNewPrivileges=true | Proses dan anaknya tidak bisa menaikkan hak (mis. lewat sudo/su) |
ProtectSystem=strict | Seluruh filesystem hanya-baca, kecuali direktori yang diizinkan ReadWritePaths |
PrivateTmp=true | Proses mendapat direktori /tmp pribadi, tak terlihat proses lain |
ProtectHome=true | Direktori /home, /root, /run/user tak terlihat proses |
Terakhir, Type=notify memberi tahu systemd kapan aplikasi siap menerima trafik: aplikasi menulis READY=1 ke socket yang alamatnya dibagikan lewat variable NOTIFY_SOCKET (protokol sd_notify). Go tidak punya helper bawaan untuk protokol ini; pola HTTP kita cukup mengandalkan Type=simple plus health check eksternal — dibahas di chapter Containerization
.
Unit yang baik berdiri di atas environment yang disiplin: variabel konsisten, secret tidak ditulis mentah, user dengan hak minimum. Chapter berikutnya merapikan ketiganya.
Referensi #
- Dokumentasi package text/template — referensi resmi sintaks templat yang dipakai program generator unit.
- Dokumentasi package os/signal
— dasar penanganan
SIGTERMyang dikirim systemd saat layanan dihentikan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
systemd-demo.go
Unduh berkaspackage main
import (
"fmt"
"os"
"text/template"
)
// unitSystemd adalah templat berkas unit systemd untuk layanan sapa.
const unitSystemd = `[Unit]
Description={{ .Deskripsi }}
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ .Pengguna }}
WorkingDirectory={{ .Direktori }}
ExecStart={{ .Direktori }}/sapa-server
Restart=always
RestartSec=2
Environment=APP_PORT=8080
EnvironmentFile=-/etc/sapa/env
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
`
// konfigurasi menyimpan nilai yang diisikan ke templat unit.
type konfigurasi struct {
Deskripsi string
Pengguna string
Direktori string
}
func main() {
kfg := konfigurasi{
Deskripsi: "Layanan API sapa",
Pengguna: "sapa",
Direktori: "/opt/sapa",
}
tmpl := template.Must(template.New("unit").Parse(unitSystemd))
if err := tmpl.Execute(os.Stdout, kfg); err != nil {
fmt.Println("gagal mengeksekusi templat:", err)
os.Exit(1)
}
}