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.

Verdict

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.
Head to head

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
Trade-offs

Pros & cons

What Ofelia still does better

  • Config lives in Docker labels; no extra file to manage, version, or mount.
  • job-exec targets any container by name over the Docker API — no compose file, no Docker CLI on the host. RunWisp's exec is compose-scoped.
  • job-run spins 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 = 1 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 + run execs inside the live container, and compose_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

The simple case

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

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
[tasks.backup]
cron = "0 3 * * *"
run  = "/usr/local/bin/backup.sh"
Production-grade

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 (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, 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

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

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

FAQ

Can RunWisp run jobs inside Docker containers like Ofelia does?
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 in the run field like any other shell command.
Does RunWisp need Docker at all?
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.
Can I migrate from Ofelia one job at a time?
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.
How does RunWisp handle missed firings after a reboot?
catch_up = 1 (the default) fires the most recent missed tick when the daemon starts. Set catch_up = N to replay up to N missed ticks, or catch_up = 0 to do nothing. Ofelia has no catch-up mechanism.
Is there a web dashboard?
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. Upstream Ofelia has no dashboard; the netresearch fork adds an optional one.
What about the netresearch/ofelia fork?
This page compares upstream mcuadros/ofelia, the image most Compose stacks pull. The netresearch/ofelia fork is actively maintained and closes several gaps in the table above: job retries, webhook notifications (Slack, Discord, ntfy and more), an optional web UI with execution history, and job dependencies. If you want to stay label-driven inside Docker, try the fork before switching schedulers.
What about Ofelia's job-local type?
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.
Does RunWisp support the same cron syntax as Ofelia?
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 a spring-forward run shifted by the gap instead of skipped.
How much RAM does RunWisp use?
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.
Get started
GPL 3.0 · no accounts · free to self-host

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.

Install in 30 seconds
curl -fsSL https://get.runwisp.com | sh