Bayangkan sebuah warung kopi yang buka hampir 24 jam. Pemiliknya tetap menutup pintu sebentar setiap subuh untuk mengepel lantai dan membuang ampas, karena pada jam itu pembeli paling sedikit. Server juga butuh momen seperti itu. Proses yang berjalan berminggu-minggu menumpuk memori, koneksi menggantung, dan cache membengkak. Restart terjadwal adalah cara kita “mengepel lantai” secara otomatis pada jam paling sepi, tanpa harus bangun subuh.
Di artikel ini kami membagikan pengaturan nyata yang sudah berjalan sejak 1 September 2026 di jaringan ISP kami. Tiga server Linux (billing, FreeRADIUS, dan GenieACS) me-restart layanannya setiap hari pukul 04.00–04.12 WIT. Selain itu, dua router MikroTik reboot sendiri setiap 4 hari. Kami juga menceritakan jebakan yang benar-benar kami temui: tanda % di crontab, PATH cron yang minim, layanan yang “active” padahal belum siap, dan timezone router yang menebak sendiri sampai nyasar ke New York.

Kapan Restart Terjadwal Masuk Akal, dan Kapan Tidak
Restart terjadwal bukan obat untuk semua masalah. Jika sebuah aplikasi bocor memori, restart hanya menunda gejalanya, sedangkan bug-nya tetap ada. Namun, untuk server kecil yang menjalankan banyak layanan pihak ketiga, restart rutin pada jam sepi adalah asuransi yang murah. Oleh karena itu, kami memakai patokan berikut sebelum memasang jadwal.
| Situasi | Restart terjadwal? | Alasan |
|---|---|---|
| VPS kecil berisi MySQL, PHP-FPM, Nginx | Ya, restart layanan | Membersihkan worker PHP dan koneksi menggantung, downtime hanya beberapa detik |
| Router MikroTik rumahan di belakang CGNAT | Ya, reboot tiap beberapa hari | Tunnel dan tabel koneksi kembali bersih, router tetap jalan walau server pusat mati |
| Server tanpa akses konsol darurat | Hindari reboot OS | Jika gagal boot, tidak ada yang bisa menolong sampai pagi |
| Database dengan transaksi 24 jam | Tidak | Lebih baik cari sumber masalahnya dan pakai replikasi |
| Aplikasi yang bocor memori | Sementara saja | Restart menunda gejala, perbaikan kode tetap wajib |
Restart Layanan, Bukan Reboot OS
Permintaan awalnya sederhana: “reboot ketiga server setiap jam 4 pagi”. Namun, kami memilih restart layanan, bukan reboot sistem operasi. Alasannya, reboot harian tanpa pengawasan mengandung risiko besar. Jika VPS gagal boot karena kernel baru, disk penuh, atau masalah di penyedia, seluruh layanan ISP mati sampai ada orang yang sadar. Sebaliknya, restart layanan memberi efek pembersihan memori yang hampir sama, sementara kernel dan jaringan tetap hidup.
Selain itu, kami mengatur urutan dari hulu ke hilir. GenieACS mulai pukul 04.00, FreeRADIUS pukul 04.05, lalu billing pukul 04.12. Billing sengaja memakai menit 12, bukan 10. Pasalnya, cron billing yang lain berjalan setiap 5 dan 10 menit, sehingga restart di menit kelipatan 5 akan memotong cron yang sedang jalan.
Skrip Restart Terjadwal untuk Server Linux
Inti dari pengaturan ini adalah satu skrip Bash di /usr/local/bin/restart-harian.sh dengan mode 700 milik root. Kami sengaja menaruhnya di luar webroot. Skrip ini tidak sekadar menjalankan systemctl restart, tetapi juga menunggu setiap layanan benar-benar siap lalu memeriksa fungsinya. Kami membaginya menjadi tiga bagian. Simpan ketiganya berurutan dalam satu file.
Bagian 1: PATH, Log, dan Fungsi Tunggu
#!/usr/bin/env bash
# Restart harian layanan Billing — dipanggil cron 04:12 WIT (= 19:12 UTC)
set -u
# PATH eksplisit — cron memberi PATH minimal (/usr/bin:/bin), padahal
# ss/systemctl/mysqladmin bisa berada di /sbin & /usr/sbin.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
LOG=/var/log/restart-harian.log
SERVICES=(mysql php8.5-fpm nginx)
GAGAL=0
say() { echo "[$(date -u '+%Y-%m-%d %H:%M:%S') UTC] $*" >> "$LOG"; }
# Tunggu layanan benar-benar aktif, maksimal 90 detik.
tunggu_aktif() {
local s=$1 n=0
while [ "$n" -lt 90 ]; do
systemctl is-active --quiet "$s" && return 0
sleep 3; n=$((n + 3))
done
return 1
}
Baris PATH= di awal skrip adalah pelajaran pertama kami, dan kami membahasnya lebih jauh di bagian jebakan. Kemudian fungsi say menulis log dengan stempel waktu UTC agar cocok dengan jam server. Fungsi tunggu_aktif mengecek status layanan setiap 3 detik dan menyerah setelah 90 detik.
Bagian 2: Tunggu Port dan Restart Berurutan
# Tunggu port benar-benar mendengarkan, maksimal 60 detik ('t' TCP, 'u' UDP).
tunggu_port() {
local p=$1 proto=${2:-t} n=0
while [ "$n" -lt 60 ]; do
ss -ln"$proto" 2>/dev/null | grep -q ":$p " && return 0
sleep 3; n=$((n + 3))
done
return 1
}
cek_port() {
local p=$1 proto=${2:-t} label=${3:-$1}
if tunggu_port "$p" "$proto"; then
say " PORT $label mendengarkan"
else
say " PORT $label TIDAK mendengarkan setelah 60 detik"
GAGAL=1
fi
}
say "===== mulai restart harian (Billing) ====="
for s in "${SERVICES[@]}"; do
if ! systemctl cat "$s" >/dev/null 2>&1; then
say " LEWAT $s — unit tidak ada di server ini"; continue
fi
say " restart $s ..."
if systemctl restart "$s" 2>>"$LOG"; then
if tunggu_aktif "$s"; then say " OK $s aktif"
else say " GAGAL $s tidak aktif setelah 90 detik"; GAGAL=1; fi
else
say " GAGAL perintah restart $s error"; GAGAL=1
fi
done
Fungsi tunggu_port memakai ss untuk memastikan port benar-benar terbuka, karena status systemd saja belum cukup. Selanjutnya, perulangan me-restart layanan sesuai urutan di array SERVICES, yaitu database dulu, lalu PHP-FPM, terakhir Nginx. Perintah systemctl cat melewati unit yang tidak ada, sehingga satu skrip bisa kita pakai di banyak server.
Bagian 3: Verifikasi Fungsional dan Exit Code
cek_port 3306 t "3306 (MySQL)"
cek_port 443 t "443 (HTTPS)"
if mysqladmin ping >/dev/null 2>&1; then
say " DB MySQL merespons ping"
else
say " DB MySQL TIDAK merespons"; GAGAL=1
fi
for u in https://app.contoh.com/ https://pwa.contoh.com/auth/login; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$u" 2>/dev/null || echo 000)
say " HTTP $u -> $code"
[ "$code" = "200" ] || GAGAL=1
done
if [ "$GAGAL" -eq 0 ]; then say "===== selesai — semua layanan sehat ====="
else say "===== SELESAI DENGAN MASALAH — periksa server! ====="; fi
# Jaga log tetap ramping: simpan 2000 baris terakhir
if [ -f "$LOG" ] && [ "$(wc -l < "$LOG")" -gt 2000 ]; then
tail -n 2000 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
fi
exit "$GAGAL"
Bagian ini menguji layanan dari sudut pandang pengguna. Misalnya, mysqladmin ping memastikan database menjawab, lalu curl memastikan halaman aplikasi mengembalikan kode 200. Jika ada satu saja yang gagal, skrip menulis baris “SELESAI DENGAN MASALAH” dan keluar dengan kode 1. Dengan demikian, sistem monitoring bisa menangkap kegagalan itu. Untuk FreeRADIUS kami cukup mengganti isinya menjadi SERVICES=(mariadb radiusd) dengan cek port 1812 dan 1813 UDP. Untuk GenieACS isinya mongod plus empat layanan GenieACS dan lima port.
Menjadwalkan dengan /etc/cron.d
Kami sengaja menulis jadwal di file terpisah /etc/cron.d/restart-harian, bukan di root crontab. Server billing sudah punya 17 cron di root crontab, jadi file terpisah membuat jadwal baru ini mudah kita matikan tanpa menyentuh cron lain. Cukup rm satu file.
# Restart harian layanan Billing # 04:12 WIT (UTC+9) = 19:12 UTC. Timezone OS server ini = UTC, jadi jamnya ditulis UTC. # Skrip: /usr/local/bin/restart-harian.sh | Log: /var/log/restart-harian.log SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 12 19 * * * root /usr/local/bin/restart-harian.sh
Format cron.d punya satu kolom tambahan ketimbang crontab biasa, yaitu nama user (root) sebelum perintah. Kemudian perhatikan komentarnya. Kami menulis jam WIT dan UTC berdampingan agar siapa pun yang membaca tidak salah hitung. Pemilik file harus root dengan mode 644, karena cron mengabaikan file cron.d yang juga bisa user lain ubah.

Jebakan 1: Tanda % di Crontab
Ini jebakan yang paling licik. Saat menguji cron pertama kali, kami menulis perintah yang memuat date "+%Y-%m-%d". Perintah itu tidak pernah menghasilkan apa pun, sehingga kami sempat mengira file cron.d tidak terbaca. Ternyata penyebabnya adalah tanda %. Di crontab, % adalah karakter khusus yang cron ubah menjadi baris baru. Semua teks setelah % pertama masuk ke stdin, bukan lagi bagian perintah.
Kami mengulang uji ini di server uji dengan dua baris yang hampir sama. Bedanya, baris kedua memakai \%.
SHELL=/bin/bash * * * * * root echo "tanpa escape: $(date +%H:%M)" >> /tmp/uji-cron.log * * * * * root echo "dengan escape: $(date +\%H:\%M)" >> /tmp/uji-cron.log

Hasilnya jelas. Journal mencatat perintah pertama hanya sampai echo "tanpa escape: $(date +. Akibatnya Bash gagal mengurai perintah, dan karena server tidak punya MTA, cron membuang pesan error-nya. Sementara itu, baris kedua menulis dengan escape: 17:26 dengan benar. Solusi terbaik adalah tidak menaruh % di file cron sama sekali. Pindahkan logika ke skrip, seperti say() di atas, karena di dalam skrip tanda % aman.
Jebakan 2: PATH Cron yang Minim
Skrip yang lancar saat kita jalankan manual bisa gagal saat cron yang menjalankannya. Penyebab paling umum adalah PATH. Cron tidak membaca .bashrc atau profil login, sehingga PATH-nya jauh lebih pendek. Uniknya, perilaku ini berbeda per distro, dan kami menemukannya langsung di server produksi.
command -v ss env -i PATH=/usr/bin:/bin bash -c "command -v ss || echo ss TIDAK DITEMUKAN"

Di Ubuntu 22.04, ss berada di /usr/bin, jadi PATH pendek pun masih menemukannya. Sebaliknya, di Rocky Linux 9 ss berada di /usr/sbin, dan daemon cronie memakai PATH bawaan /usr/bin:/bin. Tanpa baris PATH=, fungsi tunggu_port di server FreeRADIUS dan GenieACS akan selalu gagal, lalu melaporkan port mati padahal layanan sehat. Oleh karena itu, kami menulis PATH di dua tempat sekaligus, yaitu di file cron.d dan di dalam skrip.
Cara mengujinya pun sederhana. Jalankan skrip dengan lingkungan kosong yang meniru cron, lalu lihat exit code-nya.
sudo env -i PATH=/usr/bin:/bin /usr/local/bin/restart-harian.sh; echo "exit code: $?" sudo tail -8 /var/log/restart-harian.log
Perintah env -i menghapus semua variabel lingkungan, sehingga skrip berjalan dalam kondisi yang mirip cron. Kami juga menguji skrip yang sama di server uji lewat cron.d dengan PATH=/usr/bin:/bin, dan hasilnya tetap exit 0.
Jebakan 3: Status Active Belum Tentu Siap
systemctl restart kembali begitu proses layanan berjalan, padahal aplikasi di dalamnya mungkin masih memuat data. Untuk membuktikannya, kami membuat layanan uji yang baru membuka port 4 detik setelah start.
sudo systemctl restart uji-layanan systemctl is-active uji-layanan ss -lnt | grep -q ":18080 " && echo "port 18080 siap" || echo "port 18080 BELUM mendengarkan" # active # port 18080 BELUM mendengarkan
Systemd langsung melaporkan active, tetapi port belum terbuka. Jika skrip hanya mengecek sekali, hasilnya gagal palsu. Kasus nyatanya muncul di server GenieACS. Log produksi mencatat genieacs-ui aktif pukul 19:00:02, sedangkan kelima port baru siap pukul 19:00:05. Itulah sebabnya tunggu_port mencoba ulang setiap 3 detik sampai 60 detik.
Jebakan 4: Timezone Server dan Jam Cron
Ketiga server kami memakai timezone OS UTC, sedangkan kami tinggal di zona WIT (UTC+9). Cron selalu membaca jam menurut timezone OS. Akibatnya, jadwal “jam 4 pagi WIT” harus kita tulis sebagai 19 UTC pada hari sebelumnya. Daripada menghitung di kepala, biarkan date yang menghitung.
for j in 04:00 04:05 04:12; do printf "%s WIT = " $j; date -u -d "TZ=\"Asia/Jayapura\" 2026-11-04 $j" "+%H:%M UTC"; done # 04:00 WIT = 19:00 UTC # 04:05 WIT = 19:05 UTC # 04:12 WIT = 19:12 UTC systemd-analyze calendar "*-*-* 04:00:00 Asia/Jayapura" # Normalized form: *-*-* 04:00:00 Asia/Jayapura # Next elapse: Mon 2026-11-02 19:00:00 UTC
Perintah pertama mengubah jam WIT ke UTC. Kemudian perintah kedua menunjukkan alternatif yang lebih rapi. Timer systemd bisa menulis zona waktu langsung di OnCalendar=, sehingga jadwal tidak bergeser walaupun timezone OS berubah. Namun, kami tetap memakai cron karena tim sudah terbiasa dan server ini tidak pernah berganti timezone. Jangan pula mengubah timezone OS server yang sudah berjalan, karena semua cron lama akan ikut bergeser.
Reboot Terjadwal MikroTik dengan Scheduler
Untuk dua router MikroTik, kami memasang jadwal di dalam router lewat /system scheduler, bukan lewat cron di server billing. Dengan cara ini, reboot tetap jalan walaupun server pusat atau tunnel sedang putus. Router RouterOS 6 reboot pukul 04.05 dan router RouterOS 7 pukul 04.15. Jedanya 10 menit, jadi jika router pertama bermasalah saat naik, pelanggan di router kedua belum ikut terdampak. Kami memasangnya dari billing lewat RouterOS API dengan parameter berikut.
$param = [
'name' => 'tbradius-reboot-terjadwal',
'start-time'=> $jam, // mis. 04:15:00
'interval' => $interval, // mis. 4d
'on-event' => '/system reboot',
'policy' => 'read,write,policy,test,reboot',
];
if ($start_date !== '') $param['start-date'] = $start_date; // mis. 2026-09-05
$res = $api->comm('/system/scheduler/add', $param);
Ada dua hal penting di sini. Pertama, policy reboot wajib ada. Tanpa policy itu, scheduler tetap muncul di daftar tetapi gagal diam-diam saat waktunya tiba. Kedua, kami selalu mengisi start-date, dan alasannya ada di contoh kasus kedua. Jika Anda lebih suka terminal, padanan CLI-nya adalah /system scheduler add dengan parameter yang sama.

Contoh Kasus 1: Router yang Mengira Dirinya di New York
Saat kami memeriksa kedua router sebelum memasang jadwal, jam keduanya ternyata berbeda. Router pertama memakai Asia/Jakarta (GMT+7), sedangkan router kedua memakai America/New_York (GMT−4). Keduanya mengaktifkan time-zone-autodetect, fitur yang menebak lokasi dari IP publik. Karena uplink-nya Starlink, tebakannya meleset jauh.
=== Router #7 (sebelum diperbaiki, 1 Sep 2026) === Jam router : 2026-09-01 07:06:51 Timezone : America/New_York (GMT-04:00) autodetect=true === Router #6 === Jam router : sep/01/2026 18:06:32 Timezone : Asia/Jakarta (GMT+07:00) autodetect=true
Jam UTC router sebenarnya benar, hanya labelnya yang salah. Namun, scheduler membaca jam lokal. Akibatnya, “04.05” di router New York akan jatuh pukul 17.05 WIT, tepat saat pelanggan ramai. Lebih parah lagi, tebakan bisa berubah sendiri saat IP Starlink berpindah. Kami lalu mematikan autodetect dan menyeragamkan timezone kedua router.
/system clock set time-zone-autodetect=no time-zone-name=Asia/Jayapura
Setelah itu, kedua router menunjukkan Asia/Jayapura (GMT+09:00) autodetect=false, sehingga jam di scheduler sama dengan jam dinding kami.
Contoh Kasus 2: Scheduler yang Tidak Akan Pernah Jalan
Dua hari setelah pemasangan, kami memeriksa ulang dan menemukan keanehan di router RouterOS 7. Kolom next-run kosong, sedangkan router RouterOS 6 dengan parameter yang sama menampilkan tanggal 5 September dengan normal.
tbradius-reboot-terjadwal start=2026-09-01 04:15:00 interval=4d next=-
on-event: /system reboot
Penyebabnya, kami membiarkan start-date memakai nilai bawaan, lalu tanggal itu terlanjur lewat. RouterOS 7 tidak menghitung ulang jadwal berikutnya, sehingga scheduler itu tidak akan pernah jalan. Solusinya, kami memasang ulang dengan start-date=2026-09-05. Hati-hati juga dengan kebalikannya. Saat itu jam router menunjukkan 04.12, sedangkan start-time 04.15. Seandainya kami mengisi tanggal hari itu, router akan reboot tiga menit kemudian.
Menguji Scheduler Tanpa Me-reboot Router
Kita tentu tidak mau me-reboot router produksi hanya untuk membuktikan scheduler bekerja. Karena itu, kami membuat scheduler sementara yang hanya menulis log setiap 20 detik dengan policy yang sama, menunggu 40 detik, lalu menghapusnya.
BUKTI: [20:48:25] TBRADIUS-UJI-SCHEDULER-d535d7 BUKTI: [20:48:45] TBRADIUS-UJI-SCHEDULER-d535d7 ==> SCHEDULER ROUTER AKTIF & DIEKSEKUSI. Scheduler uji dibersihkan, sisa: 0 (harap 0)
Dua baris BUKTI dengan jarak 20 detik menandakan mesin scheduler router berjalan. Selain itu, ada temuan menarik dari pemeriksaan hari ini. Uptime kedua router lebih pendek dari 4 hari karena router sempat menyala ulang di luar jadwal, tetapi next-run tetap 4 November. Artinya, RouterOS menghitung jadwal dari start-date, bukan dari waktu boot terakhir. Waspadai juga device-mode di RouterOS 7. Pada mode tertentu, router langsung menolak perintah /system scheduler add dengan pesan not allowed by device-mode, dan membukanya butuh konfirmasi fisik di router.

Kesalahan Umum
- Memakai reboot OS padahal restart layanan sudah cukup. Reboot yang gagal di jam 4 pagi bisa membuat layanan mati sampai pagi.
- Menulis tanda % langsung di crontab. Perintah terpotong diam-diam. Pakai
\%atau pindahkan logika ke skrip. - Mengandalkan PATH bawaan cron. Tulis
PATH=di file cron dan di skrip, lalu uji denganenv -i PATH=/usr/bin:/bin. - Menganggap status active berarti siap. Tunggu port dan uji fungsi aplikasi, misalnya dengan
curlataumysqladmin ping. - Lupa bahwa cron membaca timezone OS. Tulis jam lokal dan jam UTC berdampingan di komentar.
- Me-restart semua server di menit yang sama. Beri jeda bertahap dan hindari menit tempat cron lain berjalan.
- Membiarkan timezone autodetect di MikroTik. Tebakan dari IP publik bisa salah dan berubah sendiri.
- Scheduler MikroTik tanpa policy reboot atau tanpa start-date. Keduanya membuat jadwal gagal tanpa pesan error. Selalu cek kolom
next-runsetelah memasang.
Kesimpulan
Restart terjadwal yang baik bukan sekadar satu baris cron. Pertama, pilih jenis restart yang paling aman, yaitu restart layanan untuk server Linux dan reboot penuh hanya untuk perangkat yang memang tahan, seperti router MikroTik. Kemudian buat skrip yang menunggu layanan benar-benar siap dan melaporkan hasilnya lewat log serta exit code. Terakhir, kenali jebakan lingkungannya: tanda %, PATH cron, dan timezone, baik di server maupun di router.
Hasilnya di jaringan kami cukup meyakinkan. Ketiga server sudah menjalankan restart terjadwal 62 kali sejak 1 September tanpa satu pun kegagalan, dan kedua router reboot tiap 4 hari dengan jadwal yang sama dengan jam dinding kami. Dengan demikian, kami bisa tidur nyenyak sementara server “mengepel lantainya” sendiri setiap subuh.
Baca Juga
- Cara Akses Proxmox dari Internet Tanpa IP Publik (WireGuard + VPS)
- Keamanan Server Linux: Tutorial Hardening SSH, Firewall UFW, CrowdSec, dan Alarm Telegram
- Cara Membuat Dashboard Monitoring Server (NOC) dengan Uptime Kuma dan Beszel
Referensi: crontab(5) — Ubuntu Manpages, systemd.time(7), MikroTik RouterOS: Scheduler, MikroTik RouterOS: Clock.

