Skip to content

${...} substitution

Any string value in runwisp.toml can take its value from the daemon’s environment or from a file:

[tasks.backup]
cron = "${BACKUP_CRON}" # from the daemon's environment
description = "backs up ${REGION}" # works inside a longer string
run = "/usr/local/bin/backup.sh"
[notifiers.slack-ops]
type = "slack"
webhook_url = "${file:secrets/slack.url}" # from a file

It works on every string value (cron, paths, env values, notifier credentials, compose blocks) except run. Use it to keep credentials out of the TOML file.

${VAR} is replaced with VAR from the daemon’s environment (the shell, systemd unit, or container that started runwisp daemon).

  • An unset variable is a config-load error that names the variable and the key:

    tasks.backup.cron: environment variable BACKUP_CRON is not set
  • A variable that is set but empty gives an empty string.

${file:path} is replaced with the file’s contents, with leading and trailing whitespace removed (so a trailing newline doesn’t break a token).

  • A missing or unreadable file is a config-load error.
  • Paths can be absolute, ~/... (your home directory), or relative to the directory of runwisp.toml.
  • Works well with files written by a secrets tool (Vault agent, sops, Docker secrets in /run/secrets/...). Set the file to chmod 600.

Substitution runs once, when the config is loaded: at startup and on runwisp reload. The daemon doesn’t watch the environment or the files. After a change, reload or restart.

run (on tasks and services) is never substituted. The shell expands ${VAR} in it at run time, using the full process environment, including env, env_file, secrets, and secrets_file:

[tasks.backup]
run = "backup.sh --bucket ${BACKUP_BUCKET}" # the shell expands this
[tasks.backup.env]
BACKUP_BUCKET = "s3://prod-backups"
  • Keys. Env var names, task names, and compose service names stay as written. Only values are substituted.
  • Referenced files. Dotenv files (env_file, secrets_file) and compose files (compose_file) are read as they are.
  • Crontabs read through include_cron.

Write $${ for a literal ${:

description = "template is $${VAR}" # → template is ${VAR}
  • A single $ (like cost: $5) needs no escaping.
  • An unclosed ${ (no }) is a config-load error.
  • runwisp import escapes ${ for you, because cron and supervisord don’t substitute. Imported text keeps its meaning. To make an imported value substitute, remove the extra $.
[tasks.export]
cron = "${EXPORT_CRON}"
run = "/usr/local/bin/export"
[tasks.export.secrets]
API_TOKEN = "${file:secrets/export.token}"
[notifiers.slack-ops]
type = "slack"
webhook_url = "${SLACK_OPS_WEBHOOK}"
[notifiers.tg-oncall]
type = "telegram"
bot_token = "${file:~/.config/runwisp/tg.token}"
chat_id = "-1001234567890"

Start the daemon with EXPORT_CRON and SLACK_OPS_WEBHOOK set and the token files at 0600. The TOML file then contains no secrets.