RunWisp vs Supercronic: cron in Docker that remembers what ran.
Supercronic is the go-to answer to “how do I run cron in a container”: a crontab runner that logs to stdout and passes your env through. RunWisp also runs in the container, then keeps every run's exit code and output, retries the failures, and tells you about them.
Should you switch?
The 10-second version. Skim, decide, scroll down for the receipts.
Switch to RunWisp if
- “Did last night's job run?” means grepping docker logs that may have rotated away.
- You want failures in Slack, Telegram, Discord or email, not only in Sentry.
- You're wrapping crontab lines in timeout and retry loops by hand.
- Someone needs a “run now” button that isn't docker exec.
- You'd like one config that also runs on the host, a Pi or a Mac.
- You also have long-running workers that need restarting.
Stay on Supercronic if
- Your crontab already works and stdout logs into Loki or Datadog are enough.
- You want the smallest possible thing in the image: one binary, no data volume, no port.
- You run it as PID 1 and rely on it to reap zombies.
- Sentry or Prometheus is already where your team looks for failures.
- Your crontab uses the year field or Quartz-style L / W / # modifiers.
Yes / no, at a glance
Yes / no — and both columns win on something.
| Capability | Supercronic | RunWisp |
|---|---|---|
| Built for containers | ✓its whole purpose | ✓official image + static binary |
| Reads a standard crontab | ✓ | ~import to TOML |
| Container env passed to jobs | ✓ | ✓ |
| Run history per firing | ✗stdout only | ✓SQLite rows + UI |
| Captured output per run | ~interleaved in docker logs | ✓per run, searchable |
| Retries with backoff | ✗ | ✓ |
| Per-job timeout | ✗wrap in timeout(1) | ✓ |
| Failure notifications | ~Sentry only | ✓Slack / Telegram / Discord / ntfy / email / webhook |
| Overlap policy | ~skip, or -overlapping | ✓skip / queue / kill |
| Catch-up after downtime | ✗ | ✓catch_up = N |
| Timezone per job | ~one CRON_TZ per file | ✓ |
| Web UI / TUI | ✗ | ✓ |
| Manual “run now” | ✗ | ✓UI, CLI, REST, hook token |
| Long-running services | ✗ | ✓[services.*] |
| Prometheus metrics | ✓per job | ✓opt-in /metrics |
| Sentry integration | ✓ | ✗webhook instead |
| Reaps zombies as PID 1 | ✓ | ✗use init: true |
| Stateless: no volume, no port | ✓ | ✗wants a data volume |
| Year field, L / W / # modifiers | ✓ | ✗ |
| macOS / bare metal | ✗Linux builds only | ✓ |
Pros & cons
What Supercronic does better
- Smaller footprint: one ~14 MB binary, about 11 MB of RAM, no database, no port, no volume.
- Takes the crontab you already have. No new config format to learn.
- Built-in PID 1 handling: it forks itself, reaps zombies, and forwards signals.
- Sentry DSN and per-job Prometheus histograms are one flag each.
-inotifyreloads the crontab when a Kubernetes ConfigMap swaps under it.- Fully stateless. Kill the container, start another, nothing to migrate.
What RunWisp adds
- Every firing is a row: status, duration, exit code, peak memory, and its own stdout/stderr, kept per task.
timeout,retry_attemptsandretry_backoffas fields, not shell wrappers in every crontab line.- Failure alerts to Slack, Telegram, Discord, ntfy, Gotify, Pushover, email or a webhook, with repeats grouped during an outage.
on_overlap = "skip" | "queue" | "kill", andcatch_upfor firings missed while the container was down.- A web UI and a TUI with a Run button, live logs, and a phone-friendly layout.
failures = ['+output:ERROR']fails a run that printed an error but exited 0.- Supervises long-running workers in the same file, so one container can hold the worker and its schedule.
- Same binary and TOML on a VPS, a Pi, macOS or WSL, outside Docker too.
The simple case
A Django job at 03:00, inside the app image. Both are one static binary added to your Dockerfile. RunWisp's binary is a COPY away from the official image.
# Dockerfile: the README stanza, pinned by SHA-1
ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.49/supercronic-linux-amd64 \
SUPERCRONIC_SHA1SUM=e63c11a9726b775a6a11801e81af4f3fb926aa68 \
SUPERCRONIC=supercronic-linux-amd64
RUN curl -fsSLO "$SUPERCRONIC_URL" \
&& echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \
&& chmod +x "$SUPERCRONIC" \
&& mv "$SUPERCRONIC" /usr/local/bin/supercronic
COPY crontab /app/crontab
CMD ["supercronic", "/app/crontab"]
# crontab
0 3 * * * python manage.py clearsessions # Dockerfile: copy the static binary out of the official image
COPY --from=runwisp/runwisp:1 /usr/local/bin/runwisp /usr/local/bin/runwisp
COPY runwisp.toml /etc/runwisp/runwisp.toml
ENV RUNWISP_CONFIG=/etc/runwisp/runwisp.toml \
RUNWISP_DATA=/var/lib/runwisp
CMD ["runwisp", "daemon"]
# runwisp.toml
[tasks.clear-sessions]
cron = "0 3 * * *"
run = "python manage.py clearsessions" The version that runs in prod
Three jobs, hardened. Supercronic's answer is a second container from the app image with JSON logs, metrics and Sentry; timeouts live in the crontab line. RunWisp runs as its own service, execs into the live app container, and keeps history, retries and alerts per task.
# compose.yaml: a second copy of the app image runs the crontab
services:
cron:
image: myapp:latest
command: >
supercronic -json -split-logs
-prometheus-listen-address 0.0.0.0
-sentry-dsn ${SENTRY_DSN}
/app/crontab
# crontab: no timeout or retry in Supercronic, so wrap each line
0 3 * * * timeout 55m /app/bin/backup.sh
*/5 * * * * curl -fsS http://app:8080/health
0 4 * * 0 find /tmp -mtime +7 -delete # compose.yaml: one runwisp service execs into the live app container
# runwisp:
# image: runwisp/runwisp:latest-docker
# volumes: [/var/run/docker.sock:/var/run/docker.sock, ...]
# runwisp.toml
[defaults]
compose_file = "/srv/app/compose.yaml"
compose_service = "app"
notify = ["slack-ops"]
[tasks.backup]
cron = "0 3 * * *"
on_overlap = "skip"
timeout = "55m"
retry_attempts = 2
retry_backoff = "exponential"
keep_runs = 60
run = "/app/bin/backup.sh"
[tasks.healthcheck]
cron = "*/5 * * * *"
catch_up = 0
timeout = "30s"
run = "curl -fsS http://app:8080/health" Migration cheat sheet
Supercronic flag or crontab habit on the left, runwisp.toml on the right.
| Supercronic | RunWisp |
|---|---|
supercronic /app/crontab | runwisp import cron /app/crontab -o runwisp.toml, then runwisp daemon |
| A crontab line | [tasks.<name>] with cron = and run = |
KEY=value lines / container env | Container env is inherited; per-task env, env_file, secrets_file |
CRON_TZ=Europe/Berlin | timezone = "Europe/Berlin", per task or global |
Default no-overlap / -overlapping | on_overlap = "skip" / max_concurrent |
timeout 55m cmd in the line | timeout = "55m" |
-sentry-dsn | notify = ["slack-ops"], or a webhook notifier |
-prometheus-listen-address | [daemon] metrics_enabled = true |
-json logs in docker logs | Per-run logs in the Web UI and TUI; daemon logs stay on stdout |
SIGUSR2 / -inotify reload | runwisp reload or SIGHUP, validated before applying |
-test | runwisp validate |
Gotchas
Six fields mean different things
In Supercronic a 6-field line adds a year at the end; seconds need all 7 fields. In RunWisp 6 fields means a leading seconds field. Year fields and the L, W, # modifiers have no RunWisp equivalent. Check any line with more than five fields by hand after runwisp import cron.
RunWisp doesn't reap zombies as PID 1
Supercronic forks itself when it is PID 1 so the parent can reap orphaned processes. RunWisp doesn't. If your jobs spawn background processes, run the container with init: true in compose (or docker run --init) so tini does it.
Give it a volume and an auth setting
Run history lives in SQLite under RUNWISP_DATA. Without a volume it is wiped on every redeploy, which puts you back at Supercronic's level. The daemon also wants RUNWISP_PASSWORD, or RUNWISP_AUTH=off if the UI is never exposed.
You may not need a binary in the app image at all
Supercronic has to live in the image whose code it runs. RunWisp can run as its own compose service and exec into your app's running container with compose_file + compose_service, so the app image stays untouched and one RunWisp covers every service in the stack.
FAQ
What is the best Supercronic alternative for Docker?
Can RunWisp run inside my app's container like Supercronic?
Can I keep my crontab file?
Does Supercronic retry failed jobs or send alerts?
Is RunWisp heavier than Supercronic?
Does RunWisp work in Kubernetes?
How do I see what a Supercronic job printed last Tuesday?
Keep the crontab habit. Lose the guessing.
Import your crontab, drop the binary into the image or run it as a compose service, and watch the first firing land with its exit code and output.
curl -fsSL https://get.runwisp.com | sh