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.
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-execruns a command inside an already-running container. No sidecar, no volume dance. -
job-runspins 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
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
[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, 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, 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
No job-exec equivalent
Ofelia's job-exec runs a command inside a running container via the Docker API. RunWisp has no container exec primitive. If you need to exec into a container, call docker exec <container> <cmd> in the run field. Works, but the Docker CLI must be on the host.
No label-driven discovery
Ofelia reads container labels at startup and on Docker events. RunWisp reads a static TOML file. If your workflow relies on adding a label to a new service and having the scheduler pick it up automatically, you add a stanza to the TOML and run runwisp reload (no restart needed; the edit is validated first).
catch_up defaults to latest; set skip for probes
If the daemon was down during a scheduled healthcheck, you probably want fresh state, not a stale catch-up. Set catch_up = "skip" for probes and liveness checks.
Ofelia jobs and RunWisp can coexist
Ofelia runs as a Docker container reading Docker labels. RunWisp runs as a host binary reading a TOML file. They do not share state. Migrate one job at a time: remove the label, add the TOML stanza, run runwisp reload, confirm in the Web UI.
FAQ
Not natively. Ofelia talks to the Docker API to exec or run containers. RunWisp runs shell commands. You can call docker exec or docker run in the run field, but it is a shell command, not an API call. If tight Docker integration is the whole point, Ofelia is the simpler path.
No. It is a static Go binary that runs on Linux, macOS, WSL, or inside a Docker container. Docker is one deployment option, not a requirement. Ofelia requires the Docker socket.
Yes. Remove the Ofelia label from one container, add a [tasks.*] stanza to runwisp.toml, run runwisp reload (no restart; the edit is validated first). The two schedulers do not interact. Roll back by reversing the same steps.
catch_up = "latest" (the default) fires the most recent missed tick when the daemon starts. catch_up = "all" fires every missed tick. catch_up = "skip" does nothing. Ofelia has no catch-up mechanism.
Yes. RunWisp ships an embedded Web UI and a TUI in the same binary. Every firing, its exit code, duration, and captured output are visible. Ofelia has no dashboard.
job-local runs a command on the Ofelia container itself, not inside another container. The RunWisp equivalent is just run = "your-command". Since RunWisp runs on the host, every task is already local.
Yes, and a bit more. Both take 5-field cron plus aliases like @hourly, @daily, @every 5m. RunWisp also accepts six-field cron with an optional leading seconds field (*/30 * * * * * for every 30s), and adds per-task timezone, DST dedup on fall-back, and next-valid-minute on spring-forward.
About 25 MB idle. Ofelia is also lightweight since it is a Go binary in a container. Neither will stress a small VPS or a Raspberry Pi.
More comparisons: RunWisp vs cron · RunWisp vs PM2 · RunWisp vs supervisord .
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.