Cronicle Job Scheduler 0.9.137
Overview
This guide shows how to launch and use the Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. The AMI is one software version of the SecureAnyCloud Coolify and Container Platforms and PaaS listing on AWS Marketplace. Select 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, each made up of a shell script, an HTTP request or your own plugin plus a schedule. 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.
What you get:
- 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. It also generates the secret that 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 that a client can forge. Requests for a host name that 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 that is subscribed to the listing, an EC2 key pair, and a VPC subnet with a route to the internet (or a private route that you use for management).
- A security group that allows inbound TCP 22 (administrator SSH), TCP 443 (Cronicle) and TCP 80 (redirects to HTTPS) from your own networks only.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Select Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 as the software version, pick your Region, and choose Launch through EC2.
Select m5.large as the instance type, then choose your key pair and the security group described in Prerequisites. The AMI includes 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}]'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>Check the services
First boot takes a few seconds. When it finishes, the three units are active:
systemctl is-active cronicle-firstboot cronicle nginxactive active activeCronicle (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 -u0.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 nginxCheck the Cronicle version, the release it was built from, and the Node.js runtime:
node -p "require('/opt/cronicle/package.json').version" cat /usr/share/doc/cronicle/SOURCE
node --version0.9.137 Cronicle 0.9.137 source https://github.com/jhuckaby/Cronicle tag v0.9.137 commit 34fbd63058ac9f121e76527fded7211b366b996d v22.23.3Get your URL and password
Read the credentials file:
sudo cat /root/cronicle-credentials.txtThe file holds the address that Cronicle is served on (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://secureanycloud.com/stacks/cronicle-job-scheduler-0-9-137/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.
Create an event and watch a job run
Open Schedule, then Add Event.... Give the event a name, and then choose the General category, the Primary Group target (this server) and a plugin. The image includes three plugins:
- 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 useful for trying 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:
Sign in from the command line
Cronicle's API answers every sign-in request with HTTP 200, and the JSON code says whether it worked. This example reads your password from the credentials file (it never appears on a command line), signs in, and 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).
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/queueBack 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.
Security checks
Port 80 sends browsers to HTTPS, a request from another website is refused, and nginx closes the connection for a host name that 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/ || trueport 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ web interface: 200 cross-site API call: 403 unknown host name: 000Cronicle 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 IPAddressDenyIPAddressDeny=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/128You 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.
Check the remaining settings. 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 instanceThe 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 subjectAltNamesubject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = sac 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:localhostUse your own DNS name and certificate
Point a DNS record at the instance, and then set the name that Cronicle is served as. This command creates 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.exampleTo 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.
Change the admin password
Cronicle stops briefly while the password is changed, so make the change when no job is running. To generate a new random password (written to /root/cronicle-credentials.txt):
sudo cronicle-set-passwordTo 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 --stdinEither 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.
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 -cMaintenance
- 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 the servers must be able 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 SecureAnyCloud's hardening, first-boot setup and support. For help with this image, email sales@secureanycloud.com.
Deploy on AWS
Launch Cronicle Job Scheduler 0.9.137 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com