Skip to content

[daemon]

[daemon] holds settings for the whole daemon. The table is optional. runwisp reload applies a change to any key except the TLS, metrics, and allow_station_dispatch ones, which need runwisp restart.

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

runwisp.toml
[daemon]
name = "storage-nas"
shutdown_timeout = "10s"
timezone = "Europe/Bratislava"
external_url = "https://runwisp.example.com"
check_updates = true
heartbeat_url = "https://hc-ping.com/your-check-uuid"
log_output = false
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: string default: unset since: 1.6

A label for this daemon, so you can tell several of them apart. The TUI sidebar, the Web UI, the browser tab title, and notification footers show it in place of the instance fingerprint.

  • Leading and trailing spaces are removed.
  • When unset, those places show the fingerprint.
  • ${HOSTNAME} gives the machine’s hostname, even when the variable isn’t exported (see substitution).
runwisp.toml
[daemon]
name = "${HOSTNAME}"

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. Cron schedules stop firing as soon as shutdown begins. Services stop first, then tasks, and each step gets the full shutdown_timeout.

  • 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.
  • Under runwisp service install, systemd or launchd waits long enough for the whole shutdown. After you change shutdown_timeout, run runwisp service install again so it waits the new amount.
runwisp.toml
[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.

runwisp.toml
[daemon]
timezone = "Europe/Bratislava"

type: string default: the address you last signed in to the Web UI from

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.
  • Since 1.6, when unset, the daemon uses the address of the most recent Web UI sign-in, and remembers it across restarts. A sign-in from another address replaces it. Only password sign-ins in a browser count: runwisp --url clients and Web UI tabs opened from the TUI don’t. Behind a reverse proxy, the forwarded host and scheme count only from a trusted proxy. Set external_url to pin one address. runwisp status shows which address is in use.
  • With RUNWISP_AUTH=off nobody signs in, so nothing is detected: set external_url or notifications leave out the link.
runwisp.toml
[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. The check is always skipped when the CI environment variable is set (and not false or 0), as it is in GitHub Actions, GitLab CI and most other CI services.

runwisp.toml
[daemon]
check_updates = false

type: string (http or https URL) default: unset since: 1.8

A URL the daemon requests (GET) when it starts and then every minute while it runs. Point it at a dead man’s switch service such as healthchecks.io: when the requests stop, that service alerts you. It covers the one failure RunWisp can’t report itself: the daemon being down.

runwisp.toml
[daemon]
heartbeat_url = "${file:secrets/heartbeat.url}"
  • On healthchecks.io, create a check with a period of 1 minute and give it a few minutes of grace time, then copy its ping URL.
  • A response other than 2xx counts as a failed ping. The first failure is logged with the host name, and so is the first success after it.
  • The URL often contains a secret token, so RunWisp never logs it or shows it in the API. Keep it out of runwisp.toml with substitution.
  • Unset, nothing is sent.

type: boolean default: false since: 1.7

Prints each line a run writes to the daemon’s stdout as task | line (stderr lines go to stderr), so docker logs and journalctl show job output. With --log-format=json, each line is one JSON object. Run logs are unchanged.

  • The RUNWISP_LOG_OUTPUT env var (true or false) overrides this key. The Docker image sets it to true.
  • In a burst, RunWisp may skip lines here and prints how many; the run log keeps them all.
  • A TUI session ignores it.
runwisp.toml
[daemon]
log_output = true

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
runwisp.toml
[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.

runwisp.toml
[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.
runwisp.toml
[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.
runwisp.toml
[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.
runwisp.toml
[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.

runwisp.toml
[daemon]
include_cron = [
"/etc/crontab",
"/etc/cron.d/*",
"/etc/anacrontab", # cron.daily/weekly/monthly on Debian, Ubuntu, Fedora
"/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)
/etc/anacrontab anacrontab root
any other file named anacrontab anacrontab the daemon’s user
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).
  • Directories and dotfiles (Debian’s .placeholder) are passed over without a warning.
  • 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. anacrontab jobs are the exception, see below.
  • 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. run-parts /etc/cron.daily becomes cron-daily. If two crontabs give the same name, the second becomes backup-<crontab name>, and the rename is reported.
  • Guard lines that do nothing on this machine are left out. Since 1.8, distro lines such as test -x /usr/sbin/anacron || run-parts … or test -e /run/systemd/system || … are not imported when their own test shows they would skip. runwisp import cron and runwisp takeover list them. runwisp reload imports them again once that changes.
  • 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.
  • anacrontab jobs catch up. Since 1.8, an anacrontab is read too. Each job becomes a task named after its job id (cron.daily becomes cron-daily). It runs at the first hour of START_HOURS_RANGE (07:00 without one) plus its delay in minutes: daily, on Mondays for a 7-day period, on the 1st for @monthly. It gets catch_up = 1, so a machine that was off runs it once when it is back, like anacron. RANDOM_DELAY is not used. The jobs are held while anacron runs.

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