Running in Docker
The official image is runwisp/runwisp, for amd64 and arm64. It contains
the same release binary as the install script and the npm package.
Quick start
Section titled “Quick start”services: runwisp: image: runwisp/runwisp:latest restart: unless-stopped ports: - "9477:9477" environment: - RUNWISP_PASSWORD=change-me volumes: - ./runwisp:/etc/runwisp - runwisp-data:/var/lib/runwisp
volumes: runwisp-data:If your app already has a compose.yaml, add the runwisp service and the
runwisp-data volume to it. Then start it:
mkdir runwispdocker compose up -dOpen http://localhost:9477 and log in with the password. On first start,
RunWisp writes a starter config with one example task to
./runwisp/runwisp.toml. Edit it and apply your changes:
docker compose exec runwisp runwisp reloadThe same container with docker run:
docker run -d --name runwisp \ -p 9477:9477 \ -e RUNWISP_PASSWORD=change-me \ -v ./runwisp:/etc/runwisp \ -v runwisp-data:/var/lib/runwisp \ runwisp/runwisp:latestWhere tasks run
Section titled “Where tasks run”A task’s run command runs with /bin/sh -c
inside the RunWisp container, so every tool it calls must be in the image. The
image has bash, tzdata, and ca-certificates, and nothing else: no curl,
no git, no database clients, no Docker CLI. A task that calls a missing tool
fails with pg_dump: not found in its log.
You have two ways to give a task its tools: run it in your app’s container, or add the tools to the RunWisp image.
Run tasks in your app’s containers
Section titled “Run tasks in your app’s containers”A job that belongs to your app (php artisan, manage.py, a database dump)
can run in the app’s own container, with the app’s code and credentials.
Change the runwisp service in compose.yaml:
services: runwisp: image: runwisp/runwisp:latest-docker # ... volumes: - /var/run/docker.sock:/var/run/docker.sock - ./runwisp:/etc/runwisp - runwisp-data:/var/lib/runwisp- The
-dockertag adds the Docker CLI with the Compose plugin. - The Docker socket lets RunWisp start commands in your containers.
Run docker compose up -d to recreate the container, then point a task at a
service:
[tasks.laravel-schedule]cron = "* * * * *"compose_service = "app"run = "php artisan schedule:run"RunWisp runs the command in the running app container of its own compose
project. Your stack keeps running from docker compose up, as before. To start
a fresh container for each run, or to target another project, see
If RunWisp runs in a container.
Add tools to the image
Section titled “Add tools to the image”For a job that belongs to no app (restic, curl a health URL), extend the
image:
FROM runwisp/runwisp:latestRUN apk add --no-cache restic curlOn a -debian tag, use apt-get install. The entrypoint, healthcheck, and
environment defaults are kept. To check what an image contains:
docker run --rm --entrypoint sh runwisp/runwisp:latest -c 'command -v pg_dump curl'Image variants and tags
Section titled “Image variants and tags”| Base | Tags | Notes |
|---|---|---|
| Alpine (default) | latest, X.Y.Z, X.Y, X, and the same four with -alpine |
Smaller; busybox and apk |
| Debian slim | latest-debian, X.Y.Z-debian, X.Y-debian, X-debian |
glibc and apt-get |
| Alpine + Docker | latest-docker, and -alpine-docker / X.Y.Z / X.Y / X variants |
Adds a Docker CLI + Compose plugin, ~100MB more |
| Debian + Docker | latest-debian-docker, X.Y.Z-debian-docker, X.Y-debian-docker, X-debian-docker |
Same, on the Debian base |
- Pick Debian if your tasks need glibc or a Debian-only package, otherwise Alpine.
- Pick a
-dockertag only if your tasks rundockerthemselves, including tasks withcompose_file. X.Y.Zis the only tag that never changes.X.Y,X, andlatestmove with new releases. Breaking changes only ship in a new major version, so1is safe to follow.- A prerelease gets only its exact
X.Y.Ztag, solatestnever points at a release candidate.
Required settings
Section titled “Required settings”The entrypoint refuses to start the daemon unless both of these are set, and exits 1 with a message saying what’s missing.
An auth setting. Set either RUNWISP_PASSWORD or RUNWISP_AUTH=off (no
login, trusted networks only). Without one, the daemon would generate a new
random password on every start, which nobody could read from a container. See
Authentication.
A config file at /etc/runwisp/runwisp.toml. On first start, if the file
is missing and /etc/runwisp is a writable mount, RunWisp writes the starter
config there. Otherwise a missing config stops the container, including when
the data volume already holds a database: that means the config volume was
mounted in the wrong place, and starting with the starter would quietly drop
your tasks.
Both checks apply to anything that starts a daemon: daemon, station,
restart, demo, bare runwisp, and any subcommand the entrypoint doesn’t
know. One-shot commands such as validate, run, list, status, reload,
import, and tui skip them:
docker run --rm -v ./runwisp:/etc/runwisp runwisp/runwisp:latest runwisp validateEnvironment defaults
Section titled “Environment defaults”The image sets these. Override any of them with -e.
| Variable | Image default | Purpose |
|---|---|---|
RUNWISP_CONFIG |
/etc/runwisp/runwisp.toml |
Where the config is read from. |
RUNWISP_DATA |
/var/lib/runwisp |
SQLite database, run logs, and the control socket. |
RUNWISP_HOST |
0.0.0.0 |
Listen on all interfaces, so port mapping works. |
RUNWISP_TLS |
unset (daemon defaults to off) |
See Plain HTTP by default. |
RUNWISP_LOG_FORMAT |
text |
Readable docker logs. Set json if a log collector parses them. |
RUNWISP_LOG_OUTPUT |
true |
Task output in docker logs. See log_output. |
Use -e RUNWISP_DATA=… rather than the --data flag. The healthcheck reads
only the environment, so a flag makes it look in the wrong place. The
entrypoint warns if you pass --data or --socket.
Volumes
Section titled “Volumes”/etc/runwisp: the directory holdingrunwisp.toml. RunWisp only writes to it once, to create the starter, so you can mount it:roafter that. Put more config files next torunwisp.tomland load them withinclude. Mount the directory rather than the single file: many editors save by replacing the file, and a single-file mount keeps showing the container the old copy, soreloadmisses your edit. If./runwispdoesn’t exist yet, Docker creates it owned by root; runmkdir runwispfirst to own it./var/lib/runwisp: the database and run logs. Without a volume here, every container recreate deletes your run history and logs you out.
Healthcheck
Section titled “Healthcheck”The image has a HEALTHCHECK that runs runwisp status, so docker ps and
compose’s depends_on: condition: service_healthy work without setup.
It talks to the control socket, which it finds from RUNWISP_DATA or
RUNWISP_SOCKET. A --data flag on the command line isn’t visible to it, and
the container then reports unhealthy although the daemon is fine. Use the
environment variables, or --no-healthcheck to supply your own.
Plain HTTP by default
Section titled “Plain HTTP by default”The daemon serves plain HTTP. Either:
- put a reverse proxy in front for TLS and set
RUNWISP_TRUSTED_PROXIESto its CIDR, so session cookies are still markedSecure(trusted_proxies); or - set
-e RUNWISP_TLS=autoand RunWisp serves HTTPS with a self-signed certificate.
Timezone
Section titled “Timezone”The timezone database is built into the binary, so
[daemon] timezone works on both bases
without extra packages. The image also installs tzdata for your tasks, so
date, PHP, Python, and similar tools get real zone data.
To use the host’s local time in tasks, set -e TZ=Europe/Helsinki or mount
/etc/localtime:/etc/localtime:ro.
Inside your own image
Section titled “Inside your own image”To replace cron or supervisord in your app’s image, copy the binary into it:
FROM php:8.4-fpmCOPY --from=runwisp/runwisp:1 /usr/local/bin/runwisp /usr/local/bin/runwispCOPY runwisp.toml /etc/runwisp/runwisp.tomlENV RUNWISP_LOG_OUTPUT=trueCMD ["runwisp", "daemon"]Run the container with init: true (docker run --init): RunWisp doesn’t reap
orphaned processes itself. List the processes as
services that stay in the foreground:
[services.php-fpm]run = "php-fpm -F"
[services.nginx]run = "nginx -g 'daemon off;'"Set RUNWISP_PASSWORD or RUNWISP_AUTH=off, as in
Required settings. From supervisord
maps the rest of a supervisord.conf.
Running as non-root
Section titled “Running as non-root”The image runs as root, because running a task as another
user needs root. If no task sets user, you
can run the container as another UID with user: in compose (or --user).
That UID must be able to write the /var/lib/runwisp volume; the entrypoint
tells you if it can’t.
Operating the container
Section titled “Operating the container”The CLI inside the container talks to the daemon over the control socket and needs no password:
docker compose exec runwisp runwisp statusdocker compose exec runwisp runwisp listdocker compose exec runwisp runwisp run helloWith docker run, use docker exec runwisp instead. After editing
runwisp.toml on the host, run runwisp reload the same way (see
Reload).