Troubleshooting

Troubleshooting

Cronable runs entirely on your own machine, so almost every problem is a local configuration or environment issue with a clear, local fix. This page collects the ones operators hit most, with the exact cause and what to change. Most settings below are environment variables read at daemon startup — see also Operations & scaling and Security.

The daemon won’t start

The daemon (the engine) fails loudly — it exits non-zero with a one-line reason rather than starting in a broken state. Check its logs / journalctl -u cronable for the message.

”Encrypted secrets exist but CRONABLE_ENCRYPTION_KEY is not set”

You stored at least one secret, and the key that decrypts it is missing from the environment. The daemon refuses to start rather than run without access to its own secrets. Set CRONABLE_ENCRYPTION_KEY to the exact value that was set when the secrets were created.

”CRONABLE_ENCRYPTION_KEY does not match the stored secrets”

The key is set but wrong (or the encrypted store was tampered with) — decryption fails its authentication check. Set the correct key. If the key is genuinely lost, the secrets are unrecoverable by design (they are AES-256-GCM encrypted with a key derived from yours, and the key is never written to disk). You’ll need to clear the secrets store and re-enter them under a new key. Back up CRONABLE_ENCRYPTION_KEY separately from the database so this never happens — see Secrets & credentials.

The port is already in use

The daemon binds 127.0.0.1:8787 by default. If another process holds the port, change it with CRONABLE_PORT. The bind host is CRONABLE_HOST (default 127.0.0.1keep it on loopback; never expose the daemon port directly, put a reverse proxy in front instead).

Boot-time config warnings

On startup the daemon validates its environment and warns about values that are set but invalid (a malformed URL, an out-of-range number) instead of silently ignoring them. These are warnings, not fatal — but they usually explain why a feature isn’t behaving as expected. Read them and fix the named variable.

You can’t log in

First run — the setup screen

A brand-new install with no users lands on a “create the first admin” screen. Fill it in and you’re logged in on the canvas. This runs once: after an admin exists the setup endpoint returns 409 and the screen never reappears (it can’t be used to reset an existing admin).

Seeding an admin without the UI

For an unattended install you can seed the first admin from the environment: set both CRONABLE_ADMIN_USER and CRONABLE_ADMIN_PASSWORD before boot. The admin is created only when zero users exist — it never resets or overwrites an existing account — and the password must meet the minimum length or the admin is skipped with a warning.

Locked out after repeated attempts

Local login has brute-force protection: after several failed attempts for a username within a short window, further logins are temporarily blocked. This is expected — wait for the window to elapse and try again with the correct password. A successful login (or waiting out the window) clears the block.

Jobs aren’t running

Work through these in order:

  • Is the daemon running? The daemon owns every timer and child process — if it’s stopped, nothing fires. Run it under systemd with Restart=always (see the deployment guide).
  • Is the job enabled? A disabled job is skipped. Check the job’s state in the graph.
  • Is the schedule right? Verify the schedule points and the job’s timezone — a cron that looks wrong is usually a timezone mismatch (see Scheduling).
  • Is the job type installed? A job whose extension isn’t enabled is skipped rather than run. Check Settings → Extensions.
  • Licensed build refusing to run? A licensed daemon whose license is blocked (revoked, expired, or never activated) boots in an inert mode: it comes up, keeps its license heartbeat running, but refuses to start new runs until the license is valid again. In-flight runs are never interrupted, and recovery is seamless — once the license goes valid the scheduler re-arms with no restart. See Updates for activation.

Git sync isn’t syncing

Two-way git sync of the jobs repo is off by default — both the pull and push toggles start disabled. Enable them in Settings.

  • Authentication uses your machine’s own git credentials (your SSH agent/keys or a configured credential helper). Cronable never stores a token. If pushes fail with an auth error, confirm the daemon’s user can git push that remote from a shell.
  • A pull conflict aborts safely. Pull is git pull --rebase; if it hits a conflict the rebase is aborted and your working tree is left untouched — no data loss. The sync status shows conflict. Reconcile the jobs repo by hand (the jobs directory is an ordinary git repo), then let sync resume. Full detail in the git-sync reference.

The database or logs keep growing

Nothing should grow without bound, but retention is opt-in for run history so a fresh install keeps everything until you choose otherwise. If disk use climbs on a long-lived install, enable the retention and size caps documented in Operations & scaling — per-run log size cap, log age retention, run-row retention by age and count, and event retention.

Health checks

For a one-shot diagnostic, run cronable doctor — it checks the daemon, database, jobs directory, encryption key, Claude auth, job-config errors, license, and git sync and prints an OK / WARN / FAIL report, exiting non-zero if any check fails so a post-install step or a cron health check can gate on it:

cronable doctor && echo "healthy"

For programmatic monitoring, two unauthenticated HTTP endpoints make the daemon easy to gate a container on:

  • GET /api/health — a preflight report: { ok, db, configDir, encryptionKey, claudeAuth, configErrors }, the same signals the dashboard (and cronable doctor) surface. It is richer than a liveness ping — ok is false when a job file fails to load or the encryption key is missing even though the daemon is up and serving — so use it to diagnose, not to gate traffic.
  • GET /api/ready — the narrow liveness check: the daemon is ready (database + secret store initialized). It deliberately does NOT gate on config errors / Claude auth / the encryption key, so a container HEALTHCHECK can poll it and orchestration only routes traffic once the daemon is serving.

Both are reachable without a login or token so a health probe never needs a credential.