← Back to posts
comparison guide self-hosted · · 14 min read

Best Self-Hosted Cron Job Schedulers 2026: Cronicle vs Rundeck vs Autocomplete

Compare the best self-hosted cron job schedulers for 2026. Complete guide to Cronicle, Rundeck, and alternatives with Docker deployment, configuration, and real-world use cases.

OS
Editorial Team

Why Self-Host Your Cron Job Scheduler?

The traditional Unix crontab has been the backbone of scheduled task execution for decades. It is simple, reliable, and available on every Linux system. But as infrastructure grows in complexity, plain crontab reveals critical shortcomings that make it unsuitable for modern operations:

  • No centralized visibility — scattered across dozens of servers, crontab entries are invisible until something breaks. There is no single pane of glass to monitor what ran, when, and whether it succeeded.
  • Zero built-in logging — crontab writes output to local mail or stdout. You need additional tooling to capture, aggregate, and search execution logs across machines.
  • No dependency management — running Task B after Task A finishes requires fragile shell scripting. There is no native DAG (Directed Acyclic Graph) execution model.
  • No access control — anyone with shell access can edit any crontab. There is no role-based permissions system to restrict who can modify scheduled jobs.
  • No retry logic — if a job fails at 3 AM, crontab will not retry it. You either miss the execution or write custom error-handling wrappers.
  • No concurrent execution safeguards — overlapping job runs can corrupt data, exhaust resources, or trigger cascading failures. Crontab offers no built-in locking mechanism.

Self-hosted cron schedulers solve every one of these problems while keeping your infrastructure data, job definitions, and execution history entirely under your control. You avoid the vendor lock-in, rate limits, and per-node licensing fees of cloud scheduling services like AWS EventBridge, GCP Cloud Scheduler, or hosted Rundeck. This guide covers the best open-source options available in 2026.

Comparison: Cronicle vs Rundeck vs Other Options

FeatureCronicleRundeck (Community)Go-Cron (lightweight)
LicenseMITApache 2.0MIT
LanguageNode.jsJava / GroovyGo
Web UI✅ Full dashboard✅ Full dashboard❌ CLI only
Distributed✅ Multi-worker✅ Multi-node❌ Single-node
DAG dependencies✅ Chain jobs✅ Job orchestration❌ Sequential only
RBAC / ACL✅ User roles✅ Fine-grained ACLs❌ None
API✅ REST✅ REST +docker ❌ CLI only
Docker support✅ Official images✅ Official images✅ Community images
DatabaseSQLite / MySQL / PostgreSQLH2 / MySQL / PostgreSQLSQLite / PostgreSQL
Resource usage~200 MB RAM~800 MB RAM~30 MB RAM
Best forTeams needing distributed schedulingEnterprise with strict compliance needsMinimal single-server setups

When to Choose Each Tool

Cronicle is the best all-around choice for homelab operators and small-to-medium teams. It is lightweight, has a clean web interface, supports distributed workers across multiple servers, and handles job chaining natively. If you want something that “just works” without a Java dependency, this is it.

Rundeck is the enterprise heavyweight. It has the most mature RBAC system, audit logging for compliance frameworks (SOC 2, HIPAA), and deep integration with LDAP/Active Directory. The trade-off is higher resource consumption and a steeper learning curve for Groovy-based job definitions.

Go-Cron (and similar lightweight tools) serve a narrow niche: a single-server setup where you want better visibility and logging than crontab but do not need a web UI, distributed execution, or team collaboration. Think of it as “crontab with a database and a REST API.”

Deployment Guide: Cronicle

Cronicle is built by Joseph Huckaby (the creator of the classic logrotate alternative logrotate-ng). It runs on Node.js and provides a full-featured web UI with real-time job monitoring, multi-worker support, and a comprehensive REST API.

Prerequisites

  • Docker and Docker Compose installed
  • At least 512 MB RAM available
  • A persistent volume for job data and logs

Docker Compose Setup

Create a docker-compose.yml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: "3.8"

services:
  cronicle:
    image: ghcr.io/jhuckaby/cronicle:latest
    container_name: cronicle
    restart: unless-stopped
    ports:
      - "3012:3012"
    environment:
      - CRONICLE_secret_key=change-me-to-a-random-string
      - CRONICLE_web_port=3012
      - CRONICLE_storage_backend=Filesystem
      - CRONICLE_storage_File_base_dir=/opt/cronicle/data
      - CRONICLE_log_dir=/opt/cronicle/logs
    volumes:
      - cronicle_data:/opt/cronicle/data
      - cronicle_logs:/opt/cronicle/logs
    networks:
      - cronicle_net

networks:
  cronicle_net:
    driver: bridge

volumes:
  cronicle_data:
  cronicle_logs:

Start the service:

1
docker compose up -d

Initial Configuration

Access the web UI at http://your-server:3012. The default credentials are:

  • Username: admin
  • Password: admin

Immediately change the default password via Settings > User Management.

Adding a Worker Node

To add a second server as a Cronicle worker, install the same Docker image and point it to the master’s API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
services:
  cronicle-worker:
    image: ghcr.io/jhuckaby/cronicle:latest
    container_name: cronicle-worker
    restart: unless-stopped
    environment:
      - CRONICLE_secret_key=change-me-to-a-random-string
      - CRONICLE_server=master-server
      - CRONICLE_server_port=3012
      - CRONICLE_worker=true
      - CRONICLE_web_port=0
    volumes:
      - worker_data:/opt/cronicle/data
    networks:
      - cronicle_net

The master automatically discovers workers and distributes jobs based on their availability.

Creating Your First Scheduled Job

  1. Navigate to Schedules > Create New Schedule
  2. Set the cron expression (e.g., 0 2 * * * for daily at 2 AM)
  3. Choose Shell Script as the plugin
  4. Enter your command:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash
# Database backup script
BACKUP_DIR="/opt/backups/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

pg_dump -U postgres mydb | gzip > "$BACKUP_DIR/mydb.sql.gz"

# Retain only the last 7 days
find /opt/backups -type d -mtime +7 -exec rm -rf {} \;

echo "Backup completed: $(ls -lh "$BACKUP_DIR/mydb.sql.gz")"
  1. Set Timeout to 3600 seconds (1 hour)
  2. Enable Catch Up if the job should run even if a previous instance was missed
  3. Save and enable the schedule

Job Chaining (DAG Execution)

Cronicle supports chaining jobs so that Job B runs only after Job A completes successfully:

  1. Create Job A (e.g., “Download data from API”)
  2. Create Job B (e.g., “Process downloaded data”)
  3. In Job B’s settings, add Job A as a dependency
  4. Optionally create Job C (e.g., “Send notification”) depending on Job B

This creates an execution pipeline where each step waits for the previous one to succeed. If any step fails, the chain stops and you receive an alert.

Using PostgreSQL Instead of SQLite

For production workloads with hundreds of jobs, switch from the default SQLite backend to PostgreSQL:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
services:
  cronicle:
    image: ghcr.io/jhuckaby/cronicle:latest
    environment:
      - CRONICLE_secret_key=change-me-to-a-random-string
      - CRONICLE_storage_backend=SQL
      - CRONICLE_storage_SQL_type=postgresql
      - CRONICLE_storage_SQL_host=postgres
      - CRONICLE_storage_SQL_port=5432
      - CRONICLE_storage_SQL_database=cronicle
      - CRONICLE_storage_SQL_user=cronicle
      - CRONICLE_storage_SQL_password=strong-password-here
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: cronicle
      POSTGRES_USER: cronicle
      POSTGRES_PASSWORD: strong-password-here
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  pg_data:

Deployment Guide: Rundeck

Rundeck is the most feature-complete open-source job scheduler. It is used by enterprises worldwide for infrastructure automation, deployment orchestration, and scheduled maintenance windows.

Docker Compose Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: "3.8"

services:
  rundeck:
    image: rundeck/rundeck:5.6.0
    container_name: rundeck
    restart: unless-stopped
    ports:
      - "4440:4440"
    environment:
      - RUNDECK_GRAILS_URL=http://your-server:4440
      - RUNDECK_SERVER_ADDRESS=0.0.0.0
      - RUNDECK_LOGGING_AUDIT_ENABLED=true
      - RUNDECK_DATABASE_DRIVER=org.h2.Driver
      - RUNDECK_DATABASE_URL=jdbc:h2:file:/home/rundeck/server/data/grailsdb;MVCC=true
      - RUNDECK_PASSWORD=admin
    volumes:
      - rundeck_data:/home/rundeck/server/data
      - rundeck_logs:/home/rundeck/server/logs
      - rundeck_projects:/home/rundeck/projects

volumes:
  rundeck_data:
  rundeck_logs:
  rundeck_projects:

Start Rundeck:

1
docker compose up -d

Wait approximately 60 seconds for the JVM to initialize, then access http://your-server:4440. Log in with admin/admin (or whatever password you set via RUNDECK_PASSWORD).

Defining a Job in YAML

Rundeck stores job definitions as YAML files in the project’s jobs/ directory. Here is a backup job definition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
- name: Daily Database Backup
  group: maintenance
  description: PostgreSQL backup with retention policy
  schedule:
    month: '*'
    weekday: '*'
    hour: '2'
    minute: '0'
  executionEnabled: true
  loglevel: INFO
  timeout: '3600'
  retry: '2'
  retryDelay: '300'
  nodeFilterEditable: false
  nodefilter:
    filter: 'name: db-server'
  sequence:
    keepgoing: false
    strategy: node-first
    commands:
      - exec: |
          #!/bin/bash
          set -euo pipefail
          BACKUP_DIR="/var/backups/postgres/$(date +%Y-%m-%d)"
          mkdir -p "$BACKUP_DIR"

          echo "Starting backup at $(date)"
          pg_dump -h localhost -U postgres --format=custom mydb > "$BACKUP_DIR/mydb.dump"
          echo "Backup size: $(du -h "$BACKUP_DIR/mydb.dump" | cut -f1)"

          # Compress and verify
          gzip "$BACKUP_DIR/mydb.dump"
          gzip -t "$BACKUP_DIR/mydb.dump.gz"

          # Cleanup old backups
          find /var/backups/postgres -name "*.dump.gz" -mtime +14 -delete
          echo "Backup completed successfully at $(date)"
      - exec: |
          # Send notification on failure (only reached if retry exhausted)
          curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
            -d '{"text": "❌ Daily backup FAILED on db-server"}'

Setting Up Access Control (ACL)

Rundeck’s ACL policy files control who can do what. Create a file at /home/rundeck/projects/your-project/etc/acl-policy.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
description: Developer team access
context:
  project: 'production'
for:
  resource:
    - allow: ['read']
  adhoc:
    - allow: ['read', 'run']
  job:
    - allow: ['read', 'run', 'view']
      match:
        group: 'maintenance/.*'
  node:
    - allow: ['read']
by:
  group: 'developers'

---

description: Admin full access
context:
  project: '.*'
for:
  resource:
    - allow: ['*']
  job:
    - allow: ['*']
  node:
    - allow: ['*']
  adhoc:
    - allow: ['*']
by:
  group: 'admins'

Integrating with LDAP / Active Directory

Rundeck supports LDAP authentication out of the box. Configure it via environment variables:

1
2
3
4
5
6
7
8
environment:
  - RUNDECK_SECURITY_AUTHENTICATION_PROVIDER=ldap
  - RUNDECK_LDAP_PROVIDER_URL=ldap://ldap-server:389
  - RUNDECK_LDAP_PROVIDER_DN=cn=admin,dc=company,dc=com
  - RUNDECK_LDAP_PROVIDER_PASSWORD=ldap-password
  - RUNDECK_LDAP_USER_BASE_DN=ou=users,dc=company,dc=com
  - RUNDECK_LDAP_USER_SEARCH_FILTER=uid={0}
  - RUNDECK_LDAP_GROUP_BASE_DN=ou=groups,dc=company,dc=com

Reverse Proxy Configuration (Nginx)

Both Cronicle and Rundeck should sit behind a reverse proxy with TLS termination. Here is a production-ready Nginx configuration for Cronicle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
server {
    listen 80;
    server_name cronicle.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name cronicle.example.com;

    ssl_certificate /etc/letsencrypt/live/cronicle.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cronicle.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://127.0.0.1:3012;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    # WebSocket support for real-time job monitoring
    location /api/ {
        proxy_pass http://127.0.0.1:3012;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Obtain a free TLS certificate with Certbot:

1
sudo certbot --nginx -d cronicle.example.com

Real-World Use Cases

Use Case 1: Automated SSL Certificate Renewal Check

Monitor all your domains and get notified before certificates expire:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
# SSL certificate expiry checker
DOMAINS=("example.com" "api.example.com" "mail.example.com")
THRESHOLD_DAYS=14

for domain in "${DOMAINS[@]}"; do
    expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

    if [ -z "$expiry" ]; then
        echo "ERROR: Cannot check $domain"
        continue
    fi

    expiry_epoch=$(date -d "$expiry" +%s)
    current_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - current_epoch) / 86400 ))

    if [ "$days_left" -lt "$THRESHOLD_DAYS" ]; then
        echo "WARNING: $domain expires in $days_left days ($expiry)"
    fi
done

Schedule this to run daily at 9 AM via Cronicle with a webhook notification to your team chat.

Use Case 2: Log Rotation and Cleanup

Automate log management across multiple servers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash
# Find and compress logs older than 1 day, delete compressed logs older than 30 days
LOG_DIRS=("/var/log/app" "/var/log/nginx" "/opt/backups/logs")

for dir in "${LOG_DIRS[@]}"; do
    if [ ! -d "$dir" ]; then continue; fi

    # Compress uncompressed logs older than 1 day
    find "$dir" -name "*.log" -mtime +1 -exec gzip {} \;

    # Delete compressed logs older than 30 days
    find "$dir" -name "*.gz" -mtime +30 -delete

    # Report disk usage
    usage=$(du -sh "$dir" 2>/dev/null | cut -f1)
    echo "$dir: $usage after cleanup"
done

Use Case 3: Database Health Checks

Run periodic health checks on your databases:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
# PostgreSQL health check
MAX_CONNECTIONS=$(psql -t -c "SHOW max_connections" | tr -d ' ')
ACTIVE_CONNECTIONS=$(psql -t -c "SELECT count(*) FROM pg_stat_activity" | tr -d ' ')
DB_SIZE=$(psql -t -c "SELECT pg_size_pretty(pg_database_size('mydb'))" | tr -d ' ')
UPTIME=$(psql -t -c "SELECT date_trunc('second', current_timestamp - pg_postmaster_start_time())" | tr -d ' ')

echo "=== PostgreSQL Health Report ==="
echo "Connections: $ACTIVE_CONNECTIONS / $MAX_CONNECTIONS"
echo "Database size: $DB_SIZE"
echo "Uptime: $UPTIME"

# Alert if over 80% connection usage
USAGE_PCT=$(( ACTIVE_CONNECTIONS * 100 / MAX_CONNECTIONS ))
if [ "$USAGE_PCT" -gt 80 ]; then
    echo "CRITICAL: Connection usage at ${USAGE_PCT}%"
    exit 1
fi

# Alert if database grew more than 10% since last check
LAST_SIZE_FILE="/tmp/db_size_last_check"
if [ -f "$LAST_SIZE_FILE" ]; then
    LAST_SIZE=$(cat "$LAST_SIZE_FILE")
    CURRENT_SIZE=$(psql -t -c "SELECT pg_database_size('mydb')" | tr -d ' ')
    GROWTH=$(( (CURRENT_SIZE - LAST_SIZE) * 100 / LAST_SIZE ))
    if [ "$GROWTH" -gt 10 ]; then
        echo "WARNING: Database grew ${GROWTH}% since last check"
    fi
fi
echo "$CURRENT_SIZE" > "$LAST_SIZE_FILE"

Monitoring and Alerting Integration

Webhook Notifications

Both Cronicle and Rundeck support webhook notifications. Configure a webhook to send job status updates to your preferred messaging platform:

1
2
3
4
5
6
7
8
9
# Cronicle webhook payload format
{
    "event": "job_complete",
    "job_id": "{{job.id}}",
    "job_name": "{{job.name}}",
    "status": "{{event.code}}",
    "duration": "{{event.duration}}",
    "output": "{{event.output}}"
}

Gotify / Pushover Integration

For self-hosted push notifications, integrate with Gotify:

1
2
3
4
5
6
#!/bin/bash
# Send notification via Gotify
MESSAGE="Job '$JOB_NAME' completed with status: $STATUS"
curl -s -X POST "https://gotify.example.com/message?token=APP_TOKEN" \
    -H "Content-Type: application/json" \
    -d "{\"title\": \"Cronicle Alert\", \"m[prometheus](https://prometheus.io/)\"$MESSAGE\", \"priority\": 5}"

Prometheus Metrics Export

Expose job execution metrics to Prometheus for dashboarding:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# cronicle_metrics_exporter.sh
# Run as a cron job every minute

SCHEDULER_URL="http://localhost:3012"
API_KEY="your-api-key"

# Fetch active jobs and export metrics
ACTIVE=$(curl -s "$SCHEDULER_URL/api/app/tick" \
    -H "X-Cronicle-Api-Key: $API_KEY" \
    | jq '.active_jobs | length')

echo "cronicle_active_jobs $ACTIVE" > /var/lib/node_exporter/cronicle.prom

# Fetch recent job completions
COMPLETED=$(curl -s "$SCHEDULER_URL/api/app/log/events" \
    -H "X-Cronicle-Api-Key: $API_KEY" \
    -d '{"offset":0,"limit":100}' \
    | jq '[.events[] | select(.event == "job_complete")] | length')

echo "cronicle_jobs_completed_last_hour $COMPLETED" >> /var/lib/node_exporter/cronicle.prom

Migration from Crontab

If you have existing crontab entries to migrate, use this script to convert them into a structured format:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
# Export all user crontabs to a readable CSV
echo "User,Schedule,Command,Last Modified" > cron_migration.csv

for user in $(cut -d: -f1 /etc/passwd); do
    crontab -u "$user" -l 2>/dev/null | grep -v '^#' | grep -v '^$' | while read -r line; do
        schedule=$(echo "$line" | awk '{print $1, $2, $3, $4, $5}')
        command=$(echo "$line" | awk '{for(i=6;i<=NF;i++) printf "%s ", $i; print ""}')
        last_mod=$(stat -c %Y /var/spool/cron/crontabs/$user 2>/dev/null || echo "unknown")
        echo "$user,$schedule,$command,$last_mod" >> cron_migration.csv
    done
done

echo "Migration inventory saved to cron_migration.csv"
echo "Review this file, then recreate jobs in your chosen scheduler."

This gives you a complete inventory of every scheduled task across your servers — often revealing forgotten jobs, duplicate entries, and opportunities for consolidation.

Choosing the Right Scheduler

Your decision should be guided by three factors:

Team size and collaboration needs. If you are a solo homelab operator, Cronicle or even a lightweight Go-based scheduler may be sufficient. If you have a team of 5+ engineers who need role-based access, audit trails, and approval workflows for production jobs, Rundeck’s maturity justifies its resource overhead.

Infrastructure scale. For single-server setups, any tool works. Once you need to schedule jobs across 3+ servers with load balancing and failover, Cronicle’s built-in multi-worker architecture or Rundeck’s node-based execution model become essential.

Compliance requirements. If you operate in a regulated environment (finance, healthcare, government), Rundeck’s audit logging, LDAP integration, and granular ACL system will save you significant effort during compliance audits. Cronicle covers the basics but lacks the depth required for formal compliance frameworks.

For most homelab operators and small teams starting out, Cronicle offers the best balance of features, simplicity, and resource efficiency. Deploy it via Docker, configure your backup and maintenance jobs, and you will have full visibility into every scheduled task across your infrastructure — something that plain crontab simply cannot provide.

Frequently Asked Questions (FAQ)

Which one should I choose in 2026?

The best choice depends on your specific requirements:

  • For beginners: Start with the simplest option that covers your core use case
  • For production: Choose the solution with the most active community and documentation
  • For teams: Look for collaboration features and user management
  • For privacy: Prefer fully open-source, self-hosted options with no telemetry

Refer to the comparison table above for detailed feature breakdowns.

Can I migrate between these tools?

Most tools support data import/export. Always:

  1. Backup your current data
  2. Test the migration on a staging environment
  3. Check official migration guides in the documentation

Are there free versions available?

All tools in this guide offer free, open-source editions. Some also provide paid plans with additional features, priority support, or managed hosting.

How do I get started?

  1. Review the comparison table to identify your requirements
  2. Visit the official documentation (links provided above)
  3. Start with a Docker Compose setup for easy testing
  4. Join the community forums for troubleshooting
Advertise here