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:

ini
# /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.target

Seksi [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]:

  1. Type=simple: ExecStart dianggap proses utama tanpa protokol kesiapan tambahan; varian Type=notify dibahas di akhir chapter.
  2. User=sapa / Group=sapa: proses berjalan sebagai user khusus tanpa hak istimewa — bukan root.
  3. WorkingDirectory=/opt/sapa: direktori kerja proses; path relatif resol ke sini.
  4. ExecStart=/opt/sapa/sapa-server: perintah yang dijalankan, memakai path absolut.
  5. Restart=always + RestartSec=2: systemd memulai ulang proses dua detik setiap kali mati, apa pun penyebabnya.
  6. Environment / EnvironmentFile: menyuntikkan environment variable — dibedah tuntas di chapter Environment Production .
  7. 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:

go
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:

text
[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.target

Berkas sapa-service.conf pada bagian bawah halaman adalah hasil cetak program tersebut — deploy script cukup memipakan output ke berkas tujuan. Bagian penting programnya:

  1. Baris 11 (const unitSystemd = ...): templat unit disimpan sebagai konstanta string; aksi {{ .Deskripsi }} akan digantikan nilai field struct saat eksekusi.
  2. Baris 29 (type konfigurasi struct): field struct (Deskripsi, Pengguna, Direktori) menjadi sumber nilai templat — nilai per server diatur di satu tempat.
  3. Baris 38 (template.Must(...)): mem-parse templat saat inisialisasi; kesalahan sintaks langsung panic saat pengembangan, bukan kejutan saat deploy.
  4. Baris 39 (tmpl.Execute(os.Stdout, kfg)): mengeksekusi templat dengan data kfg ke 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):

bash
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 -f

Perintah 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:

NilaiBangkit ketikaCocok untuk
no (bawaan)Tidak pernahProses sekali jalan (batch job)
on-failureExit status ≠ 0Layanan yang exit 0 berarti sengaja berhenti
alwaysMati karena apa punAPI 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:

DirektifEfek
NoNewPrivileges=trueProses dan anaknya tidak bisa menaikkan hak (mis. lewat sudo/su)
ProtectSystem=strictSeluruh filesystem hanya-baca, kecuali direktori yang diizinkan ReadWritePaths
PrivateTmp=trueProses mendapat direktori /tmp pribadi, tak terlihat proses lain
ProtectHome=trueDirektori /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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

systemd-demo.go

Unduh berkas
go
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)
	}
}