Restart Terjadwal Server Linux dan MikroTik: Cron, Scheduler, dan Jebakannya

Restart terjadwal server Linux dan MikroTik dengan cron, scheduler, dan jebakannya

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.

Diagram jadwal restart terjadwal tiga server Linux dan dua router MikroTik antara 04.00 dan 04.15 WIT
Jadwal restart terjadwal kami: bertahap dari server paling hulu, dengan jam WIT dan padanannya dalam UTC.

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.

SituasiRestart terjadwal?Alasan
VPS kecil berisi MySQL, PHP-FPM, NginxYa, restart layananMembersihkan worker PHP dan koneksi menggantung, downtime hanya beberapa detik
Router MikroTik rumahan di belakang CGNATYa, reboot tiap beberapa hariTunnel dan tabel koneksi kembali bersih, router tetap jalan walau server pusat mati
Server tanpa akses konsol daruratHindari reboot OSJika gagal boot, tidak ada yang bisa menolong sampai pagi
Database dengan transaksi 24 jamTidakLebih baik cari sumber masalahnya dan pakai replikasi
Aplikasi yang bocor memoriSementara sajaRestart 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.

Log restart terjadwal tiga server Linux billing, FreeRADIUS, dan GenieACS dengan status semua layanan sehat
Tangkapan asli log restart terjadwal di ketiga server, domain disensor. Sejak 1 September tercatat 62 kali jalan tanpa satu pun “MASALAH”.

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
Journal cron memperlihatkan perintah terpotong pada tanda persen sehingga restart terjadwal gagal diam-diam
Tangkapan asli dari server uji: perintah tanpa escape terpotong di “date +” dan output-nya hilang tanpa jejak.

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"
Perbandingan PATH cron di Ubuntu dan Rocky Linux: perintah ss tidak ditemukan saat restart terjadwal
Tangkapan asli dari tiga server produksi: di Rocky Linux, ss ada di /usr/sbin sehingga hilang dari PATH cron.

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.

Scheduler MikroTik untuk restart terjadwal tiap 4 hari dengan timezone Asia/Jayapura dan policy reboot
Tangkapan asli kondisi scheduler kedua router hari ini, IP, port, dan nama router disensor.

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.

Diagram alur restart terjadwal: cron, PATH eksplisit, restart berurutan, tunggu active, tunggu port, verifikasi, dan exit code
Alur satu kali restart terjadwal di server Linux, dari cron sampai exit code yang siap masuk ke sistem monitoring.

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 dengan env -i PATH=/usr/bin:/bin.
  • Menganggap status active berarti siap. Tunggu port dan uji fungsi aplikasi, misalnya dengan curl atau mysqladmin 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-run setelah 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

Referensi: crontab(5) — Ubuntu Manpages, systemd.time(7), MikroTik RouterOS: Scheduler, MikroTik RouterOS: Clock.

Ali Akbar

Software Developer yang fokus mengembangkan aplikasi berbasis Web dan Desktop. Senang mempelajari teknologi baru terutama di bidang web design dan web development.

View all posts by Ali Akbar →

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *