Skip to content

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.

compose.yaml
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:

Terminal window
mkdir runwisp
docker compose up -d

Open 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:

Terminal window
docker compose exec runwisp runwisp reload

The same container with docker run:

Terminal window
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:latest

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.

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:

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 -docker tag 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:

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

For a job that belongs to no app (restic, curl a health URL), extend the image:

FROM runwisp/runwisp:latest
RUN apk add --no-cache restic curl

On a -debian tag, use apt-get install. The entrypoint, healthcheck, and environment defaults are kept. To check what an image contains:

Terminal window
docker run --rm --entrypoint sh runwisp/runwisp:latest -c 'command -v pg_dump curl'
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 -docker tag only if your tasks run docker themselves, including tasks with compose_file.
  • X.Y.Z is the only tag that never changes. X.Y, X, and latest move with new releases. Breaking changes only ship in a new major version, so 1 is safe to follow.
  • A prerelease gets only its exact X.Y.Z tag, so latest never points at a release candidate.

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:

Terminal window
docker run --rm -v ./runwisp:/etc/runwisp runwisp/runwisp:latest runwisp validate

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.

  • /etc/runwisp: the directory holding runwisp.toml. RunWisp only writes to it once, to create the starter, so you can mount it :ro after that. Put more config files next to runwisp.toml and load them with include. 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, so reload misses your edit. If ./runwisp doesn’t exist yet, Docker creates it owned by root; run mkdir runwisp first 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.

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.

The daemon serves plain HTTP. Either:

  • put a reverse proxy in front for TLS and set RUNWISP_TRUSTED_PROXIES to its CIDR, so session cookies are still marked Secure (trusted_proxies); or
  • set -e RUNWISP_TLS=auto and RunWisp serves HTTPS with a self-signed certificate.

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.

To replace cron or supervisord in your app’s image, copy the binary into it:

Dockerfile
FROM php:8.4-fpm
COPY --from=runwisp/runwisp:1 /usr/local/bin/runwisp /usr/local/bin/runwisp
COPY runwisp.toml /etc/runwisp/runwisp.toml
ENV RUNWISP_LOG_OUTPUT=true
CMD ["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:

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

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.

The CLI inside the container talks to the daemon over the control socket and needs no password:

Terminal window
docker compose exec runwisp runwisp status
docker compose exec runwisp runwisp list
docker compose exec runwisp runwisp run hello

With docker run, use docker exec runwisp instead. After editing runwisp.toml on the host, run runwisp reload the same way (see Reload).