Cronicle Job Scheduler 0.9.137 on AWS User Guide
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
adminpassword before Cronicle ever starts, so the upstream defaultadmin/adminsign 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:3012behind 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
croniclesystem 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/croniclethat 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 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.

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
cronicleuser. - 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.

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:

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

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-requiredexists. - 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_addressandudp_broadcast_portsettings before you add servers. - Email notifications: set
smtp_hostname,smtp_portandemail_fromin/opt/cronicle/conf/config.json, thensudo 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.