Skip to content

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.

Terminal window
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 9477

Browse 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 HEALTHCHECK calls the binary’s own runwisp status, which hits /health over loopback.
docker-compose.yml
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:

Terminal window
# .env (chmod 0600, do not commit)
RUNWISP_PASSWORD=change-me-please

Then 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.

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:

Terminal window
sudo chown -R 1000:1000 ./data # match the image's user
chmod 0750 ./data

If you see permission denied errors creating data/runwisp.db on first start, this is the cause.

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:

# Dockerfile
FROM alpine:3
RUN addgroup -S runwisp && adduser -S -G runwisp runwisp
COPY --from=ghcr.io/runwisp/runwisp:latest /usr/local/bin/runwisp /usr/local/bin/runwisp
COPY runwisp.toml /etc/runwisp/runwisp.toml
USER runwisp
WORKDIR /data
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD runwisp status
EXPOSE 9477
ENTRYPOINT ["/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.

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.

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.

Pull the new image, recreate the container, keep the volume:

Terminal window
docker compose pull
docker compose up -d --force-recreate

The 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.