[daemon]
[daemon] holds the handful of settings that apply to the daemon as a
whole, rather than to any one task or service. The whole section is
optional — leave it out and the built-in defaults below take over.
You won’t find the data directory or listen address in here,
and that’s on purpose: they have to be set before the config file is
even read, so they live on the CLI instead (or in however your
supervisor invokes runwisp). They’re documented on this page anyway,
since in practice you tend to think about them together.
[daemon]shutdown_timeout = "10s"external_url = "https://runwisp.example.com"tls = "auto"tls_cert = ""tls_key = ""metrics_enabled = falsemetrics_listen = ""include = ["conf.d/*.toml"]| Key | Default | What it does |
|---|---|---|
shutdown_timeout | "10s" | Whole-daemon shutdown budget. After SIGTERM, the daemon SIGKILLs any in-flight runs that haven’t exited within this window so the process can actually exit. |
external_url | unset | Public base URL of this daemon’s Web UI. When set, notification messages (Slack, Telegram) include a deep-link back to the run; when unset, the link line is omitted. |
tls | "auto" | "auto" serves HTTPS automatically whenever the bind address is non-loopback (self-signing a cert on first boot); "off" always serves plain HTTP. Loopback binds stay HTTP either way. Ignored when tls_cert/tls_key are set. |
tls_cert | unset | Path to a PEM certificate to serve instead of the self-signed one. Set together with tls_key. When set, HTTPS is served on every bind address, loopback included. |
tls_key | unset | Path to the PEM private key matching tls_cert. Both keys are set together or not at all. |
metrics_enabled | false | Master switch for the Prometheus-compatible /metrics endpoint. Off by default — task names and the daemon version label are visible to anyone who can reach the endpoint, so it stays closed until you turn it on. |
metrics_listen | unset | Optional host:port for a dedicated metrics listener (e.g. "127.0.0.1:9478"). When set, /metrics is only reachable on this address — never on the main UI/REST listener. Only consulted when metrics_enabled = true. |
include | unset | Glob patterns for extra TOML files to merge into this config at load. Lets you split tasks across conf.d/*.toml instead of one giant file. Only valid in the root config. |
shutdown_timeout
Section titled “shutdown_timeout”Think of shutdown_timeout as the budget for the whole daemon.
Every task and service still has its own
graceful_stop, but it has to
fit inside that overall cap. If some task’s graceful_stop is longer
than shutdown_timeout, the daemon warns you about it by name at boot —
because in that situation it’ll SIGKILL the straggler before its
per-task grace window is even up.
You’ve got three ways out of that: raise shutdown_timeout, lower the
per-task graceful_stop, or just accept that the task’s cleanup hook
might get cut short when the daemon goes down.
external_url
Section titled “external_url”external_url is the public address where someone — you, or whoever
gets a notification — actually reaches this daemon’s Web UI. That might
be https://runwisp.example.com behind a reverse proxy, or
http://192.168.1.50:9477 on a LAN. Trailing slashes get stripped, and
the scheme has to be http or https.
The daemon never calls out to this URL itself — it’s purely for
rendering. When a Slack or Telegram notification fires, the template
tacks on a “View run” link like
<external_url>/tasks/<task>/<id>, which drops you straight into
that run’s detail panel in the dashboard.
Leaving external_url unset is perfectly fine and fully supported.
Notifications still render in full — they just skip the link line rather
than print a broken URL.
tls, tls_cert, tls_key
Section titled “tls, tls_cert, tls_key”RunWisp serves HTTPS by itself, with no certificate to obtain and nothing to wire up. The rule is simple: the moment you bind somewhere other than loopback, the channel is encrypted.
- Loopback (
--host 127.0.0.1, the default): plain HTTP. There’s nothing on the wire to eavesdrop, andcurl http://localhost:9477just works for local dev. - Non-loopback (
--host 0.0.0.0, a LAN IP, …) withtls = "auto": the daemon generates a long-lived self-signed certificate on first boot, stores it under<data>/tls/, and serves HTTPS. Auth cookies and CHAP responses never cross a real network in cleartext.
Because the cert is self-signed, the first browser visit shows the usual “not trusted” warning, and the CLI/TUI pin the cert on first connect (trust-on-first-use, like SSH). To let you verify you’re talking to the right daemon, the startup log prints the certificate’s SHA-256 fingerprint:
Serving HTTPS bind=0.0.0.0 cert=self-signed fingerprint=sha256:1a2b3c…Compare that against the warning your browser shows, or the fingerprint
the CLI pins, and you know the connection is genuine. If you ever
regenerate the cert (delete <data>/tls/ and restart), the CLI will
refuse to connect until you clear the old pin — same loud “host identity
changed” behaviour as ssh.
Bring your own certificate by pointing tls_cert and tls_key at a
PEM pair — a cert from your internal CA, say, or one a tool like mkcert
made. Supplying a pair forces HTTPS on every bind address (loopback
included) and skips the self-signed flow entirely.
Turn it off with tls = "off" when something else already terminates
TLS — most commonly a reverse proxy (nginx, Caddy, Traefik) doing TLS out
front and forwarding plain HTTP to RunWisp on a private network. In that
case set RUNWISP_TRUST_PROXY to the proxy’s CIDR so the daemon honours
X-Forwarded-Proto and still marks session cookies Secure. A
non-loopback bind on tls = "off" prints a loud startup banner — plain
HTTP on a real network exposes your auth tokens, and that should never be
silent.
What RunWisp deliberately does not do is ACME / Let’s Encrypt: that needs outbound network and a public domain, which would break the local-first, offline-complete promise. For a publicly-trusted cert, use a reverse proxy or supply your own pair.
metrics_enabled
Section titled “metrics_enabled”metrics_enabled is the gate on the OpenMetrics scrape endpoint at
/metrics, and it’s off by default for a reason: runwisp_task_active_runs
exposes your task names as label values, and runwisp_build_info
exposes the daemon version — that’s exactly the kind of recon detail a
publicly-reachable daemon shouldn’t be handing out unasked. Flip it to
true when you’re ready to wire RunWisp into Prometheus, Grafana Agent,
or an OpenTelemetry collector. The full label list and a sample scrape
config are over in Operations / Metrics.
metrics_listen
Section titled “metrics_listen”With metrics_enabled = true, the endpoint rides on the main UI/REST
listener by default. Point metrics_listen at something like
"127.0.0.1:9478" (any host:port works) and /metrics binds there
instead. That’s the knob you want when --host 0.0.0.0 puts the
dashboard out in public but you’d rather keep the scrape surface on
loopback. The dedicated listener serves only /metrics — the UI, REST
API, and /health all stay on the main listener.
The dedicated metrics listener is always plain HTTP — auto-HTTPS (see
tls) does not wrap it. Keep it on loopback (or a
private interface like a Tailscale address) and let your scraper reach it
locally or through a proxy; don’t expose it on a public interface.
And if you set metrics_listen but never turned metrics_enabled on,
the daemon rejects it at boot instead of quietly ignoring it.
include
Section titled “include”Once you’ve got more than a handful of tasks, one runwisp.toml gets
unwieldy. include lets you break it up: point it at one or more glob
patterns, and every matching file gets merged in as if you’d pasted it
into the root config.
[daemon]include = ["conf.d/*.toml", "services/*.toml"]
[tasks.heartbeat]run = "curl -fsS https://example.com/ping"# conf.d/backups.toml — no [daemon] here, just tasks[tasks.nightly-backup]run = "/opt/backup.sh"cron = "0 3 * * *"A few rules keep “what wins” obvious:
- Patterns are relative to the file that wrote them. A glob in the
root config resolves against the root config’s directory; the same
goes for any relative path (
run’sworking_dir,env_file,compose_file,${file:...}) inside an included file — those resolve against that file’s directory, not the root’s. So aconf.d/backups.tomlreferencingenv_file = "backup.env"looks forconf.d/backup.env. - Tasks, services, compose blocks, notifiers, and routes accumulate. Everything from the root and every matched file is pooled together. Merge order is the root first, then matched files sorted by path — but order only matters for tie-breaking error messages, since…
- Names must be unique across all files. Two files defining a task,
service, or
[compose.*]alias with the same name is a hard error that names both files. No silent “last one wins”. - The big singleton tables stay in the root.
[daemon],[scheduler],[defaults],[storage], and[notify]may only appear in the root config — setting one in an included file is an error. This keeps daemon-wide settings and[defaults]inheritance in exactly one place. - Includes don’t nest. An included file can’t have its own
[daemon].include. One level, root-out.
Editing — or adding, or deleting — any included file shows up the same
way a root-config edit does: the daemon flags the config as stale in
/api/info (and the UI), and a runwisp reload
(or a restart) re-globs the patterns and picks up the change. Includes
are resolved fresh on every load, so dropping a new conf.d/*.toml in
place gets it merged on the next reload — no restart required.
CLI flags: config, data directory & listen address
Section titled “CLI flags: config, data directory & listen address”These flags decide which config file gets read, where state lives, and
where the HTTP/Web UI listens. They apply to every runwisp subcommand
(daemon, tui, exec, and so on):
| Flag | Default | What it does |
|---|---|---|
--config, -c | runwisp.toml | Path to the TOML config file, resolved against the working directory. |
--data | .runwisp | Directory for all persistent state — SQLite database, per-task logs, PID file, and the local Unix socket. |
--socket | <data>/runwisp.sock | Path to the control socket. The daemon binds it; every CLI subcommand connects to it. Also RUNWISP_SOCKET. See Control socket. |
--host | 127.0.0.1 | Bind address for the HTTP server. Use 0.0.0.0 to listen on every interface. |
--port | 9477 | TCP port for the HTTP server (REST API, SSE log stream, Web UI). |
--log-level | info | Log verbosity — debug, info, warn, error. Also reads RUNWISP_LOG_LEVEL. |
--log-format | auto | Log shape — auto, text, json. Also reads RUNWISP_LOG_FORMAT. |
runwisp daemon --data /var/lib/runwisp --host 0.0.0.0 --port 9477--log-level and --log-format shape the daemon’s own log output —
Operations: Logging covers what each value does.
For the complete list of runwisp subcommands and flags in one place, see
the CLI reference.
It’s worth picking --data once and sticking with it. The database
file (runwisp.db), the local Unix socket (runwisp.sock), and every
task’s logs all live under that directory, so relocating later is a
plain directory move — not a config change.
The daemon never persists the password or the JWT signing key. The
password comes from RUNWISP_PASSWORD if you set it; otherwise a fresh
ephemeral one is minted every boot. The JWT key is derived
deterministically from the password, so setting RUNWISP_PASSWORD (via
a Docker secret or systemd LoadCredential=, say) is what keeps browser
sessions alive across restarts. Auth has the full
story.
Environment variables
Section titled “Environment variables”| Variable | What it does |
|---|---|
RUNWISP_PASSWORD | Sets the daemon password in memory. When unset, a fresh ephemeral password is minted every boot (and every session rotates with it). |
RUNWISP_NO_AUTH | 1 or true disables authentication entirely — local dev / trusted networks only. Mutually exclusive with RUNWISP_PASSWORD. See Auth. |
RUNWISP_TRUST_PROXY | Comma-separated CIDR list of reverse proxies whose X-Forwarded-* headers the daemon may honor. |
RUNWISP_CLOUD_TOKEN | Used by runwisp cloud to connect to a control plane. Ignored in standalone mode. |
RUNWISP_SOCKET | Control socket path. Same effect as --socket; the flag wins when both are set. |
RUNWISP_DEBUG_ADDR | Opt-in. A loopback address (e.g. 127.0.0.1:6060) on which to serve Go pprof memory/CPU profiles. Off by default; a non-loopback address is refused so profiles never reach the network. |
Control socket
Section titled “Control socket”On startup the daemon creates a Unix socket — <datadir>/runwisp.sock
by default. Local CLI commands and the TUI talk to the daemon over this
socket without ever needing a password — access is gated by the data
dir’s 0700 mode, the socket’s own 0600 mode, and a SO_PEERCRED
check when a connection is accepted. On a graceful shutdown, the socket
file is cleaned up.
You can move the socket off the data dir with --socket (or
RUNWISP_SOCKET). Two cases where that’s the difference between working
and not:
- Bind-mounted data dir. Some filesystems — Docker Desktop’s
osxfs/virtiofs bind mounts, a few network filesystems — let the daemon
bind a socket but reject the
chmodthat locks it to0600. RunWisp tolerates that (it warns and keeps serving, since the0700data dir and theSO_PEERCREDcheck still gate access), but if you’d rather avoid it entirely, point--socketat a Linux-native path like/run/runwisp.sockand keep the database and logs on the bind mount. - Reaching a non-default daemon from the CLI. Because every
subcommand connects to
--socket, you can talk to a daemon by its socket alone —runwisp status --socket /run/runwisp.sock— without restating the--datadirectory it was started with.
The daemon and the CLI must agree on the path: whatever you pass to
runwisp daemon --socket …, pass the same to runwisp status,
runwisp exec, and friends (or set RUNWISP_SOCKET once in the
environment they share).