Skip to content

[daemon]

[daemon] holds settings for the whole daemon. The table is optional. A change to any [daemon] key except include and include_cron needs runwisp restart; runwisp reload rejects it.

The config path, data directory, and listen address are command-line flags, because they are needed before the config file is read.

[daemon]
shutdown_timeout = "10s"
timezone = "Europe/Bratislava"
external_url = "https://runwisp.example.com"
check_updates = true
tls = "off"
tls_cert = ""
tls_key = ""
metrics_enabled = false
metrics_listen = ""
trusted_proxies = ["127.0.0.1/32"]
include = ["conf.d/*.toml"]
# include_cron = ["/etc/crontab", "/etc/cron.d/*"]

type: duration default: "10s"

How long the daemon waits for running tasks and services to exit on shutdown. After that, it SIGKILLs what is left.

  • Each unit’s graceful_stop must fit inside this. If one is longer, the daemon warns about it by name at boot, and at shutdown the unit may be killed before its own grace window ends.
  • To fix the warning, raise shutdown_timeout or lower that unit’s graceful_stop.
[daemon]
shutdown_timeout = "30s"

type: string default: the host zone (UTC if it can’t be detected)

The IANA zone used to read cron for every task that doesn’t set its own timezone. An unknown zone name is rejected at load.

[daemon]
timezone = "Europe/Bratislava"

type: string default: unset

The public address of this daemon’s Web UI, for example https://runwisp.example.com behind a reverse proxy or http://192.168.1.50:9477 on a LAN. Notifications (Slack, Telegram, and others) add a “View run” link of the form <external_url>/tasks/<task>/<id>.

  • Must be http or https with a host. A trailing slash is removed.
  • The daemon never connects to this URL.
  • When unset, notifications leave out the link.
[daemon]
external_url = "https://runwisp.example.com"

type: boolean default: true

Checks for a newer release (usually every 6 hours) and shows an amber marker next to the version in the Web UI and TUI. It never downloads or installs anything. It also lets the Web UI ask for a quick rating, at most once a month. Nothing is sent unless you press Submit, and then only your rating, optional note, and RunWisp version. Set false to keep RunWisp fully offline.

[daemon]
check_updates = false

tls: enum, default "off" · tls_cert: string, default unset · tls_key: string, default unset

How the HTTP server does TLS. The bind address comes from --host.

Setting Loopback bind (127.0.0.1, default) Other bind (0.0.0.0, LAN IP)
tls = "off" HTTP HTTP, with a startup warning
tls = "auto" HTTP HTTPS, self-signed certificate
tls_cert + tls_key HTTPS, your certificate HTTPS, your certificate
[daemon]
tls = "auto"
  • Self-signed (auto): generated on first boot and stored in <data>/tls/. The startup log prints its fingerprint (Serving HTTPS bind=0.0.0.0 cert=self-signed fingerprint=sha256:…) so you can compare it with what your browser shows. The CLI and TUI pin the certificate on first connect, like SSH.
  • New certificate: if you delete <data>/tls/ and restart, the CLI refuses to connect until you clear the old pin.
  • Your own certificate: set tls_cert and tls_key to a PEM pair (from your CA or mkcert). Set both or neither. This overrides tls.
  • Reverse proxy: leave tls = "off" when a proxy (nginx, Caddy, Traefik) handles TLS, and set trusted_proxies so the daemon trusts X-Forwarded-Proto and marks cookies Secure.
  • No ACME / Let’s Encrypt. For a publicly trusted certificate, use a reverse proxy or your own pair.
  • The RUNWISP_TLS env var overrides tls.

type: boolean default: false

Turns on the OpenMetrics endpoint at /metrics (for Prometheus, Grafana Agent, OpenTelemetry). It is off by default because the metrics show task names and the daemon version. See Metrics for the metric list.

[daemon]
metrics_enabled = true

type: string default: unset

A separate host:port for /metrics, for example "127.0.0.1:9478". Use it to keep metrics on loopback while the Web UI is public.

  • When set, /metrics is served only on this address. The UI, REST API, and /health stay on the main listener.
  • Setting it turns metrics on. metrics_enabled = false together with metrics_listen is an error.
  • This listener is always plain HTTP. Keep it on loopback or a private interface.
[daemon]
metrics_listen = "127.0.0.1:9478"

type: string[] (CIDR) default: unset

Reverse proxies (nginx, Caddy, Traefik, Cloudflare Tunnel) whose X-Forwarded-For header RunWisp trusts for the real client IP.

  • A bare IP means a single address ("127.0.0.1" is /32).
  • 0.0.0.0/0 and ::/0 are rejected, because they would let any client fake its IP.
  • The RUNWISP_TRUSTED_PROXIES env var (comma-separated) overrides this key.
  • See Reverse proxies.
[daemon]
trusted_proxies = ["127.0.0.1/32", "10.0.0.0/8"]

type: boolean default: false

Lets a connected control-plane peer start ad-hoc runs: one-shot commands that are not in your config. Without it, the peer can only watch runs and trigger tasks defined in runwisp.toml.

  • Ad-hoc runs never change your task definitions. They are logged and stored like any other run.
  • They run with the daemon’s privileges. Enable this only if you need it.
[daemon]
allow_station_dispatch = true

type: string[] (glob) default: unset

Extra TOML files to merge into the config, so you can split tasks across files.

runwisp.toml
[daemon]
include = ["conf.d/*.toml", "services/*.toml"]
conf.d/backups.toml
[tasks.nightly-backup]
cron = "0 3 * * *"
run = "/opt/backup.sh"
  • Globs and relative paths inside a file resolve against that file’s directory. env_file = "backup.env" in conf.d/backups.toml means conf.d/backup.env.
  • Tasks, services, compose blocks, notifiers, and routes from all files are merged. The root file loads first, then matches in path order.
  • Names must be unique across all files. A duplicate is an error that names both files.
  • [daemon], [defaults], [storage], and [notify] are allowed only in the root file.
  • Includes don’t nest: an included file can’t have its own include.
  • runwisp reload re-reads the globs, so added, changed, or deleted files are picked up without a restart.

type: string[] (glob) default: unset

Real crontabs to load as tasks. RunWisp reads these files and never writes them. Every job becomes a task with captured output, run history, and a page in the UI.

[daemon]
include_cron = [
"/etc/crontab",
"/etc/cron.d/*",
"/var/spool/cron/crontabs/*", # per-user crontabs (needs a root daemon)
]
  • sudo runwisp takeover finds the crontabs on the host and writes this key for you. It doesn’t edit an include_cron you already have; it prints what to add.
  • After crontab -e, run runwisp reload.
  • runwisp promote <task> copies a job into runwisp.toml and comments out the crontab line. See Staging and promoting.

The path decides the format:

Path Format Jobs run as
/etc/crontab, any cron.d/* system crontab the user in the line’s 6th field
/var/spool/cron/crontabs/<user> per-user spool <user>, from the file name
/var/spool/cron/<user> per-user spool <user> (RHEL/SUSE layout)
anything else crontab -l dump the daemon’s user
  • Running a job as another user needs a root daemon. Without root, RunWisp refuses that spool file instead of running the jobs as itself. Your own crontab works without root.
  • A spool file must be owned by the user it is named after (cron checks the same).

A glob picks only the files cron would read:

  • /etc/crontab and cron.d/*: regular files with names made of letters, digits, -, and _. So backup.dpkg-old, job.disabled, and README are skipped.
  • Spool directories: any name that could be a user (john.doe is fine), except tmp.* (the temporary file crontab -e writes).
  • A path you name exactly is always read, whatever its name.
  • A symlink is not followed, even though cron may run it. It is reported as a job that is not running, and runwisp takeover stops until you replace it with the file it points to.
  • Skipped files are reported with the reason.
  • A spool directory the daemon can’t read (Debian ships /var/spool/cron/crontabs as 1730 root:crontab) is reported, because cron may be running jobs RunWisp can’t see. Run RunWisp as root or as the owner.
  • A job RunWisp can’t reproduce is skipped; the rest of the file still runs. Each skip is reported as file:line at boot and by runwisp reload, runwisp status, and runwisp validate. runwisp import cron explains each one.
  • An unreadable file is skipped; other files still load. (For example: a spool file for a deleted user, wrong file mode.) It is a config error only when every matched file fails.
  • NSS/LDAP/SSSD users are not visible to the default release build (CGO_ENABLED=0, reads only /etc/passwd), so their jobs are skipped. Check with getent passwd <name>. A CGO build, or adding the user to /etc/passwd, fixes it.
  • MAILTO= is reported, not used. RunWisp keeps the output and notifies on failure instead. A sendmail notifier replaces cron mail and removes the warning. A SHELL= that isn’t an absolute path is reported too.
  • Missed jobs are not run later. A job missed while the daemon was down is recorded as missed, but not started. promote the task to get catch_up.
  • Overlapping runs queue. Cron starts a second copy of a slow job; RunWisp waits for the first. promote the task and set max_concurrent to change this.
  • Task names come from the command. If two crontabs give the same name, the second becomes backup-<crontab name>, and the rename is reported.
  • Files anyone could edit are refused. A group- or world-writable crontab, or one with an unrelated owner, fails the whole load. Spool directories are the exception: they are group-writable and sticky by design.
  • No ${...} substitution in cron files. 0 3 * * * dump.sh ${DB} reaches the shell as written.
  • A file can’t match both include and include_cron. That is an error.
  • anacron is not read. /etc/anacrontab jobs keep running under anacron.

Otherwise a cron task is a normal task: [defaults] applies, notifications fire, runwisp run starts it, and reload treats it like any other task.