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.
- You need Telegram, Discord, generic webhooks, or alert coalescing beyond Ofelia's global email/Slack reporters.
- 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 — and both columns win on something.
| Capability | Ofelia | RunWisp |
|---|---|---|
| Run history per firing | ~optional report files | ✓SQLite rows + UI |
| Captures stdout / stderr | ~optional files | ✓ |
| Retries with backoff | ✗ | ✓constant / linear / exp |
| Failure notifications | ✓email / Slack | ✓Slack / Telegram / Discord / email / webhook |
| Coalesced alerts (no notify storms) | ✗ | ✓ |
| Overlap policy | ~no-overlap | ✓skip / queue / kill |
| Catch-up after downtime | ✗ | ✓latest / all / skip |
| DST handling | ✗cron lib default | ✓deduped / shifted |
| 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 = 1fires 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" The version that runs in prod
Three jobs: DB backup, healthcheck, weekly cleanup. Ofelia can prevent overlap and report failures; the TOML version adds retained UI history, retries/backoff, timeouts, and broader alert routing.
# 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 = ["slack-ops"]
run = "pg_dump -U postgres mydb > /backups/nightly.sql"
[tasks.healthcheck]
cron = "@every 5m"
catch_up = 0
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 = true | on_overlap = "skip" (or queue / kill) |
mail-only-on-error / slack-only-on-error | notify = ["slack-ops"]; Telegram, Discord, email, and generic webhooks are also built in |
| Optional timestamped stdout/stderr + JSON report files | Captured automatically in SQLite; browse and stream 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. |
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 1; set 0 for probes
If the daemon was down during a scheduled healthcheck, you probably want fresh state, not a stale catch-up. Set catch_up = 0 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
Can RunWisp run jobs inside Docker containers like Ofelia does?
Does RunWisp need Docker at all?
Can I migrate from Ofelia one job at a time?
How does RunWisp handle missed firings after a reboot?
Is there a web dashboard?
What about the netresearch/ofelia fork?
What about Ofelia's job-local type?
Does RunWisp support the same cron syntax as Ofelia?
How much RAM does RunWisp use?
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.
curl -fsSL https://get.runwisp.com | sh