Ontime 4.15.1 on AWS User Guide
Overview
This guide covers launching and using the Ontime 4.15.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg media servers listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Ontime is an open source rundown manager and show timer for live events, broadcast and theatre. You build the running order of a show in the browser editor, and Ontime keeps every screen in the venue on the same clock: a large stage timer for presenters, a cuesheet for the show caller, a backstage view for the crew, a studio clock and public schedule views. Companion, vMix, OBS, QLab and scripts can drive it over its HTTP and WebSocket API.
What cloudimg adds:
- Built from the upstream 4.15.1 source. Ontime is built by cloudimg from the checksum verified v4.15.1 release tag (commit
b5580040) with upstream's own build. The upstream code is unmodified; the one change is a dependency raise that lifts the vulnerable versions of twelve packages the 4.15.1 lockfile resolves (among them body-parser, qs, path-to-regexp, ws and axios) to their fixed releases, inside upstream's own version ranges. The exact diff and the raised lockfile ship on the image. - A per-instance password, created before any port opens. First boot generates the Ontime password for your instance and writes it, with the API token derived from it, to
/root/ontime-credentials.txt. Ontime and nginx refuse to start until that has happened. Everything except the login page and the health checks needs the password: the editor, every display view, the data and integration APIs and the WebSocket. - HTTPS only. nginx serves Ontime on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS and answers
/healthzfor load balancers. The session cookie isSecureandSameSite=Strict, a login can only redirect to a page on your own server, and a request another web site makes in your browser can never act on your session: Ontime's control API changes the show on a plain GET, so nginx refuses any cross-site request that carries the session cookie. - Ontime's own port is closed to the network. Ontime listens on port 4001 on every address; a firewall rule drops that port on every interface except loopback, so the only way in is the HTTPS front. OSC input is off until you list the addresses allowed to send it.
- A sandboxed service that cannot reach inside your network. Ontime runs as its own unprivileged account in a strict systemd sandbox. A firewall table loaded at every boot stops that account from opening connections to the EC2 instance metadata service, to private and reserved address ranges, and to your VPC's own IPv4 and IPv6 ranges, so an HTTP automation URL cannot be used to reach your internal services. If the instance cannot read its VPC ranges from instance metadata at boot, Ontime stays stopped and the firewall service retries until it can.
- Fully patched Ubuntu 24.04 LTS at build time, IMDSv2 required, with unattended security upgrades switched on.
What is on the image: Ontime 4.15.1 in /opt/ontime/app with its own Node.js 24.22.2 runtime in /opt/ontime/node (no npm or other package manager), 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.
- A security group that allows:
- TCP 22 (SSH) from your own networks only
- TCP 443 (Ontime over HTTPS) from the networks of your operators and of every screen that shows a view
- TCP 80 (redirect to HTTPS and
/healthz) from the same sources as 443
Ontime is light: an m5.large drives many display screens. Every screen needs to reach port 443, so for a venue network the usual set up is a security group that admits 443 from the venue's public address.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Ontime 4.15.1 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.
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=ontime}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Ontime 4.15.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot generates the certificate and the password, then starts the firewall service, Ontime and nginx:
systemctl is-active ontime-egress-guard ontime nginx
sudo test -f /var/lib/cloudimg/ontime-firstboot.done && echo FIRSTBOOT_DONE
curl -s --cacert /etc/ssl/ontime/ontime.crt --resolve "$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt):443:127.0.0.1" "https://$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt)/healthz"
All three units are active, the first boot marker is present and the HTTPS front answers its health check:
active
active
active
FIRSTBOOT_DONE
ok
nginx serves Ontime on 80 and 443. Ontime itself listens on 4001 (its process shows as MainThread), and the firewall drops that port on every interface except loopback:
sudo ss -lntpH | awk '{split($6, a, "\""); print $4, a[2]}' | sort -u
0.0.0.0:22 sshd
0.0.0.0:4001 MainThread
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.53%lo:53 systemd-resolve
127.0.0.54:53 systemd-resolve
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx
Step 4: Get your login details
sudo grep -E '^ONTIME_(HOST|URL)=' /root/ontime-credentials.txt
sudo stat -c '%a %U %n' /root/ontime-credentials.txt
ONTIME_HOST=ec2-203-0-113-10.compute-1.amazonaws.com
ONTIME_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/
600 root /root/ontime-credentials.txt
ONTIME_HOST is the instance's public DNS name from EC2 instance metadata; the certificate also names the public and private IP addresses. Open Ontime by that name: a request for any other name (including the bare IP address) is refused. To use your own DNS name, see Step 9.
The full file, which also holds ONTIME_PASSWORD and the ONTIME_API_TOKEN derived from it, is readable by root only. Show it when you need the values:
sudo cat /root/ontime-credentials.txt
Keep the password and the token secret: either one controls your show.
Step 5: Log in and open the editor
Browse to the ONTIME_URL from Step 4 and accept the self signed certificate warning (Step 9 shows how to use your own name and certificate). Ontime shows its login page; enter your ONTIME_PASSWORD.

You land on the editor. Ontime creates a demo project on its first start, so the rundown already holds a few example events; replace them with your own, or create a new project from the menu:

Use the playback controls to start the first event. Your browser keeps the session for later visits. Open Ontime by typing its address or from a bookmark: a link to it followed from another web site arrives as a cross-site request and is refused (see Step 11).
Step 6: The display views
Every view counts to the same clock. Open one on each screen you want to drive, logged in with the same password. The timer view at https://<host>/timer shows the presenter a large countdown with the current and next events:

The cuesheet at https://<host>/cuesheet gives the show caller the full rundown with start, end and duration for every cue:

Other views include /backstage, /studio, /timeline, /countdown and /info. All of them receive their updates live over the WebSocket at wss://<host>/ws, which nginx forwards for you.
For a screen where nobody can type a password, Ontime's share link feature creates a view address that carries a token (?token=); anyone holding that link can open the view, so share it only with the screens that need it.
Step 7: Self test and the API
The image includes a self test that checks the whole front from the instance itself: the health checks, that anonymous requests to the API, the data API and the WebSocket are refused, that a wrong password is refused, that a login gives a Secure and SameSite=Strict cookie and can only redirect to this server, that cross-origin and cross-site requests are refused, and that the session and the token can use the API. When no show is running it also starts the first event, checks that the timer advances, and stops it again:
sudo ontime-selftest
The last line sums up every check:
OK Ontime self-test passed: HTTPS front, anonymous API/data 401, view -> /login, anonymous WebSocket closed 1008, wrong password 401, login 302 with a Secure SameSite=Strict cookie and a host-confined redirect, session + token API 202, cross-origin POST, cross-site API GET and WebSocket 403, session WebSocket streams; timer ran (3598972 -> 3596000 ms) and was stopped
Integrations send the API token as a Bearer header. This example reads the token into a shell variable without printing it, asks Ontime for its version and reads the timer state:
H=$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt)
TOK=$(sudo sed -n 's/^ONTIME_API_TOKEN=//p' /root/ontime-credentials.txt)
curl -s --cacert /etc/ssl/ontime/ontime.crt --resolve "$H:443:127.0.0.1" -H "Authorization: Bearer $TOK" "https://$H/api/version"; echo
curl -s --cacert /etc/ssl/ontime/ontime.crt --resolve "$H:443:127.0.0.1" -H "Authorization: Bearer $TOK" "https://$H/api/poll" | python3 -c 'import json, sys; t = json.load(sys.stdin)["payload"]["timer"]; print("playback:", t["playback"])'
unset TOK
{"payload":"4.15.1"}
playback: stop
The control endpoints work the same way, for example /api/start, /api/pause, /api/stop, /api/roll and /api/load/index/2. The Ontime documentation describes the full API and the Companion module.
Step 8: HTTP automations and OSC
Ontime's automations can call a URL or send OSC when an event starts or ends. Two things to know:
- Targets must be public. The firewall table described in the Overview refuses connections from Ontime to the instance metadata service, to private and reserved address ranges and to your VPC. To let Ontime reach a private address (a lighting desk on your VPN, say), add it to
ONTIME_EGRESS_ALLOWin/etc/ontime/firewall.confand runsudo systemctl restart ontime-egress-guard. The metadata service can never be allowed. - OSC input is off. To let a desk send OSC to Ontime, enable OSC input in Ontime's settings, list the desk's address in
ONTIME_OSC_IN_ALLOW_FROMin the same file, restartontime-egress-guard, and open UDP 8888 from that address in your security group.
Step 9: Use your own DNS name and certificate
Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell the instance its name. This regenerates the self signed certificate for that name, updates the credentials file and reloads nginx:
sudo ontime-set-hostname ontime.your-domain.example
To use a certificate from a public CA instead, replace /etc/ssl/ontime/ontime.crt (full chain) and /etc/ssl/ontime/ontime.key (mode 0600) and run sudo systemctl reload nginx.
Step 10: Change the password
To replace the password (for example after a staff change), run the rotation helper. It writes a new password and API token to Ontime and the credentials file and restarts Ontime; every browser, screen and integration then has to log in again or use the new token:
sudo ontime-rotate-credentials
Step 11: Security checks
These checks show the protections described in the Overview on your own instance. First, the front: an anonymous API request is refused, plain HTTP redirects to HTTPS, and the firewall accepts Ontime's port 4001 only on the loopback interface:
H=$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt)
C="--cacert /etc/ssl/ontime/ontime.crt --resolve $H:443:127.0.0.1"
curl -s $C -o /dev/null -w 'https://<host>/api/poll without a login -> HTTP %{http_code}\n' "https://$H/api/poll"
curl -s --resolve "$H:80:127.0.0.1" -o /dev/null -w 'http://<host>/ -> HTTP %{http_code}\n' "http://$H/"
sudo nft list chain inet cloudimg_ontime_egress input | grep -E 'iifname|dport 4001' | sed -E -e 's/counter packets [0-9]+ bytes [0-9]+ //' -e 's/^[[:space:]]+//'
https://<host>/api/poll without a login -> HTTP 401
http://<host>/ -> HTTP 301
iifname "lo" accept
tcp dport 4001 drop comment "cloudimg-ontime-port"
From any other machine, curl http://<instance-address>:4001/ gets no answer. (On the instance itself, traffic to its own addresses travels over loopback, so test this from another host.) Next, cross-site requests. A web page on another site can make your browser send a request to Ontime, and the browser marks it Sec-Fetch-Site: cross-site (or same-site from a sibling domain). nginx refuses every such request that carries a session cookie, on every path and every spelling of it, including a ?token= that is not a real share link token; a request with no cookie gets the normal login answer:
H=$(sudo sed -n 's/^ONTIME_HOST=//p' /root/ontime-credentials.txt)
C="--cacert /etc/ssl/ontime/ontime.crt --resolve $H:443:127.0.0.1"
for p in /api/stop /API/stop /api//stop '/api/stop?token=junk' /data/db; do
curl -s $C -o /dev/null -H 'Sec-Fetch-Site: cross-site' -H 'Cookie: token=any' -w "cross-site $p with a session cookie -> %{http_code}\n" "https://$H$p"
done
curl -s $C -o /dev/null -H 'Sec-Fetch-Site: same-site' -H 'Cookie: token=any' -w 'same-site /api/stop with a session cookie -> %{http_code}\n' "https://$H/api/stop"
curl -s $C -o /dev/null -H 'Sec-Fetch-Site: cross-site' -w 'cross-site /api/stop without a cookie -> %{http_code}\n' "https://$H/api/stop"
cross-site /api/stop with a session cookie -> 403
cross-site /API/stop with a session cookie -> 403
cross-site /api//stop with a session cookie -> 403
cross-site /api/stop?token=junk with a session cookie -> 401
cross-site /data/db with a session cookie -> 403
same-site /api/stop with a session cookie -> 403
cross-site /api/stop without a cookie -> 401
With a ?token=, the cookie is removed before the request reaches Ontime, so the token has to be valid on its own: a junk one gets 401 and a real share link token works. Next, the logs: query strings (where share link tokens travel) are never written, the error log records only critical errors, and the log directory is closed to other accounts:
sudo nginx -T 2>/dev/null | grep -c 'error_log /var/log/nginx/error.log crit;'
sudo nginx -T 2>/dev/null | grep -m1 'log_format cloudimg_ontime'
stat -c '%a %U:%G %n' /var/log/nginx
4
log_format cloudimg_ontime '$remote_addr - [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent "$http_user_agent"';
750 root:www-data /var/log/nginx
Next, the egress sandbox: the ontime account cannot reach the instance metadata service or the instance's own private address, the firewall table holds your VPC's ranges, and the firewall service retries until it has them:
sudo -u ontime curl -s -m 5 -o /dev/null -w 'uid ontime -> instance metadata: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
sudo -u ontime curl -s -m 5 -o /dev/null -w "uid ontime -> this instance's private address: %{http_code}\n" "http://$(hostname -I | awk '{print $1}')/healthz" || true
sudo nft list table inet cloudimg_ontime_egress | grep -oE 'comment "cloudimg-[a-z0-9-]+"' | sort -u
grep -E '^vpc[46]=' /run/cloudimg/ontime-egress-vpc
systemctl show ontime-egress-guard -p Restart --value
systemctl show ontime nginx -p IPAddressDeny --value
uid ontime -> instance metadata: 000
uid ontime -> this instance's private address: 000
comment "cloudimg-allow"
comment "cloudimg-allow6"
comment "cloudimg-metadata"
comment "cloudimg-metadata6"
comment "cloudimg-multicast"
comment "cloudimg-multicast6"
comment "cloudimg-ontime-port"
comment "cloudimg-osc-allow"
comment "cloudimg-osc-allow6"
comment "cloudimg-osc-in"
comment "cloudimg-private"
comment "cloudimg-private6"
comment "cloudimg-vpc"
comment "cloudimg-vpc6"
vpc4=172.31.0.0/16
vpc6=none
on-failure
169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
The vpc rules hold your VPC's IPv4 and IPv6 ranges (none when the VPC has no IPv6 block), read from instance metadata at every boot. Finally, the build: Ontime reports 4.15.1, it runs as its own account in a strict sandbox, there is no package manager on the image, and Ubuntu's shared snakeoil certificate service is masked:
systemctl show ontime -p User -p ProtectSystem -p NoNewPrivileges --value
command -v npm pnpm npx corepack || echo 'no package manager on the PATH'
systemctl is-enabled ssl-cert.service || true
ontime
strict
yes
no package manager on the PATH
masked
Maintenance
- Data: Ontime keeps its projects, settings and logs in
/var/lib/ontime/data, readable by theontimeaccount only. Export a project from the editor menu to keep a copy elsewhere. - Logs:
journalctl -u ontimefor Ontime,/var/log/nginx/for the HTTPS front. - Health checks:
GET /healthzon port 80 or 443 returnsokwithout a login. - Configuration guard: Ontime does not start if first boot has not run, if the password is missing or weak, or if the firewall table is not loaded.
journalctl -u ontime-egress-guardshows why when it is waiting. - Security updates: unattended security upgrades are on and cover nginx and every system library. Reboot when a new kernel is installed. Ontime and its Node.js runtime are updated by a new version of this image.
Licences and source code
Ontime is licensed under the GNU General Public License version 3 or later. It is built from the upstream 4.15.1 source; the only change is the dependency raise, and the overrides, the lockfile diff and the raised lockfile are in /usr/share/doc/ontime-cloudimg/. The corresponding source (ontime-v4.15.1-source.tar.gz), the licence notice and full text, the Node.js licence and the licences of the bundled third party packages (third-party-licenses.json) are in the same directory, with build details in README.cloudimg.
Support
cloudimg provides 24/7/365 expert technical support for this image. Contact support@cloudimg.co.uk. This image is provided by cloudimg and is not affiliated with or endorsed by the Ontime project.