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 / terminate |
| 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 a running container | any container | ~ compose services |
| Fresh container per firing | job-run, any image | ~ compose_mode = "run" |
| 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-exectargets any container by name over the Docker API — no compose file, no Docker CLI on the host. RunWisp's exec is compose-scoped. -
job-runspins up a fresh container from any image, compose-defined or not. - 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.
- Compose-backed tasks:
compose_file+compose_service+runexecs inside the live container, andcompose_mode = "run"starts a fresh one — with the history, retries, and alerts on top. - 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 = — plus compose_file + compose_service to exec inside the live container |
ofelia.job-run.<name>.image | compose_mode = "run" for a compose service, or docker run in run = for any other image |
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 for host tasks; compose-backed tasks shell out to the Docker CLI instead. |
Walkthrough: adding cron with a web UI to a docker-compose stack →
Gotchas
RunWisp's exec is compose-scoped, not arbitrary-container
Ofelia's job-exec targets any container by name over the Docker API. RunWisp's equivalent goes through Compose: compose_file + compose_service + run is a docker compose exec into that service's running container. So the service has to be compose-managed, it has to be up (otherwise the run fails visibly with container … is not running, not a silent skip), and the Docker CLI with the Compose plugin has to be reachable. For a container Compose doesn't manage, call docker exec <container> <cmd> in run like any other shell command.
timeout can't reach inside an exec
Docker offers no way to cancel an exec, so when a run times out RunWisp kills the local client and the process inside the container carries on. Bound long work on the inside too — run = "timeout 3600 php artisan long-job". RunWisp warns at startup if you put a long-running service in exec mode, where the leftover process compounds on every restart.
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
Yes, for Compose services. Point a task at compose_file + compose_service and add run, and RunWisp does a docker compose exec inside that service's already-running container — the live env, code version, and credentials, nothing new started. That is compose_mode = "exec", the default whenever run is set; compose_mode = "run" is the other half, a docker compose run --rm in a fresh container. The limits are real, though: the service has to be Compose-managed, its container has to be up, and the Docker CLI plus the Compose plugin has to be reachable. Ofelia's job-exec talks to the Docker API directly and can target any container by name, no compose file involved — for that, or for a container Compose does not manage, put docker exec <container> <cmd> in the run field like any other shell command.
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.