Developer Tools AWS

Cronicle Job Scheduler 0.9.137 on AWS User Guide

| Product: Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Coolify and Container Platforms and PaaS listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Cronicle is a job scheduler and cron replacement with a web interface. You define events (a shell script, an HTTP request or your own plugin, with a schedule), and Cronicle runs them, retries and chains them, enforces timeouts and resource limits, keeps the history, and shows each running job's progress, CPU, memory and log live in the browser. Several people can share it, each with their own account and privileges. This image runs the official Cronicle 0.9.137 release with its built in storage, so there is no separate database to run.

The cloudimg image adds:

  • HTTPS from the first boot. nginx serves Cronicle on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • An admin password and session secret unique to your instance. First boot creates Cronicle's storage and sets the admin password before Cronicle ever starts, so the upstream default admin / admin sign in is never live, and it generates the secret Cronicle signs sessions with. Nothing secret is baked into the image.
  • Fail closed. Cronicle refuses to start until first boot has finished, and so does nginx.
  • Cronicle on loopback only. Cronicle listens on 127.0.0.1:3012 behind nginx, including the WebSocket that streams live job logs. Cross site requests and cross site WebSocket connections are refused. The open ports are 22, 80 and 443.
  • Jobs run as an unprivileged user. Cronicle and every job it starts run as the cronicle system user (upstream runs them as root).
  • Protection against server side request forgery. Cronicle's HTTP Request plugin fetches any URL you give it, so Cronicle, every job it runs and nginx are blocked from the instance metadata service at the kernel level. The AMI requires IMDSv2.
  • Your schedule on its own volume. Cronicle's storage (schedule, users, job history and logs) lives on a separate 20 GiB gp3 volume at /var/lib/cronicle that you can resize or snapshot on its own.
  • Hardened defaults. Cronicle's address comes from your configuration, never from a request; client addresses come from the connection, not from headers a client can forge; requests for a host name the instance does not serve are dropped.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Cronicle 0.9.137 (built from the release tag with its locked dependencies), Node.js 22 LTS, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (administrator SSH), TCP 443 (Cronicle) and TCP 80 (redirects to HTTPS) from your own networks only.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above. The AMI brings a second 20 GiB volume for Cronicle's data; you can make it larger on the launch page.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=cronicle}]'

Step 2: Connect to your instance

Version SSH user
Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot takes a few seconds. Then the three units are active:

systemctl is-active cronicle-firstboot cronicle nginx
active
active
active

Cronicle (its process is called Cronicle Server) listens on loopback only, and nginx serves ports 80 and 443:

sudo ss -tlnpH | sed -E 's/^LISTEN +[0-9]+ +[0-9]+ +([^ ]+) .*users:\(\("([^"]+)".*/\1  \2/' | sort -u
0.0.0.0:22  sshd
0.0.0.0:443  nginx
0.0.0.0:80  nginx
127.0.0.1:3012  Cronicle Server
127.0.0.53%lo:53  systemd-resolve
127.0.0.54:53  systemd-resolve
[::]:22  sshd
[::]:443  nginx
[::]:80  nginx

The version on the image, where it was built from, and the Node.js runtime:

node -p "require('/opt/cronicle/package.json').version"
cat /usr/share/doc/cronicle/SOURCE
node --version
0.9.137
Cronicle 0.9.137
source https://github.com/jhuckaby/Cronicle tag v0.9.137 commit 34fbd63058ac9f121e76527fded7211b366b996d
v22.23.3

Step 4: Get your URL and password

sudo cat /root/cronicle-credentials.txt

The file holds the address Cronicle is served as (your instance's public DNS name by default), the user name and the password generated on this instance:

# Cronicle 0.9.137 - generated on first boot by cronicle-firstboot.service
# This password is unique to this instance; none was baked into the image. Keep this file secret.

CRONICLE_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
CRONICLE_USERNAME=admin
CRONICLE_PASSWORD=<CRONICLE_PASSWORD>
#
# Open CRONICLE_URL in a browser (self-signed certificate minted on this instance's first boot; port 80
# redirects to https) and sign in with the user and password above.
# Jobs run as the unprivileged 'cronicle' user. Schedule, users and job history: /var/lib/cronicle
# Change the password: sudo cronicle-set-password   Change the served name: sudo cronicle-set-hostname <name>
# User guide: https://www.cloudimg.co.uk/guides/cronicle-job-scheduler-0-9-137-on-ubuntu-24-04-aws/

Step 5: Sign in from your browser

Open CRONICLE_URL in your browser. The certificate is self signed (generated on your instance), so accept the browser warning once, or use your own name and certificate (Step 10). Sign in with admin and the password from Step 4.

The Cronicle User Login form with the admin user name and a masked password filled in

The Home tab shows server statistics, the jobs running now and the upcoming scheduled runs. The clock and status at the top right update live over a WebSocket.

The Cronicle Home tab with server statistics, one active Hello World job with its progress bar, and the upcoming hourly runs

Step 6: Create an event and watch a job run

Open Schedule, then Add Event.... Give the event a name, choose the General category, the Primary Group target (this server) and a plugin. The image ships three:

  • Shell Script runs the script you type, as the cronicle user.
  • HTTP Request sends a GET, HEAD or POST request to a URL and records the response.
  • Test Plugin simulates a job, which is handy to try things out.

Set the timing (or On Demand), then Create Event.

The Add New Event form: Hello World in the General category on the Primary Group, using the Test Plugin with a 45 second duration, run hourly

On the Schedule tab choose Run next to the event to start it now. Open the job from Home to see its progress, CPU and memory, and its log as it is written:

Live Job Progress for the Hello World job: progress, CPU and memory charts, and the live job event log streaming in

Finished jobs, with their result, duration and full log, are on the Completed tab:

The Completed tab listing the Hello World job with a Success result after 45 seconds

Step 7: Sign in from the command line

Cronicle's API answers every sign in with HTTP 200; the JSON code says whether it worked. This reads your password from the credentials file (it never appears on a command line) and signs in, then shows that the upstream default password is refused:

H=$(sudo sed -n 's#^CRONICLE_URL=https://\([^/]*\)/.*#\1#p' /root/cronicle-credentials.txt)
login() {
  python3 -c 'import json,sys; print(json.dumps({"username": "admin", "password": sys.stdin.read().strip()}))' \
  | curl -sk --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -H "Origin: https://$H" \
      --data-binary @- "https://$H/api/user/login" \
  | python3 -c 'import json,sys; d=json.load(sys.stdin); print("signed in" if d.get("code") == 0 else "refused: " + d.get("description", ""))'
}
echo "this instance's password: $(sudo sed -n 's/^CRONICLE_PASSWORD=//p' /root/cronicle-credentials.txt | login)"
echo "upstream default 'admin': $(echo admin | login)"
this instance's password: signed in
upstream default 'admin': refused: Username or password incorrect.

Cronicle locks an account at the sixth failed sign in within an hour, and unlocking it normally needs a password reset email. To unlock admin (or to change its password) on this instance, use cronicle-set-password (Step 11).

Step 8: Where your data lives

Cronicle's storage (schedule, users, sessions, job history and job logs), its own logs and the job queue are on the separate data volume, linked from the install directory:

findmnt -no TARGET,SOURCE,FSTYPE,SIZE /var/lib/cronicle
sudo ls -l /opt/cronicle | awk '$1 ~ /^l/ {print $9, $10, $11}'
/var/lib/cronicle /dev/nvme1n1 ext4   19.5G
data -> /var/lib/cronicle/data
logs -> /var/lib/cronicle/logs
queue -> /var/lib/cronicle/queue

Back up Cronicle by snapshotting this volume (stop Cronicle first for a consistent copy: sudo systemctl stop cronicle, snapshot, sudo systemctl start cronicle). Settings are in /opt/cronicle/conf/config.json.

Step 9: Security checks

Port 80 sends browsers to HTTPS, a request from another web site is refused, and nginx closes the connection for a host name the instance does not serve (000 means no answer):

H=$(sudo sed -n 's#^CRONICLE_URL=https://\([^/]*\)/.*#\1#p' /root/cronicle-credentials.txt)
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'web interface: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'cross-site API call: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Origin: https://example.com' -H 'Content-Type: application/json' -d '{}' "https://$H/api/user/login"
curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
web interface: 200
cross-site API call: 403
unknown host name: 000

Cronicle runs as the unprivileged cronicle user, and neither Cronicle (with every job it starts) nor nginx can reach the instance metadata service:

systemctl show cronicle -p User -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
User=cronicle
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128

You can see the block from Cronicle itself: an HTTP Request event for http://169.254.169.254/latest/meta-data/ fails with Connect Timeout, while the same event for a public site such as https://example.com/ succeeds. A Shell Script job that tries curl http://169.254.169.254/ fails the same way.

The settings behind the rest: Cronicle listens on loopback, builds its links from the configured address, has no network discovery listener, and signs sessions with a secret generated on this instance:

sudo python3 -c 'import json; c=json.load(open("/opt/cronicle/conf/config.json")); w=c["WebServer"]; print("bind_address:", w["bind_address"]); print("base_app_url:", c["base_app_url"]); print("udp_broadcast_port:", c["udp_broadcast_port"]); print("secret_key: %d hex characters, generated on this instance" % len(c["secret_key"]))'
bind_address: 127.0.0.1
base_app_url: https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com
udp_broadcast_port: 0
secret_key: 64 hex characters, generated on this instance

The certificate was generated on this instance and names its own addresses:

H=$(sudo sed -n 's#^CRONICLE_URL=https://\([^/]*\)/.*#\1#p' /root/cronicle-credentials.txt)
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name: 
    DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

Step 10: Your own DNS name and certificate

Point a DNS record at the instance, then set the name Cronicle is served as. This mints a new self signed certificate for the name, updates nginx (and which sites may call the API), Cronicle's base_app_url (the links in notification emails and web hooks) and the URL in /root/cronicle-credentials.txt, restarts Cronicle, and keeps the name across reboots:

sudo cronicle-set-hostname cronicle.your-domain.example

To use a certificate from a public authority instead, replace /etc/nginx/ssl/cronicle.crt and /etc/nginx/ssl/cronicle.key (keep the key mode 0600) and run sudo systemctl reload nginx. Running cronicle-set-hostname again replaces them with a new self signed pair.

Step 11: Change the admin password

Cronicle is stopped for a moment while the password is changed, so do it when no job is running. To generate a new random password (written to /root/cronicle-credentials.txt):

sudo cronicle-set-password

To choose your own (16 to 128 letters and digits), type it on standard input so it never appears on a command line:

sudo cronicle-set-password --stdin

Either way the account is unlocked if Cronicle had locked it after failed sign ins. Signed in as admin, you can also change the password under My Account.

Step 12: Users, privileges and jobs that need root

Add teammates under Admin, Users. Each account can be limited to running, creating, editing or deleting events, aborting jobs or toggling the scheduler, and to particular categories and server groups. Event parameters are checked against the plugin's own parameters, so a user who may create or run events cannot inject environment variables into a job (fixed in Cronicle 0.9.124).

Jobs run as the cronicle user. When a job needs root for a specific command, allow exactly that command with a sudoers rule rather than running Cronicle as root, for example:

echo 'cronicle ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service' | sudo tee /etc/sudoers.d/cronicle-myapp
sudo chmod 0440 /etc/sudoers.d/cronicle-myapp
sudo visudo -c

Step 13: Maintenance

  • Operating system updates: unattended upgrades install Ubuntu security updates automatically. Reboot when /var/run/reboot-required exists.
  • Services: sudo systemctl status cronicle nginx; Cronicle's own logs are in /var/lib/cronicle/logs.
  • More servers: this image is a single Cronicle server (the master and the only worker). Cronicle can spread jobs over several servers, but that needs them to reach each other on Cronicle's port, which this image keeps on loopback. Plan the network and Cronicle's server_comm_use_hostnames, bind_address and udp_broadcast_port settings before you add servers.
  • Email notifications: set smtp_hostname, smtp_port and email_from in /opt/cronicle/conf/config.json, then sudo systemctl restart cronicle.

Support and licensing

Cronicle is open source under the MIT licence (the licence text is at /usr/share/doc/cronicle/LICENSE.md); there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support. For help with this image, email support@cloudimg.co.uk.