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, ntfy, generic webhooks, or alert coalescing beyond Ofelia's 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.
  • The netresearch fork (web UI, retries, webhooks) covers your gaps and you want to stay label-driven.
Head to head

Yes / no, at a glance

Yes / no — and both columns win on something.

Capability Ofelia RunWisp
Run history per firing ~container log + optional report files ✓SQLite rows + UI
Captures stdout / stderr ~optional files ✓
Retries with backoff ✗ ✓constant / linear / exp
Failure notifications ✓email / Slack ✓Slack / Telegram / ntfy / email / webhook
Coalesced alerts (no notify storms) ✗ ✓
Overlap policy ~no-overlap ✓skip / queue / kill
Catch-up after downtime ✗ ✓catch_up = 0 / 1 / N
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"
Swarm service jobs ✓job-service-run ✗
Runs without Docker ~INI + job-local only ✓static binary
macOS / WSL / bare metal ~binary, job-local only ✓
Five-field cron ~0.3.x wants a seconds field ✓5 or 6 fields
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.
  • job-run takes Docker options (volumes, network, user, entrypoint) and job-service-run covers Swarm services. RunWisp has neither.
  • In label mode it 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-invented in shell scripts.
  • Slack, Telegram, Discord, ntfy, email and webhook notifications with coalesced flapping alerts.
  • on_overlap, timeout and catch_up per task: no two backups at once, and a missed one runs after a reboot.
  • 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 history, retries and alerts on top.
  • Web UI and TUI ship in the binary: no extra container, no license key.
  • 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 (mcuadros/ofelia 0.3.x: six-field schedule, seconds first)
services:
  ofelia:
    image: mcuadros/ofelia:latest
    command: daemon --docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    labels:
      ofelia.job-run.backup.schedule: "0 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, all exec'd into the app container. 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
    command: daemon --docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    depends_on:
      - app

  app:
    image: myapp:latest
    labels:
      ofelia.enabled: "true"
      ofelia.job-exec.db-backup.schedule: "0 0 3 * * *"
      ofelia.job-exec.db-backup.command: "sh -c '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 0 4 * * 0"
      ofelia.job-exec.cleanup.command: "find /tmp -mtime +7 -delete"
runwisp.toml (production)
# runwisp.toml, same three jobs, exec'd into the same container
[tasks.db-backup]
description       = "Nightly Postgres dump"
cron              = "0 3 * * *"
compose_file      = "./docker-compose.yml"
compose_service   = "app"
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"
compose_file      = "./docker-compose.yml"
compose_service   = "app"
catch_up          = 0
timeout           = "30s"
run               = "curl -sf http://localhost:8080/health"

[tasks.cleanup]
cron              = "0 4 * * 0"
compose_file      = "./docker-compose.yml"
compose_service   = "app"
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 (six fields on 0.3.x) [tasks.<name>] with cron = (drop the seconds field, or keep it)
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"
ofelia.enabled label + daemon --docker Nothing to enable: tasks live in the TOML file.
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).

Ofelia 0.3.x wants a seconds field

On the latest tag (0.3.x) a five-field schedule is read as seconds-first, so "0 3 * * *" quietly fires at minute 3 of every hour. Ofelia's "0 0 3 * * *" is 03:00, the same as RunWisp's "0 3 * * *". RunWisp also accepts the six-field form, so you can paste the spec over unchanged.

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's label mode and its job-exec / job-run jobs need the Docker socket; its INI mode with job-local jobs doesn't.
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 far more active (a 1.0 release in September 2026) and closes several gaps in the table above: job retries, webhook notifications (Slack, Discord, ntfy and more), a web UI with execution history, and job dependencies. It ships its own image and changes the label workflow, so its compose files aren't a drop-in swap. 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. RunWisp takes five-field cron, six-field cron with a leading seconds field (the form Ofelia 0.3.x requires), and aliases like @hourly, @daily and @every 5m. It also adds per-task timezones and DST handling: a fall-back firing runs once, and a spring-forward firing runs shifted by the gap instead of being skipped.
How much RAM does RunWisp use?
An idle daemon with one task sits around 30 MB of RSS. Ofelia is also a small Go binary. 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