systemd adalah program pertama yang jalan saat server Linux menyala, dan dia yang menyalakan, menjaga, dan mencatat semua service, program yang harus hidup terus seperti catatan-api. Tiap service punya unit file, mirip kartu SOP seorang karyawan: siapa yang bekerja (User=), bekalnya apa (EnvironmentFile=), apa yang dilakukan kalau dia pingsan (Restart=), dan apakah dia masuk setiap pagi (enable). Dasar systemctl, daemon-reload, dan journalctl ada di pelajaran systemd & Services, track Terminal. Di sini kita menulis unit production untuk catatan-api di web-1.
🧪 Tebakan cepat: kamu menjalankan
sudo systemctl enable catatan-api, lalusystemctl is-active catatan-api. Apa yang dicetak,activeatauinactive? Tebak dulu, jawabannya ada di bagian "enable dan start: dua hal berbeda".
Unit file catatan-api
# /etc/systemd/system/catatan-api.service
[Unit]
Description=catatan-api
After=network.target postgresql.service
Requires=postgresql.service
[Service]
User=www-data
WorkingDirectory=/srv/catatan-api
ExecStart=/usr/bin/node app.js
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/etc/catatan-api.env
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
User=www-data: app jalan sebagaiwww-data, user yang boleh menulis distorage/(pelajaran File System & Permissions). Tanpa baris ini, app jalan sebagai root.WorkingDirectory=danExecStart=: folder kerja dan perintahnya.ExecStartwajib path lengkap (/usr/bin/node), karena systemd gak memakaiPATHmilikmu.Environment=mengisi variabel yang gak rahasia.EnvironmentFile=membacaKEY=VALUEdari file: systemd membacanya sebagai root, lalu memberikannya ke app. Karena itu file secret-nya boleh600milik root.Restart=on-failuredanRestartSec=5: kalau app crash, nyalakan lagi lima detik kemudian.WantedBy=multi-user.targetdi[Install]: dipakaisystemctl enable, artinya service ikut dijalankan saat sistem mencapai target multi-user, keadaan normal server tanpa desktop.
After= dan Requires=
| Baris | Artinya | Kalau PostgreSQL dihentikan |
|---|---|---|
After=postgresql.service |
cuma urutan: catatan-api dinyalakan setelah PostgreSQL | catatan-api tetap jalan, query-nya gagal |
Requires=postgresql.service |
ketergantungan keras: PostgreSQL ikut dinyalakan | catatan-api ikut dihentikan |
After= hanya mengatur urutan start. Requires= juga membuat service berhenti kalau dependensinya gagal atau dihentikan. Biasanya ditulis berpasangan, seperti di unit catatan-api. Versi lunaknya Wants=: ikut dinyalakan, tapi gak ikut mematikan.
Restart=: on-failure atau always
Restart= |
Exit 0 (keluar normal) | Exit selain 0 (crash) | systemctl stop |
|---|---|---|---|
no (bawaan) |
diam | diam | diam |
on-failure |
diam | restart | diam |
always |
restart | restart | diam |
Untuk server HTTP seperti catatan-api, on-failure paling pas: crash dibangkitkan, keluar normal dibiarkan. always cocok untuk worker yang memang sengaja keluar dengan exit 0 dan harus jalan lagi. systemctl stop gak pernah memicu restart. Ini journal saat PostgreSQL sempat mati dan app crash:
Oct 09 09:01:12 web-1 node[5120]: Database gagal tersambung: connect ECONNREFUSED 127.0.0.1:5432
Oct 09 09:01:12 web-1 systemd[1]: catatan-api.service: Main process exited, code=exited, status=1/FAILURE
Oct 09 09:01:12 web-1 systemd[1]: catatan-api.service: Failed with result 'exit-code'.
Oct 09 09:01:17 web-1 systemd[1]: catatan-api.service: Scheduled restart job, restart counter is at 1.
Oct 09 09:01:17 web-1 systemd[1]: Started catatan-api.service - catatan-api.
Oct 09 09:01:17 web-1 node[5207]: Server jalan di http://localhost:3000
Restart gak membetulkan config. Kalau app gagal karena secret-nya kosong, dia gagal lagi setiap lima detik, dan restart counter naik terus.
enable dan start: dua hal berbeda
start menyalakan sekarang. enable membuat symlink di multi-user.target.wants supaya service ikut nyala setiap boot, tanpa menyalakannya sekarang:
deploy@web-1:~$ sudo systemctl enable catatan-api
Created symlink /etc/systemd/system/multi-user.target.wants/catatan-api.service → /etc/systemd/system/catatan-api.service.
deploy@web-1:~$ systemctl is-active catatan-api; echo $?
inactive
3
deploy@web-1:~$ sudo systemctl start catatan-api
deploy@web-1:~$ systemctl is-active catatan-api; echo $?
active
0
- Setelah
enablesaja,is-activemencetakinactive. Itu jawaban tebakannya.sudo systemctl enable --now catatan-apimelakukan dua-duanya.disablemembatalkanenable. systemctl is-activemencetak keadaannya dan keluar dengan exit code 0 kalau active, 3 kalau tidak. Script deploy bisa langsung memakainya:if systemctl is-active --quiet catatan-api; then ....--quietmembuatnya gak mencetak apa pun.
Membaca status dan journal
deploy@web-1:~$ systemctl status catatan-api
● catatan-api.service - catatan-api
Loaded: loaded (/etc/systemd/system/catatan-api.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-10-09 09:00:09 WIB; 3s ago
Main PID: 5120 (node)
Tasks: 7 (limit: 2274)
Memory: 41.6M (peak: 42.0M)
CPU: 389ms
CGroup: /system.slice/catatan-api.service
└─5120 /usr/bin/node app.js
Oct 09 09:00:09 web-1 systemd[1]: Started catatan-api.service - catatan-api.
Oct 09 09:00:09 web-1 node[5120]: Server jalan di http://localhost:3000
Loaded:menunjukkan file unit-nya danenabledataudisabled.Active:menunjukkan keadaan sekarang:active (running),inactive (dead),failed, atauactivating (auto-restart)saat menunggu restart.- Baris di bawahnya diambil dari journal, tempat systemd menyimpan semua yang dicetak service. Kalau app gagal start,
journalctl -u catatan-apimenunjukkan alasannya. Ini yang terlihat kalauEnvironmentFile=lupa ditulis:
deploy@web-1:~$ journalctl -u catatan-api -n 4
Oct 09 09:00:03 web-1 systemd[1]: Started catatan-api.service - catatan-api.
Oct 09 09:00:03 web-1 node[5120]: Config error: DATABASE_URL, JWT_SECRET belum diset. Salin .env.example jadi .env, lalu isi.
Oct 09 09:00:03 web-1 systemd[1]: catatan-api.service: Main process exited, code=exited, status=1/FAILURE
Oct 09 09:00:03 web-1 systemd[1]: catatan-api.service: Failed with result 'exit-code'.
journalctl -u catatan-api -f mengikuti log secara live, dan -n 50 mengambil 50 baris terakhir.
Di latihan: membuat unit file dengan JavaScript
generateUnitFile(config) menyusun teks unit file dari object. Tiap section adalah baris-baris yang digabung dengan "\n", dan antar section dipisah satu baris kosong ("\n\n"). [Service] di latihan menulis Type=simple secara eksplisit: itu tipe bawaan untuk service dengan ExecStart=, artinya proses di ExecStart adalah service-nya. Kalau array after kosong, baris After= gak ditulis sama sekali.
⚠️ Jebakan yang sering ketemu
- Cuma
enable, atau cumastart.startsaja hilang setelah reboot,enablesaja belum menyalakan apa-apa. Untuk service baru:enable --now. - Lupa
User=. Service jalan sebagai root, jadi celah keamanan di app langsung jadi akses root ke seluruh server. - Path
EnvironmentFile=salah. systemd menolak start denganFailed to load environment files: No such file or directory. Kalau file-nya memang opsional, tulisEnvironmentFile=-/etc/catatan-api.env(tanda minus di depan path). - Mengandalkan
Restart=untuk config yang salah. App crash lagi setiapRestartSec. Bacajournalctl -u, betulkan penyebabnya, lalurestart. - Lupa
daemon-reloadsetelah mengedit unit. systemd masih memakai versi lama (lihat pelajaran systemd & Services).
TL;DR: Unit production catatan-api: [Unit] dengan After= (urutan start saja) dan Requires= (ikut berhenti kalau dependensinya berhenti), [Service] dengan User=www-data, ExecStart= berpath lengkap, EnvironmentFile=/etc/catatan-api.env (dibaca systemd sebagai root), Restart=on-failure (crash dibangkitkan, exit 0 dan stop dibiarkan; always juga membangkitkan exit 0) dan RestartSec=5, lalu [Install] WantedBy=multi-user.target agar ikut nyala saat boot. start = sekarang, enable = setiap boot, enable --now = dua-duanya. systemctl is-active keluar dengan 0 kalau jalan (pakai --quiet di script), status menampilkan keadaan dan log terakhir, journalctl -u catatan-api menunjukkan kenapa app gagal.