Skip to content
Pastalikek65Public

About

Tiny low-memory cron daemon for Linux that pushes Telegram/Discord/ntfy/generic webhook alerts when a scheduled job fails, with a durable retry queue and clean shutdown.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

cronping

CI

A tiny cron daemon for low-memory Linux boxes (VPS, NAS, single-board computers, PRoot jails) that runs jobs on a schedule and pushes an alert to Telegram, Discord, ntfy.sh, or any generic webhook when a job fails.

Go 1.24, standard library only (plus robfig/cron/v3 and gopkg.in/yaml.v3). Static binary, ~8 MB, designed to run at a handful of MB of RAM.

Features

  • Cron-style scheduling via robfig/cron/v3 (see its standard 5-field syntax plus descriptors like @every 10s, @daily, @hourly).
  • Per-job timeout with process-group cleanup: on expiry the whole process group gets SIGTERM, then SIGKILL shortly after.
  • Overlap protection: a job never runs twice concurrently; a skipped fire is logged.
  • stdout/stderr captured with a 4096-byte tail that ships with every alert.
  • Alerts on non-zero exit or failed spawn (exit code 1, started=false).
  • Channel support: Telegram, Discord webhooks, ntfy, and generic HTTP (custom method, headers, optional Go text/template body).
  • Durable alert queue: deliveries that fail are persisted to queue.jsonl and retried with exponential backoff until they succeed or age out (max_age). Survives restarts.
  • History log: every run (success or failure) is appended to history.jsonl, rotated on boot to stay within 1 MB.
  • Three commands: serve (daemon), run (one-shot), status (history table).
  • Graceful shutdown: on SIGTERM/SIGINT in-flight jobs are cancelled and the alert queue is drained (up to 5s) before exit 0.

Quickstart

make build
./bin/cronping serve -config testdata/example.yaml

Write your own config (cronping.yaml) and test it:

./bin/cronping run -config cronping.yaml -job mybackup
echo $?   # job's exit code
./bin/cronping status -config cronping.yaml -n 5

Configuration

YAML or JSON. All fields optional unless marked required.

state_dir: /var/lib/cronping   # where history.jsonl + queue.jsonl live (default: data)

jobs:                          # required: at least one job
  - name: nightly-backup       #   required, must be unique
    schedule: "0 3 * * *"      #   required, robfig/cron syntax
    command: /usr/local/bin/backup.sh   # required
    args: ["--quiet"]
    timeout: 300s              # default 10m; "0" disables nothing, resets to default

alert:                         # required: at least one channel below
  telegram:                    #   optional
    bot_token: "123:ABC"
    chat_id: "12345"
  discord:                     #   optional
    webhook_url: "https://discord.com/api/webhooks/..."
  ntfy:                        #   optional
    url: "https://ntfy.sh"     #   default https://ntfy.sh
    topic: "server-alerts"
  generic:                     #   optional list
    - url: "http://host:9090/hook"   # required
      method: POST             #   default POST
      headers: { "X-Api-Key": "secret" }
      body_template: '{"job":"{{.Job}}","exit":{{.ExitCode}}}'  # Go text/template

queue:
  retry_initial: 5s            # default 5s
  retry_max: 5m                # default 5m
  backoff_factor: 2.0          # default 2.0
  max_age: 24h                 # default 24h; drop undelivered after this

body_template is evaluated against these fields: Job, Host, Command, ExitCode, Started, Duration, Timestamp, Stderr, Stdout. If the template is absent or fails to render, the default JSON body below is sent.

Default generic payload

{
  "job": "nightly-backup",
  "host": "myhost",
  "command": "/usr/local/bin/backup.sh --quiet",
  "exit_code": 7,
  "started": true,
  "duration_sec": 12.34,
  "timestamp": "2026-08-12T14:00:00Z",
  "stderr": "last 4096 bytes of stderr"
}

Telegram, Discord, and ntfy each get their own native body format; stderr tail and exit code are always included.

CLI

cronping <serve|status|run> [flags]
Command Flags Behavior
serve -config PATH (default cronping.yaml) Run the daemon; register jobs, run on schedule, queue alerts, drain on SIGTERM/SIGINT.
run -config PATH, -job NAME Execute one job once and exit with its exit code. Alerts are sent synchronously.
status -config PATH, -n N (default 20) Print the N most recent history records as a table (JOB EXIT STARTED DURATION TIME ALERT).

Build

make build              # bin/cronping (host)
make build-arm64        # bin/cronping-linux-arm64
make build-amd64        # bin/cronping-linux-amd64
make cross              # both static binaries
make test               # unit + integration tests
make vet
make lint               # golangci-lint if present

Systemd

A working unit file is included at deploy/cronping.service. Install it:

sudo cp deploy/cronping.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cronping

Logs:

journalctl -u cronping -f

Run it manually once to validate your config first:

/usr/local/bin/cronping run -config /etc/cronping.yaml -job nightly-backup

PRoot / restricted environments

serve is a plain process with no extra daemonization — run it directly inside a proot jail (no systemd required):

proot -R <rootfs> ./cronping serve -config /etc/cronping.yaml

Keep state_dir inside the jail (e.g. /var/lib/cronping) so history and the retry queue survive restarts. The jail needs outbound network to reach your webhook hosts; no listening ports are required.

For environments where a long-running process is awkward, use the one-shot mode from cron or a wrapper script. The exit code is the job's:

./cronping run -config /etc/cronping.yaml -job nightly-backup

Set GOMAXPROCS=1 if you want to cap scheduler overhead to a single thread:

GOMAXPROCS=1 ./cronping serve -config /etc/cronping.yaml

Operations

  • RAM: the daemon itself is a few MB. serve logs resident RSS at boot (msg="cronping serving" ... rss_kb=5760) so you can see the baseline.
  • History: history.jsonl is truncated on start to the newest ~1 MB. Dump it with status or read the file directly.
  • Queue: undelivered alerts sit in queue.jsonl with the retry schedule above. It is append-only and safe to tail -f.
  • Logs: structured text (slog) on stderr; grep for level=ERROR or a job name to follow one job.

About

Tiny low-memory cron daemon for Linux that pushes Telegram/Discord/ntfy/generic webhook alerts when a scheduled job fails, with a durable retry queue and clean shutdown.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages