DEV Community

Cover image for Cronix: A Lightweight, Docker-Native Cron Scheduler for HTTP Jobs
Mohamad Ashraful Islam
Mohamad Ashraful Islam

Posted on

Cronix: A Lightweight, Docker-Native Cron Scheduler for HTTP Jobs

If you have ever managed a home server, self-hosted infrastructure, Docker containers, or small automation workloads, you have probably reached for cron.

Cron is excellent. It is simple, mature, and available almost everywhere. But as soon as scheduled jobs start growing in number, managing them through scattered crontab entries becomes less convenient.

You may want to:

  • See all scheduled jobs from one place.
  • Enable or disable jobs without editing a crontab.
  • Run a job manually.
  • Retry failed HTTP requests.
  • Inspect the output of previous executions.
  • Manage schedules through an API.
  • Run everything inside a small Docker container.
  • Avoid installing a full application runtime just to execute HTTP requests.

This is the problem Cronix is designed to solve.

GitHub repository

Cronix is a lightweight, cron-style scheduler for HTTP jobs that runs inside Docker. Jobs can be managed through a CLI, REST API, or web interface, while their configuration and execution history are persisted as JSON files. The project is written in Go, with a small React/Vite frontend embedded directly into the Go binary. (GitHub)


What Is Cronix?

At its core, Cronix is a scheduler.

You define a job with:

  1. A name
  2. A cron schedule
  3. A curl command
  4. Optional retries
  5. A retry delay
  6. An enabled/disabled state

For example:

{
  "name": "healthcheck",
  "schedule": "*/5 * * * *",
  "curl": "curl -s https://example.com/health",
  "retries": 2,
  "retry_delay": 10,
  "enabled": true
}
Enter fullscreen mode Exit fullscreen mode

Cronix evaluates the schedule and executes the configured HTTP request at the appropriate time.

Unlike traditional cron, however, Cronix provides a management layer around those jobs.

You can create and manage jobs using:

  • Web UI
  • CLI
  • REST API

The scheduler itself runs as a Docker container, making it particularly convenient for self-hosted environments. (GitHub)


Why Build Another Cron Scheduler?

Linux already has cron. Docker also has several cron-compatible solutions.

So why build Cronix?

The answer is not that traditional cron is bad.

It is that cron is intentionally minimal.

A traditional crontab might look like:

*/5 * * * * curl -s https://example.com/health
0 2 * * * curl -s https://example.com/backup
30 9 * * 1 curl -s https://example.com/report
Enter fullscreen mode Exit fullscreen mode

This works perfectly.

But managing these jobs becomes less pleasant when you need to:

  • Temporarily disable one job.
  • Run a job immediately.
  • See when a job last succeeded.
  • Keep execution history.
  • Retry failed requests.
  • Expose scheduling through an API.
  • Give yourself a small dashboard.

Cronix adds those capabilities while deliberately keeping the execution model small.


The Design Philosophy

One of the most interesting aspects of Cronix is its deliberately constrained execution model.

A Cronix job is fundamentally an HTTP request executed through curl.

The project does not attempt to become a general-purpose shell job runner.

The container contains essentially three important runtime components:

Cronix binary
Static curl binary
CA certificate bundle
Enter fullscreen mode Exit fullscreen mode

The runtime image is built on scratch, keeping the final container extremely small and avoiding unnecessary operating-system packages or runtime dependencies. (GitHub)

This makes Cronix a particularly interesting example of a single-purpose containerized application.


Architecture

At a high level, Cronix looks like this:

                    ┌─────────────────────┐
                    │      Web Browser    │
                    └──────────┬──────────┘
                               │
                               │ HTTP
                               ▼
┌──────────────┐      ┌─────────────────────┐
│     CLI      │─────►│                     │
└──────────────┘      │      Cronix         │
                      │                     │
┌──────────────┐      │  ┌───────────────┐  │
│  REST API    │─────►│  │   Scheduler   │  │
└──────────────┘      │  └───────┬────────┘  │
                      │          │            │
                      │          ▼            │
                      │     Job Runner        │
                      │          │            │
                      │          ▼            │
                      │       curl            │
                      └──────────┬────────────┘
                                 │
                                 ▼
                         External HTTP API
Enter fullscreen mode Exit fullscreen mode

The repository separates the application into several areas.

cmd/
internal/
ui/
Dockerfile
Makefile
go.mod
README.md
Enter fullscreen mode Exit fullscreen mode

The Go entrypoint lives under cmd/cronix, while the application implementation is organized under internal/. The frontend is located in ui/ and is built into the Go application using go:embed. (GitHub)


Two Operating Modes

Cronix essentially has two modes.

Server Mode

Running:

/cronix
Enter fullscreen mode Exit fullscreen mode

starts the main server.

The server:

  • Loads persisted jobs.
  • Starts the scheduler.
  • Starts the HTTP API.
  • Serves the web interface.

The API token is required before the server starts. (GitHub)


CLI Mode

Running:

/cronix cli <command>
Enter fullscreen mode Exit fullscreen mode

turns Cronix into an API client.

For Docker deployments, you can execute the CLI from the host using:

docker exec cronix /cronix cli ...
Enter fullscreen mode Exit fullscreen mode

This is an elegant design because the same binary provides both the server and administrative interface.

There is no separate management application that needs to be installed.


Getting Started

The simplest way to run Cronix is with Docker.

First create a data directory:

mkdir -p /tmp/cronix-data
Enter fullscreen mode Exit fullscreen mode

Then start the container:

docker run --rm -d \
  --name cronix \
  -e CRONIX_API_TOKEN=sekrit \
  -e CRONIX_USERNAME=admin \
  -e CRONIX_PASSWORD=admin \
  -v /tmp/cronix-data:/data \
  -p 8080:8080 \
  cronix
Enter fullscreen mode Exit fullscreen mode

After starting the container, open:

http://localhost:8080
Enter fullscreen mode Exit fullscreen mode

The web UI can then be used to manage jobs. (GitHub)

For anything beyond a local test environment, change the default credentials and use a strong API token.


Docker Compose

For a real deployment, Docker Compose is generally more convenient.

A simple configuration could look like:

services:
  cronix:
    image: cronix
    container_name: cronix
    restart: unless-stopped

    ports:
      - "8080:8080"

    environment:
      CRONIX_API_TOKEN: change-this-to-a-long-random-token
      CRONIX_USERNAME: admin
      CRONIX_PASSWORD: change-this-password
      TZ: Asia/Dhaka

    volumes:
      - ./data:/data
Enter fullscreen mode Exit fullscreen mode

The important part is the /data volume.

Without persistent storage, jobs and run history would disappear when the container is recreated.


How Jobs Are Stored

Cronix intentionally uses JSON rather than a database.

By default:

/data/jobs.json
/data/runs.json
Enter fullscreen mode Exit fullscreen mode

are used for job configuration and execution history respectively.

A job might look like:

{
  "id": "3f8a1c2b0d4e",
  "name": "ping",
  "schedule": "* * * * *",
  "curl": "curl -s https://example.com",
  "retries": 0,
  "retry_delay": 5,
  "enabled": true
}
Enter fullscreen mode Exit fullscreen mode

The JSON store has an important reliability characteristic.

Writes are performed using a temporary file followed by a rename.

Conceptually:

jobs.json
    │
    ├── write temporary file
    │
    └── atomic rename
             │
             ▼
        jobs.json
Enter fullscreen mode Exit fullscreen mode

This reduces the risk of leaving a partially written JSON file if the process crashes during a write. (GitHub)

For a lightweight scheduler that does not need thousands of concurrent job updates, this is a very practical trade-off.


Why JSON Instead of PostgreSQL or SQLite?

This is an important architectural decision.

A database would certainly provide more capabilities.

But Cronix is intentionally small.

If the application only needs to store:

jobs
+
run history
Enter fullscreen mode Exit fullscreen mode

a JSON file can be enough.

This means the deployment does not require:

Cronix
PostgreSQL
Database migrations
Database credentials
Database backups
Connection pooling
Enter fullscreen mode Exit fullscreen mode

Instead:

Cronix
  │
  └── /data
       ├── jobs.json
       └── runs.json
Enter fullscreen mode Exit fullscreen mode

For a small self-hosted scheduler, that simplicity is valuable.


Cron Expressions

Cronix uses standard five-field cron expressions:

minute hour day-of-month month day-of-week
Enter fullscreen mode Exit fullscreen mode

For example:

* * * * *
Enter fullscreen mode Exit fullscreen mode

means every minute.

Other examples:

*/5 * * * *
Enter fullscreen mode Exit fullscreen mode

Every five minutes.

0 9 * * *
Enter fullscreen mode Exit fullscreen mode

Every day at 09:00.

0 0 * * 1-5
Enter fullscreen mode Exit fullscreen mode

Every weekday at midnight.

30 2 1 * *
Enter fullscreen mode Exit fullscreen mode

At 02:30 on the first day of every month.

Cronix supports:

  • Ranges
  • Lists
  • Steps
  • Month names
  • Weekday names

The scheduler evaluates these schedules using the container's configured timezone. (GitHub)


Timezones Matter

A common problem with containerized schedulers is timezone confusion.

Your host may be in Bangladesh:

Asia/Dhaka
Enter fullscreen mode Exit fullscreen mode

while the container might effectively operate using another timezone.

Cronix provides the TZ environment variable for this purpose.

For example:

environment:
  TZ: Asia/Dhaka
Enter fullscreen mode Exit fullscreen mode

This is especially important for jobs such as:

Every day at 02:00
Every Monday at 09:00
Every month on the first day
Enter fullscreen mode Exit fullscreen mode

Without explicitly considering the timezone, a perfectly valid cron expression can execute at an unexpected time.


The Interesting Part: Curl-Only Jobs

Cronix intentionally restricts jobs to commands beginning with:

curl
Enter fullscreen mode Exit fullscreen mode

For example:

curl -s https://example.com
Enter fullscreen mode Exit fullscreen mode

or:

curl -s \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice"}' \
  https://api.example.com/users
Enter fullscreen mode Exit fullscreen mode

This restriction is deliberate.

Cronix does not provide a general-purpose shell.

There is no:

curl ... && something
Enter fullscreen mode Exit fullscreen mode

There is no:

curl ... | grep ...
Enter fullscreen mode Exit fullscreen mode

There is no:

curl ... > file.txt
Enter fullscreen mode Exit fullscreen mode

There is no:

curl $MY_VARIABLE
Enter fullscreen mode Exit fullscreen mode

The command is tokenized and executed directly rather than being passed through a shell. (GitHub)


Why Avoid a Shell?

This is one of the strongest design choices in the project.

A general shell executor introduces a much larger attack surface.

If arbitrary shell commands were allowed, a job definition could potentially contain:

rm -rf ...
Enter fullscreen mode Exit fullscreen mode

or:

curl ... && malicious-command
Enter fullscreen mode Exit fullscreen mode

or other shell features.

Instead, Cronix effectively follows:

Job definition
     │
     ▼
Validate command starts with curl
     │
     ▼
Parse arguments
     │
     ▼
Execute curl directly
Enter fullscreen mode Exit fullscreen mode

There is no shell expansion.

That means shell constructs such as pipes, redirects, globbing, command substitution, and environment-variable expansion are not available inside job definitions. (GitHub)

For an HTTP scheduler, this is a very sensible boundary.


HTTPS Support

The container includes a CA certificate bundle, and Cronix configures curl to use it.

That means normal HTTPS requests can perform certificate verification without requiring users to manually configure certificates inside the container. (GitHub)

For example:

curl -s https://api.example.com/health
Enter fullscreen mode Exit fullscreen mode

works as expected with TLS certificate validation.


Retries

Network requests fail.

DNS can fail.

A service can temporarily return an error.

A remote server can be restarting.

This is why Cronix supports retries.

Suppose you configure:

{
  "retries": 2,
  "retry_delay": 10
}
Enter fullscreen mode Exit fullscreen mode

Cronix can make up to:

Attempt 1
   │
   ├── success ──► DONE
   │
   └── failure
          │
          ▼
       wait 10s
          │
          ▼
Attempt 2
   │
   ├── success ──► DONE
   │
   └── failure
          │
          ▼
       wait 10s
          │
          ▼
Attempt 3
   │
   └── final result
Enter fullscreen mode Exit fullscreen mode

The important detail is that retries represents additional attempts.

So:

retries = 0 → 1 attempt
retries = 1 → 2 attempts
retries = 2 → 3 attempts
Enter fullscreen mode Exit fullscreen mode

The first successful attempt stops the run. (GitHub)


Run History

Cronix does more than execute jobs.

It remembers what happened.

Each execution is recorded in the run history.

A run contains information such as:

{
  "job_id": "3f8a1c2b0d4e",
  "trigger": "scheduled",
  "time": "2026-08-17T12:34:56Z",
  "status": "failed",
  "exit_code": 6,
  "results": [
    {
      "attempt": 1,
      "total": 2,
      "exit": 6,
      "output": "curl: (6) Could not resolve host"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

There are three important states:

ok
failed
error
Enter fullscreen mode Exit fullscreen mode

ok means the command eventually succeeded.

failed means the command executed but returned a non-zero exit code after all attempts.

error means the command could not be started. (GitHub)


Output Is Bounded

Cronix captures combined stdout and stderr from each attempt.

But it does not keep unlimited output.

The stored output is limited to the last 4 KiB.

This is a useful protection against a noisy endpoint filling the run-history file indefinitely. (GitHub)

Run history is also capped at the last 50 runs per job.

That keeps the storage model predictable.


Manual Execution

Scheduled jobs are not always enough.

Sometimes you want to test a job immediately.

Cronix supports:

docker exec cronix \
  /cronix cli run \
  --token sekrit \
  ping1
Enter fullscreen mode Exit fullscreen mode

This is useful when:

  • Testing a new API.
  • Debugging a failed job.
  • Verifying authentication.
  • Checking a webhook.
  • Confirming that the container can reach an external service.

The result shows the individual attempts and their exit codes. (GitHub)


CLI Management

Cronix provides a straightforward CLI.

List jobs:

docker exec cronix \
  /cronix cli list \
  --token sekrit
Enter fullscreen mode Exit fullscreen mode

Create a job:

docker exec cronix \
  /cronix cli add \
  --name healthcheck \
  --schedule '*/5 * * * *' \
  --curl 'curl -s https://example.com/health' \
  --retries 2 \
  --retry-delay 10 \
  --token sekrit
Enter fullscreen mode Exit fullscreen mode

Get a job:

docker exec cronix \
  /cronix cli get \
  --token sekrit \
  ping1
Enter fullscreen mode Exit fullscreen mode

Disable a job:

docker exec cronix \
  /cronix cli disable \
  --token sekrit \
  ping1
Enter fullscreen mode Exit fullscreen mode

Enable it again:

docker exec cronix \
  /cronix cli enable \
  --token sekrit \
  ping1
Enter fullscreen mode Exit fullscreen mode

Delete it:

docker exec cronix \
  /cronix cli delete \
  --token sekrit \
  ping1
Enter fullscreen mode Exit fullscreen mode

The available commands include:

list
get
add
update
delete
enable
disable
run
help
Enter fullscreen mode Exit fullscreen mode

The CLI also has defined exit codes, making it easier to integrate with scripts and automation. (GitHub)


REST API

One of the biggest advantages over a traditional crontab is the REST API.

All API routes are under:

/api/v1/
Enter fullscreen mode Exit fullscreen mode

and require bearer-token authentication.

The major endpoints include:

Method Endpoint Purpose
POST /api/v1/login Login and receive a session token
GET /api/v1/jobs List jobs
POST /api/v1/jobs Create a job
GET /api/v1/jobs/{id} Get a job
PUT /api/v1/jobs/{id} Update a job
DELETE /api/v1/jobs/{id} Delete a job
GET /api/v1/jobs/{id}/runs Get run history
POST /api/v1/jobs/{id}/run Run immediately

(GitHub)

This opens up many possibilities.

For example, another application could automatically create a Cronix job:

curl -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "backup",
    "schedule": "0 2 * * *",
    "curl": "curl -s https://backup.example.com/run"
  }' \
  http://cronix:8080/api/v1/jobs
Enter fullscreen mode Exit fullscreen mode

Now your application itself can manage scheduled automation.


Web UI

For people who don't want to manage everything from the command line, Cronix includes a small web interface.

The dashboard provides an overview of:

Total jobs
Enabled jobs
Healthy jobs
Failing jobs
Enter fullscreen mode Exit fullscreen mode

You can also:

  • Search jobs.
  • Create jobs.
  • Edit jobs.
  • Enable/disable jobs.
  • Run jobs immediately.
  • Delete jobs.
  • View job details.
  • Inspect execution history.

The job detail page exposes the schedule, retry configuration, curl command, and historical executions. (GitHub)


The Frontend Is Embedded

Another interesting implementation detail is that the frontend does not require a separate web server.

The UI is built using React and Vite.

The resulting static files are embedded into the Go binary using:

go:embed
Enter fullscreen mode Exit fullscreen mode

So the deployment becomes:

             Cronix
                │
       ┌────────┴────────┐
       │                 │
    Backend              UI
       │                 │
       └────────┬────────┘
                │
          Single binary
Enter fullscreen mode Exit fullscreen mode

There is no need to deploy:

Nginx
Node.js
React server
Separate frontend container
Enter fullscreen mode Exit fullscreen mode

The Go application serves the SPA directly. (GitHub)

This is exactly the kind of architecture that makes sense for a small self-hosted tool.


Authentication

Cronix has two authentication concepts.

API Token

The server requires:

CRONIX_API_TOKEN
Enter fullscreen mode Exit fullscreen mode

API requests use:

Authorization: Bearer <token>
Enter fullscreen mode Exit fullscreen mode

Requests without valid credentials receive an authentication error. (GitHub)


Web Login

The web UI uses:

CRONIX_USERNAME
CRONIX_PASSWORD
Enter fullscreen mode Exit fullscreen mode

The defaults are:

admin
admin
Enter fullscreen mode Exit fullscreen mode

These defaults are convenient for testing but should absolutely be changed for a real deployment. (GitHub)


Session Management

After logging in, the web interface receives a session token.

The frontend stores it in:

sessionStorage
Enter fullscreen mode Exit fullscreen mode

rather than persistent local storage.

That means closing the browser tab effectively ends the session.

The configured session lifetime defaults to:

24h
Enter fullscreen mode Exit fullscreen mode

and can be changed using:

CRONIX_SESSION_TTL
Enter fullscreen mode Exit fullscreen mode

(GitHub)


Reverse Proxy Deployment

Cronix does not provide TLS itself.

The project explicitly recommends putting a reverse proxy in front of it when exposing it beyond localhost. (GitHub)

For example:

Internet
    │
    │ HTTPS
    ▼
Nginx Proxy Manager
    │
    │ HTTP
    ▼
Cronix :8080
Enter fullscreen mode Exit fullscreen mode

This is particularly useful in a self-hosted environment where you already have a reverse proxy.

You could expose:

https://cron.example.com
Enter fullscreen mode Exit fullscreen mode

while Cronix itself continues listening internally on:

http://cronix:8080
Enter fullscreen mode Exit fullscreen mode

The reverse proxy handles:

  • TLS certificates
  • HTTPS termination
  • Domain routing
  • External access

while Cronix focuses on scheduling.


Environment Variables

Cronix exposes several environment variables:

Variable Default Purpose
CRONIX_API_TOKEN Required API bearer token
CRONIX_USERNAME admin Web username
CRONIX_PASSWORD admin Web password
CRONIX_SESSION_TTL 24h Web session lifetime
CRONIX_STORE_PATH /data/jobs.json Job storage
CRONIX_RUNS_PATH /data/runs.json Run history
CRONIX_HTTP_ADDR :8080 HTTP listen address
CURL_PATH /usr/local/bin/curl Curl executable
TZ Container timezone Scheduler timezone

(GitHub)

This makes Cronix easy to configure through Docker Compose or Kubernetes-style environments.


A Practical Example: Monitoring a Home Server

Imagine you have a self-hosted server with several services.

For example:

Jellyfin
Immich
Nginx Proxy Manager
Pi-hole
Expense Tracker
Enter fullscreen mode Exit fullscreen mode

Instead of manually checking each service, you could expose health endpoints.

For example:

https://jellyfin.example.com/health
https://immich.example.com/api/server/ping
https://example.com/api/health
Enter fullscreen mode Exit fullscreen mode

Then create Cronix jobs:

Jellyfin Health
    every 5 minutes

Immich Health
    every 5 minutes

API Health
    every 5 minutes
Enter fullscreen mode Exit fullscreen mode

A job might be:

curl -fsS https://example.com/health
Enter fullscreen mode Exit fullscreen mode

The -f option causes curl to return a failure status for HTTP error responses.

Cronix then interprets the exit code.

That gives you a simple monitoring mechanism:

Cronix
   │
   ├── Healthcheck A ──► 200 OK
   │
   ├── Healthcheck B ──► 200 OK
   │
   └── Healthcheck C ──► HTTP 500
                         │
                         ▼
                       retry
                         │
                         ▼
                       retry
                         │
                         ▼
                       failed
Enter fullscreen mode Exit fullscreen mode

The failure is then visible in the dashboard and run history.


Webhooks Are Another Natural Use Case

Cronix is especially useful for webhook-based automation.

For example:

curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"action":"backup"}' \
  https://automation.example.com/webhook
Enter fullscreen mode Exit fullscreen mode

This could trigger:

  • Backup processes
  • Deployment pipelines
  • Notification systems
  • Home automation
  • Data synchronization
  • API maintenance tasks
  • Cache invalidation
  • Report generation

The scheduler does not need to know what the remote service does.

Its responsibility is simply:

When?
  ↓
Execute HTTP request
  ↓
Record result
Enter fullscreen mode Exit fullscreen mode

That keeps the system generic.


Example: Daily Backup Trigger

Suppose you have an external backup service with an API endpoint:

POST https://backup.example.com/run
Enter fullscreen mode Exit fullscreen mode

You could configure:

{
  "name": "nightly-backup",
  "schedule": "0 2 * * *",
  "curl": "curl -fsS -X POST https://backup.example.com/run",
  "retries": 3,
  "retry_delay": 60,
  "enabled": true
}
Enter fullscreen mode Exit fullscreen mode

The behavior becomes:

02:00
 │
 ▼
Start backup request
 │
 ├── success ──► complete
 │
 └── failure
       │
       ▼
     wait 60s
       │
       ▼
     retry
Enter fullscreen mode Exit fullscreen mode

This is much easier to manage than scattering shell scripts and crontab entries across multiple servers.


Graceful Shutdown

Cronix also handles container shutdown gracefully.

When receiving:

SIGTERM
SIGINT
Enter fullscreen mode Exit fullscreen mode

the scheduler stops scheduling new jobs and waits up to 10 seconds for in-flight jobs before exiting. (GitHub)

This matters in Docker because:

docker stop cronix
Enter fullscreen mode Exit fullscreen mode

should not abruptly terminate a request that is already running whenever possible.


Container Hardening

The container runs as root by default, but Cronix does not require special privileges for its curl execution.

The documentation therefore suggests that users who want additional hardening can run the container with a non-root user, for example:

docker run \
  --user 65534:65534 \
  ...
Enter fullscreen mode Exit fullscreen mode

(GitHub)

For a production deployment, I would also consider:

Read-only filesystem
Minimal Linux capabilities
Non-root user
Restricted network access
Reverse proxy
Strong credentials
Secret management
Enter fullscreen mode Exit fullscreen mode

depending on the environment.


Cronix vs Traditional Cron

It is useful to understand where Cronix fits.

Feature Traditional Cron Cronix
Cron schedules
Docker-native
Web UI
REST API
Job enable/disable Manual
Manual execution Manual
Run history Usually external
HTTP jobs Via shell Native focus
Retry support Manual
Database required
Large runtime
Arbitrary shell commands

Cron is still better when you need arbitrary local commands:

backup.sh
find ...
rsync ...
tar ...
Enter fullscreen mode Exit fullscreen mode

Cronix is better when the workload is primarily HTTP-oriented:

GET
POST
PUT
DELETE
Webhooks
Health checks
API triggers
Remote automation
Enter fullscreen mode Exit fullscreen mode

Cronix vs Heavy Schedulers

There are also much larger scheduling systems.

For example, systems such as Airflow, Kubernetes-based schedulers, or distributed task schedulers can provide:

  • DAGs
  • Distributed workers
  • Queues
  • Persistent databases
  • Complex workflows
  • Dependencies
  • Advanced retry strategies
  • Observability

But those systems solve a much larger problem.

Cronix takes the opposite approach.

Its philosophy is closer to:

I have some HTTP requests.

I want them to run on a schedule.

I want a UI.

I want retries.

I want history.

I don't want a huge platform.
Enter fullscreen mode Exit fullscreen mode

That is a very useful niche.


What Cronix Does Not Try to Do

Understanding the limitations is just as important as understanding the features.

Cronix is not intended to replace:

  • Bash
  • System cron
  • Airflow
  • Kubernetes Jobs
  • Celery
  • Full workflow engines
  • General-purpose task queues

A job cannot simply be:

rsync ...
Enter fullscreen mode Exit fullscreen mode

because Cronix expects the command to start with curl.

Likewise, this is intentionally unsupported:

curl ... | jq ...
Enter fullscreen mode Exit fullscreen mode

because there is no shell pipeline.

That limitation is part of the design rather than an accidental missing feature. (GitHub)


Migration From Older Cronix Versions

An interesting change in the project is the move away from environment-variable-defined jobs.

Older Cronix versions used variables such as:

CRON_SCHEDULE_<i>
CRON_CURL_<i>
Enter fullscreen mode Exit fullscreen mode

Jobs are now first-class objects stored in the JSON job store.

This is a significant improvement in manageability.

Instead of:

Environment
   │
   ├── CRON_SCHEDULE_1
   ├── CRON_CURL_1
   ├── CRON_SCHEDULE_2
   └── CRON_CURL_2
Enter fullscreen mode Exit fullscreen mode

the architecture is now:

Cronix
  │
  └── Job Store
       │
       ├── Job A
       ├── Job B
       └── Job C
Enter fullscreen mode Exit fullscreen mode

Jobs can then be managed consistently through the UI, CLI, or API. (GitHub)


Project Structure

The repository itself is relatively easy to understand:

cronix/
├── cmd/
│   └── cronix/
│       └── ...
│
├── internal/
│   ├── api/
│   ├── auth/
│   ├── controller/
│   ├── runner/
│   ├── store/
│   ├── spa/
│   └── ...
│
├── ui/
│   └── React + Vite application
│
├── Dockerfile
├── Makefile
├── go.mod
├── go.sum
└── README.md
Enter fullscreen mode Exit fullscreen mode

The separation between the frontend, command entrypoint, and internal application packages keeps the project relatively clean.

The UI build is ultimately embedded into the Go executable, allowing the final application to remain a single deployable unit. (GitHub)


A Minimal Production Setup

For a small self-hosted server, I would structure the deployment like this:

                         Internet
                            │
                            ▼
                    Reverse Proxy
                    HTTPS / TLS
                            │
                            ▼
                     ┌────────────┐
                     │   Cronix   │
                     │   :8080    │
                     └─────┬──────┘
                           │
                    ┌──────┴──────┐
                    │             │
                jobs.json      runs.json
                    │             │
                    └──────┬──────┘
                           │
                         /data
Enter fullscreen mode Exit fullscreen mode

Docker Compose:

services:
  cronix:
    image: cronix
    container_name: cronix
    restart: unless-stopped

    environment:
      CRONIX_API_TOKEN: ${CRONIX_API_TOKEN}
      CRONIX_USERNAME: ${CRONIX_USERNAME}
      CRONIX_PASSWORD: ${CRONIX_PASSWORD}
      TZ: Asia/Dhaka

    volumes:
      - ./data:/data

    expose:
      - "8080"
Enter fullscreen mode Exit fullscreen mode

Then let your reverse proxy expose the service.

This keeps Cronix isolated from the public network while still allowing HTTPS access through the proxy.


Things I Would Improve Further

Cronix is intentionally small, but there are several directions where it could evolve.

Notifications

A natural next feature would be notifications:

Job fails
   │
   ├── Email
   ├── Discord
   ├── Telegram
   ├── Slack
   └── Webhook
Enter fullscreen mode Exit fullscreen mode

The current architecture already records failures, so notification support could build on that.

Better Scheduling Controls

Future scheduling features could include:

  • Timezone per job
  • Start/end dates
  • Misfire policies
  • Concurrency limits
  • Maximum execution duration

More Execution Targets

The curl-only model is a strong security boundary, but some users may eventually want:

HTTP
Shell
Container
SSH
Webhook
Enter fullscreen mode Exit fullscreen mode

That would need to be designed carefully so the simplicity and security of the current model are not lost.

Metrics

Prometheus metrics would also be useful:

cronix_jobs_total
cronix_job_runs_total
cronix_job_failures_total
cronix_job_duration_seconds
Enter fullscreen mode Exit fullscreen mode

That would make Cronix easier to integrate into existing monitoring stacks.


Why Cronix Is Interesting

The most interesting thing about Cronix is not that it implements cron scheduling.

That part is relatively straightforward.

The interesting part is how aggressively it keeps the problem small.

The architecture is essentially:

Cron expression
      │
      ▼
Scheduler
      │
      ▼
curl
      │
      ▼
HTTP endpoint
      │
      ▼
Result + history
Enter fullscreen mode Exit fullscreen mode

Around that core, Cronix provides:

              ┌─────────────┐
              │   Web UI    │
              └──────┬──────┘
                     │
┌─────────┐    ┌─────▼─────┐    ┌─────────┐
│   CLI   │───►│  REST API │◄───│ Browser │
└─────────┘    └─────┬─────┘    └─────────┘
                     │
                     ▼
                Job Store
                     │
                     ▼
                 Scheduler
                     │
                     ▼
                   curl
Enter fullscreen mode Exit fullscreen mode

There is no unnecessary database.

There is no message queue.

There is no worker cluster.

There is no Node.js runtime in production.

There is no shell execution.

There is no separate frontend server.

That simplicity is the feature.


Final Thoughts

Cronix sits in an interesting space between a traditional crontab and a full-blown workflow platform.

If all you need is:

Run this HTTP request
at this time
with these retries
and let me see what happened
Enter fullscreen mode Exit fullscreen mode

then a heavyweight scheduler can feel excessive.

Traditional cron can also feel too primitive once you want a UI, API, history, and manual execution.

Cronix fills that middle ground.

It combines:

  • Standard cron expressions
  • Docker-native deployment
  • A small Go backend
  • A React web UI
  • REST API
  • CLI management
  • JSON persistence
  • Retry support
  • Run history
  • Manual execution
  • Restricted curl-based execution
  • Minimal container runtime

The result is a small, self-contained scheduler that is particularly well suited to self-hosted infrastructure, webhooks, health checks, API automation, and Docker-based homelabs.

For someone running a home server with several services, Cronix can be a nice addition to the toolbox: instead of maintaining another collection of scattered cron entries and scripts, you get one place to define, execute, inspect, and manage HTTP-based automation.

Repository: github.com/iashraful/cronix

Top comments (0)