RunWisp

Comparison · vs Ofelia

RunWisp vs Ofelia: scheduled jobs, with or without Docker.

Ofelia reads Docker labels and fires jobs inside containers. RunWisp reads a TOML file and fires jobs anywhere. Here is where they overlap and where they split.

Quick start GitHub

Should you switch?

The 10-second version. Skim, decide, scroll down for the receipts.

Switch to RunWisp if

  • You run jobs on bare metal, a VPS, or macOS alongside Docker.
  • You need to know whether last night's backup actually ran and what it printed.
  • Retries with backoff belong in the scheduler, not in your entrypoint script.
  • Failures should ping Slack or Telegram, not vanish into container logs.
  • You also supervise long-running services, not just scheduled tasks.
  • You want a Web UI or TUI without bolting on another container.

Stay on Ofelia if

  • Everything runs in Docker Compose and that is not changing.
  • Label-driven config means zero files outside docker-compose.yml.
  • You exec into running containers and that tight coupling is the point.
  • Adding a binary outside Docker is a non-starter for your stack.
  • Three labels and it works; you don't need history or retries.

Yes / no, at a glance

Yes / no, both columns win on something.

Feature Ofelia RunWisp
Run history per firing
SQLite rows
Captures stdout / stderr
Retries with backoff
constant / linear / exp
Failure notifications (Slack, etc.)
Coalesced alerts (no notify storms)
Overlap policy
skip / queue / kill
Catch-up after downtime
latest / all / skip
DST handling
cron lib default
deduped / next valid
Web UI / dashboard
TUI over SSH
Long-running services
[services.*]
Docker-label config
labels on containers
TOML file
Exec into running container
job-exec type
Runs without Docker
needs Docker socket
static binary
macOS / WSL / bare metal
Zero-file config
labels in compose
runwisp.toml

Pros & cons

What Ofelia still does better

  • Config lives in Docker labels; no extra file to manage, version, or mount.
  • job-exec runs a command inside an already-running container. No sidecar, no volume dance.
  • job-run spins up a fresh container per firing. Disposable, isolated, image-pinned.
  • Needs only the Docker socket. No binary on the host, no install step outside the compose file.
  • If your world is 100% Docker Compose, Ofelia disappears into the stack.

What RunWisp adds

  • Every firing is a row with status, duration, exit code, and captured stdout/stderr.
  • Retries and exponential backoff per task, not re-inventing them in shell scripts.
  • Slack / Telegram / Discord / email / webhook notifications with coalesced flapping alerts.
  • on_overlap = "skip" prevents two backups from running at once.
  • catch_up = "latest" fires missed jobs after a reboot or outage.
  • Supervise long-running services and scheduled tasks in the same config file.
  • Web UI and TUI ship in the binary; no extra container, no paid tier.
  • Runs on Linux, macOS, WSL, Docker, or a Raspberry Pi. No Docker socket required.

The simple case

Nightly backup at 03:00. Labels on the left, TOML on the right.

docker-compose.yml (Ofelia)
docker-compose.yml (Ofelia)
# docker-compose.yml
services:
  ofelia:
    image: mcuadros/ofelia:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    labels:
      ofelia.job-run.backup.schedule: "0 3 * * *"
      ofelia.job-run.backup.image: alpine
      ofelia.job-run.backup.command: "/bin/sh -c '/backup.sh'"
runwisp.toml
runwisp.toml
# runwisp.toml
[tasks.backup]
cron = "0 3 * * *"
run  = "/usr/local/bin/backup.sh"

Production-grade

Three jobs: DB backup, healthcheck, weekly cleanup. Labels can only fire the command; the TOML version adds retries, overlap policy, timeouts, and failure alerts.

docker-compose.yml (Ofelia, production)
docker-compose.yml (Ofelia, production)
# docker-compose.yml, multi-job setup
services:
  ofelia:
    image: mcuadros/ofelia:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    depends_on:
      - app

  app:
    image: myapp:latest
    labels:
      ofelia.job-exec.db-backup.schedule: "0 3 * * *"
      ofelia.job-exec.db-backup.command: "pg_dump -U postgres mydb > /backups/nightly.sql"
      ofelia.job-exec.healthcheck.schedule: "@every 5m"
      ofelia.job-exec.healthcheck.command: "curl -sf http://localhost:8080/health"
      ofelia.job-exec.cleanup.schedule: "0 4 * * 0"
      ofelia.job-exec.cleanup.command: "find /tmp -mtime +7 -delete"
runwisp.toml (production)
runwisp.toml (production)
# runwisp.toml, same three jobs, hardened
[tasks.db-backup]
description       = "Nightly Postgres dump"
cron              = "0 3 * * *"
on_overlap        = "skip"
timeout           = "50m"
retry_attempts    = 2
retry_delay       = "30s"
retry_backoff     = "exponential"
keep_runs         = 60
notify_on_failure = ["slack-ops"]
run               = "pg_dump -U postgres mydb > /backups/nightly.sql"

[tasks.healthcheck]
cron     = "@every 5m"
catch_up = "skip"
timeout  = "30s"
run      = "curl -sf http://localhost:8080/health"

[tasks.cleanup]
cron    = "0 4 * * 0"
timeout = "10m"
run     = "find /tmp -mtime +7 -delete"

Migration cheat sheet

Ofelia Docker label on the left, runwisp.toml field on the right.

Ofelia RunWisp
ofelia.job-exec.<name>.schedule [tasks.<name>] with cron =
ofelia.job-exec.<name>.command run =
ofelia.job-run.<name>.image Run the command directly or wrap with docker run in run =
ofelia.job-local.<name>.command run = (already local)
No retry mechanism retry_attempts + retry_backoff
No overlap guard on_overlap = "skip"
No failure notification notify_on_failure = ["slack-ops"]
No run history Captured automatically; browse in Web UI or TUI.
No timeout timeout = "50m"
Docker socket volume mount Not needed. RunWisp runs as a host binary.

Gotchas

FAQ

More comparisons: RunWisp vs cron · RunWisp vs PM2 · RunWisp vs supervisord .

Or see what RunWisp is in one page →

Move one Docker label to a TOML stanza. See it run.

Install the binary, paste the schedule into runwisp.toml, watch the first firing land in the Web UI with captured output and exit code.