[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.
[daemon]name = "storage-nas"shutdown_timeout = "10s"timezone = "Europe/Bratislava"external_url = "https://runwisp.example.com"check_updates = trueheartbeat_url = "https://hc-ping.com/your-check-uuid"log_output = falsetls = "off"tls_cert = ""tls_key = ""metrics_enabled = falsemetrics_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).
[daemon]name = "${HOSTNAME}"shutdown_timeout
Section titled “shutdown_timeout”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_stopmust 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_timeoutor lower that unit’sgraceful_stop. - Under
runwisp service install, systemd or launchd waits long enough for the whole shutdown. After you changeshutdown_timeout, runrunwisp service installagain so it waits the new amount.
[daemon]shutdown_timeout = "30s"timezone
Section titled “timezone”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"external_url
Section titled “external_url”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
httporhttpswith 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 --urlclients 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. Setexternal_urlto pin one address.runwisp statusshows which address is in use. - With
RUNWISP_AUTH=offnobody signs in, so nothing is detected: setexternal_urlor notifications leave out the link.
[daemon]external_url = "https://runwisp.example.com"check_updates
Section titled “check_updates”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.
[daemon]check_updates = falseheartbeat_url
Section titled “heartbeat_url”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.
[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.tomlwith substitution. - Unset, nothing is sent.
log_output
Section titled “log_output”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_OUTPUTenv var (trueorfalse) overrides this key. The Docker image sets it totrue. - In a burst, RunWisp may skip lines here and prints how many; the run log keeps them all.
- A TUI session ignores it.
[daemon]log_output = truetls, tls_cert, tls_key
Section titled “tls, tls_cert, tls_key”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_certandtls_keyto a PEM pair (from your CA ormkcert). Set both or neither. This overridestls. - Reverse proxy: leave
tls = "off"when a proxy (nginx, Caddy, Traefik) handles TLS, and settrusted_proxiesso the daemon trustsX-Forwarded-Protoand marks cookiesSecure. - No ACME / Let’s Encrypt. For a publicly trusted certificate, use a reverse proxy or your own pair.
- The
RUNWISP_TLSenv var overridestls.
metrics_enabled
Section titled “metrics_enabled”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 = truemetrics_listen
Section titled “metrics_listen”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,
/metricsis served only on this address. The UI, REST API, and/healthstay on the main listener. - Setting it turns metrics on.
metrics_enabled = falsetogether withmetrics_listenis an error. - This listener is always plain HTTP. Keep it on loopback or a private interface.
[daemon]metrics_listen = "127.0.0.1:9478"trusted_proxies
Section titled “trusted_proxies”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/0and::/0are rejected, because they would let any client fake its IP.- The
RUNWISP_TRUSTED_PROXIESenv var (comma-separated) overrides this key. - See Reverse proxies.
[daemon]trusted_proxies = ["127.0.0.1/32", "10.0.0.0/8"]allow_station_dispatch
Section titled “allow_station_dispatch”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 = trueinclude
Section titled “include”type: string[] (glob) default: unset
Extra TOML files to merge into the config, so you can split tasks across files.
[daemon]include = ["conf.d/*.toml", "services/*.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"inconf.d/backups.tomlmeansconf.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 reloadre-reads the globs, so added, changed, or deleted files are picked up without a restart.
include_cron
Section titled “include_cron”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/*", "/etc/anacrontab", # cron.daily/weekly/monthly on Debian, Ubuntu, Fedora "/var/spool/cron/crontabs/*", # per-user crontabs (needs a root daemon)]sudo runwisp takeoverfinds the crontabs on the host and writes this key for you. It doesn’t edit aninclude_cronyou already have; it prints what to add.- After
crontab -e, runrunwisp reload. runwisp promote <task>copies a job intorunwisp.tomland comments out the crontab line. See Staging and promoting.
Which files, and who the jobs run as
Section titled “Which files, and who the jobs run as”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).
Which files a glob matches
Section titled “Which files a glob matches”A glob picks only the files cron would read:
/etc/crontabandcron.d/*: regular files with names made of letters, digits,-, and_. Sobackup.dpkg-old,job.disabled, andREADMEare skipped.- Spool directories: any name that could be a user (
john.doeis fine), excepttmp.*(the temporary filecrontab -ewrites). - 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 takeoverstops 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/crontabsas1730 root:crontab) is reported, because cron may be running jobs RunWisp can’t see. Run RunWisp as root or as the owner.
Other rules
Section titled “Other rules”- A job RunWisp can’t reproduce is skipped; the rest of the file still
runs. Each skip is reported as
file:lineat boot and byrunwisp reload,runwisp status, andrunwisp validate.runwisp import cronexplains 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 withgetent 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. Asendmailnotifier replaces cron mail and removes the warning. ASHELL=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.
promotethe task to getcatch_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.
promotethe task and setmax_concurrentto change this. - Task names come from the command.
run-parts /etc/cron.dailybecomescron-daily. If two crontabs give the same name, the second becomesbackup-<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 …ortest -e /run/systemd/system || …are not imported when their own test shows they would skip.runwisp import cronandrunwisp takeoverlist them.runwisp reloadimports 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
includeandinclude_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.dailybecomescron-daily). It runs at the first hour ofSTART_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 getscatch_up = 1, so a machine that was off runs it once when it is back, like anacron.RANDOM_DELAYis 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.