Deploy with Docker
RunWisp ships as a static binary, so the container can be tiny — there is no Python, no Node, no SQLite to install. The only state worth persisting is the data directory; mount it as a named volume and the rest is stateless.
This page covers a minimal docker run line, a docker-compose.yml,
the right healthcheck, and the gotchas around [storage] and
permissions.
Quick start
Section titled “Quick start”docker run -d --name runwisp \ -p 9477:9477 \ -v runwisp-data:/data \ -v $(pwd)/runwisp.toml:/etc/runwisp/runwisp.toml:ro \ -e RUNWISP_PASSWORD='change-me-please' \ --health-cmd='/usr/local/bin/runwisp status' \ --health-interval=30s \ --health-timeout=5s \ --health-retries=3 \ ghcr.io/runwisp/runwisp:latest \ daemon \ --config /etc/runwisp/runwisp.toml \ --data /data \ --host 0.0.0.0 \ --port 9477Browse to http://localhost:9477 and log in with the password you
set. The data lives in the named runwisp-data volume and survives
container restarts and image upgrades.
A few things to notice:
--host 0.0.0.0— the daemon binds inside the container; Docker publishes it to the host. Do not publish the port directly on the public internet — pair this with a TLS-terminating reverse proxy as described in the VPS guide.- The TOML config is mounted read-only — RunWisp never mutates
runwisp.toml, so this also matches the trust model. - The Docker
HEALTHCHECKcalls the binary’s ownrunwisp status, which hits/healthover loopback.
docker-compose
Section titled “docker-compose”services: runwisp: image: ghcr.io/runwisp/runwisp:latest command: - daemon - --config=/etc/runwisp/runwisp.toml - --data=/data - --host=0.0.0.0 - --port=9477 ports: - "127.0.0.1:9477:9477" volumes: - runwisp-data:/data - ./runwisp.toml:/etc/runwisp/runwisp.toml:ro environment: RUNWISP_PASSWORD: ${RUNWISP_PASSWORD:?set RUNWISP_PASSWORD in .env} healthcheck: test: ["CMD", "/usr/local/bin/runwisp", "status"] interval: 30s timeout: 5s retries: 3 restart: unless-stopped
volumes: runwisp-data:Put the password in a sibling .env file that Compose reads
automatically:
# .env (chmod 0600, do not commit)RUNWISP_PASSWORD=change-me-pleaseThen docker compose up -d and you’re running. The :? marker on the
substitution makes Compose refuse to start if the variable is unset, so
you cannot accidentally boot with a blank password.
Permissions
Section titled “Permissions”The container runs as a non-root user by default. The mounted volume needs to be writable by that user. With a named Docker volume that takes care of itself; with a host bind-mount, fix the ownership:
sudo chown -R 1000:1000 ./data # match the image's userchmod 0750 ./dataIf you see permission denied errors creating data/runwisp.db
on first start, this is the cause.
A custom Dockerfile
Section titled “A custom Dockerfile”The official image is enough for almost every use case, but if you
want to bake in your runwisp.toml, base on a tiny scratch-friendly
distro:
# DockerfileFROM alpine:3RUN addgroup -S runwisp && adduser -S -G runwisp runwispCOPY --from=ghcr.io/runwisp/runwisp:latest /usr/local/bin/runwisp /usr/local/bin/runwispCOPY runwisp.toml /etc/runwisp/runwisp.tomlUSER runwispWORKDIR /dataHEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD runwisp statusEXPOSE 9477ENTRYPOINT ["/usr/local/bin/runwisp", "daemon", \ "--config=/etc/runwisp/runwisp.toml", \ "--data=/data", \ "--host=0.0.0.0"]This is genuinely small (~25 MB image), boots instantly, and behaves identically to the prebuilt image.
Logs and stdout
Section titled “Logs and stdout”By default the daemon’s own diagnostic logs go to stderr — Docker
captures them via docker logs. Per-task output is captured to
/data/logs/<task>/ regardless. If you want the daemon’s logs in
your usual log shipper, just point at docker logs or
/var/lib/docker/containers/<id>/<id>-json.log.
Storage budget
Section titled “Storage budget”Inside a container, [storage] max_size and min_free_space apply
against the mounted volume — not the container layer or the host
disk. Size them against the volume:
[storage]max_size = "5gb"min_free_space = "500mb"If you’re running on an ephemeral disk (Fargate, GKE Autopilot,
Heroku-like), prefer 1gb / 200mb and treat the data dir as
not-quite-permanent — back it up off-host on a schedule.
See [storage] for the full breakdown.
Upgrading
Section titled “Upgrading”Pull the new image, recreate the container, keep the volume:
docker compose pulldocker compose up -d --force-recreateThe data dir is unchanged; the new daemon migrates the schema on first boot and resumes scheduling. Read Operations: upgrading before any major-minor jump — the CHANGELOG calls out breaking changes.
Where to next
Section titled “Where to next”- Deploy on a VPS — when “container plus reverse proxy” is the same target without the container.
- Recipes: Docker patterns — running tasks that
themselves invoke
docker run …from inside a RunWisp container. - Operations: data directory — what the volume actually contains.