UniTime 4.9 on Ubuntu 24.04 LTS on Azure User Guide
Overview
This guide covers deploying UniTime 4.9 on Ubuntu 24.04 LTS on Azure. UniTime is the open source educational scheduling system used by universities and colleges to build and maintain an institution wide timetable. It covers course timetabling, examination timetabling, campus event and room management, instructor scheduling and student sectioning, and it is a sponsored project of the Apereo Foundation.
The image installs UniTime 4.9.135 from the official GitHub release archive, verified against the published SHA 256 digest, and lays it down as a complete single VM appliance: Apache Tomcat 10.1 serving the application, MySQL 8.0 holding the timetabling database, and nginx as the only public listener.
Nothing is bootstrapped at build time. The image contains no database, no application database account, no UniTime user and no connection password. On the first boot of every instance, unitime-firstboot.service creates the database from the shipped schema, generates a unique administrator password and a unique database password, and writes them to a root only file. Only then is Tomcat allowed to start.
What is included:
-
UniTime 4.9.135 from the official GitHub release archive, SHA 256 verified against the release digest
-
Apache Tomcat 10.1 from the Ubuntu archive, so the servlet container keeps receiving security updates through
apt -
MySQL 8.0 from the Ubuntu archive, tuned for a 4 GB VM and bound to the loopback interface
-
OpenJDK 21, with the JVM heap sized so Tomcat and MySQL both fit in 4 GB with no swap
-
MySQL Connector/J 9.7.0, checksum verified, with its licence artifact shipped alongside it
-
nginx reverse proxy serving the application on port 80, with no version disclosure
-
unitime-firstboot.serviceprovisioning a unique per instance database and administrator credential -
Ubuntu 24.04 LTS base, fully patched, with unattended security upgrades enabled
-
24/7 cloudimg support with a 24 hour response SLA
Key facts:
| Item | Value |
|---|---|
| Default login user | azureuser |
| Application URL | http://<vm-ip>/ (nginx on port 80) |
| Sign in page | http://<vm-ip>/login.action |
| Credentials file | /root/unitime-credentials.txt (mode 0600, root only) |
| Application configuration | /etc/unitime/custom.properties (mode 0640, root:tomcat) |
| Application tree | /var/lib/tomcat10/webapps/ROOT |
| Database | timetable on 127.0.0.1:3306 |
| Licence | Apache License 2.0 |
Prerequisites
- An active Azure subscription
- A subscription to the UniTime 4.9 listing in the Azure Marketplace
- An SSH key pair
- A virtual network and subnet in your target region
Recommended VM size: Standard_B2s (2 vCPU, 4 GB RAM) for evaluation, pilot and small deployments. For a production institution timetable, and in particular for running the course, examination or student sectioning solvers over a real course catalogue, use Standard_D4s_v5 (4 vCPU, 16 GB RAM) or larger and raise the JVM heap as described under Tuning the JVM heap.
Step 1: Deploy from the Azure Portal
Search for UniTime in the Azure Marketplace, select the cloudimg offering and click Create. Configure the network security group to allow TCP 22 for SSH and TCP 80 for HTTP from your own client networks.
You do not need to open port 8080 or port 3306. Tomcat and MySQL both bind the loopback interface only and are never reachable from outside the VM.
Step 2: Deploy from the Azure CLI
RG="unitime-prod"; LOCATION="eastus"; VM_NAME="unitime-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/unitime-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name ut-vnet --address-prefix 10.102.0.0/16 \
--subnet-name ut-subnet --subnet-prefix 10.102.1.0/24
az network nsg create -g "$RG" --name ut-nsg
az network nsg rule create -g "$RG" --nsg-name ut-nsg --name allow-ssh --priority 100 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name ut-nsg --name allow-http --priority 110 \
--source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
--size Standard_B2s --storage-sku StandardSSD_LRS \
--admin-username azureuser --ssh-key-values "$SSH_KEY" \
--vnet-name ut-vnet --subnet ut-subnet --nsg ut-nsg --public-ip-sku Standard
Step 3: Connect via SSH
ssh azureuser@<vm-ip>
First boot creates the timetabling database from the shipped schema before Tomcat is allowed to start, and UniTime then initialises its own schema updates as it deploys. On a Standard_B2s allow roughly three to four minutes from power on before the sign in page answers.
Step 4: Verify the Services
sudo systemctl is-active mysql.service tomcat10.service nginx.service unitime-firstboot.service
Expected output:
active
active
active
active

Confirm first boot initialisation completed and check the listeners:
sudo test -f /var/lib/cloudimg/unitime-firstboot.done && echo FIRSTBOOT_DONE
sudo ss -tln | grep -E ':(80|8080|3306) '
Expected output:
FIRSTBOOT_DONE
LISTEN 0 60 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 50 [::ffff:127.0.0.1]:8080 *:*
LISTEN 0 511 [::]:80 [::]:*
Only port 80 is bound to a routable address. Tomcat on 8080 and MySQL on 3306 are both bound to 127.0.0.1, so neither the servlet container nor the database can be probed from the internet.
Step 5: Retrieve the Per Instance Credentials
Every instance generates its own credentials on first boot. Nothing is shared between deployments and no default password ships in the image.
sudo cat /root/unitime-credentials.txt
# UniTime 4.9 - Per-VM Credentials
# Generated on first boot by unitime-firstboot.service
UNITIME_URL=http://10.0.0.16/
UNITIME_ADMIN_USER=admin
UNITIME_ADMIN_PASSWORD=<UNITIME_ADMIN_PASSWORD>
UNITIME_DB_NAME=timetable
UNITIME_DB_USER=timetable
UNITIME_DB_PASSWORD=<UNITIME_DB_PASSWORD>
UNITIME_DB_HOST=127.0.0.1
UNITIME_DB_PORT=3306
HTTP_PORT=80
TOMCAT_HTTP_PORT=8080

There is exactly one application account and one database account:
| Account | Where | Purpose |
|---|---|---|
admin |
UniTime | Signs in to the web application as System Administrator |
timetable |
MySQL | The application's own database connection, loopback only |
The MySQL root account uses the auth_socket plugin and has no password at all. Connect to it as the operating system root user over the unix socket:
sudo mysql -e "SELECT User, Host, plugin FROM mysql.user WHERE User='root'"
Expected output:
+------+-----------+-------------+
| User | Host | plugin |
+------+-----------+-------------+
| root | localhost | auth_socket |
+------+-----------+-------------+
Step 6: Confirm the Database Was Built
First boot loads the UniTime schema into a freshly created timetable database, and UniTime then applies its own schema updates as it deploys.
sudo mysql -N -B -e "SELECT CONCAT(COUNT(*),' tables') FROM information_schema.tables WHERE table_schema='timetable'"
sudo mysql -N -B timetable -e "SELECT CONCAT('database version ', value) FROM application_config WHERE name='tmtbl.db.version'"
sudo mysql -N -B timetable -e "SELECT CONCAT(COUNT(*),' account(s)') FROM users"
Expected output:
235 tables
database version 277
1 account(s)
There is exactly one account, and it is this VM's own.

Step 7: Open the Application
The application is served by nginx on port 80, so no tunnel is needed:
curl -s -o /dev/null -w 'entry page -> HTTP %{http_code}\n' http://127.0.0.1/
curl -s -o /dev/null -w 'login form -> HTTP %{http_code}\n' http://127.0.0.1/login.action
curl -sI http://127.0.0.1/ | grep -i '^server:'
Expected output:
entry page -> HTTP 200
login form -> HTTP 200
Server: nginx
The Server header carries no version number, and Tomcat's error pages are configured not to disclose the server build either.
From your workstation, browse to http://<vm-ip>/. You are taken to the sign in page:

Sign in as admin with the password from the credentials file. The main page opens with the timetabling modules across the top and your role and active academic session in the header:

Step 8: Name Your Institution
UniTime ships with one placeholder academic session so the application has a working term from the first sign in. Naming it after your own institution is the first thing to do, and it is also the quickest way to confirm the whole data path is working end to end.
Go to Administration then Academic Sessions, click the session row, set Academic Initiative to your institution's name and click Update. The session table then shows the value you saved, read straight back out of MySQL:

You can confirm the same values on the command line:
sudo mysql -t timetable -e "SELECT academic_term AS term, academic_year AS year, academic_initiative AS institution FROM sessions"
Expected output:
+------+------+--------------------+
| term | year | institution |
+------+------+--------------------+
| Fall | 2010 | Example University |
+------+------+--------------------+
Use Add Session on the same page to create your own real terms, then switch between them with the session selector in the page header.
Step 9: Explore the Timetabling Modules
Rooms holds the teaching space inventory that the course and examination solvers schedule into. Room records carry capacity, room type, features, groups and departmental sharing:

Events manages campus event and room bookings against the academic calendar. The module reads the active session's own event date range straight from the database:

The remaining modules follow the same pattern. Courses holds instructional offerings, configurations and classes, and is where course timetabling runs. Curricula models the expected student demand that drives sectioning. Students covers student records, course requests and the scheduling assistant. Administration holds academic sessions, departments, subject areas, permissions and the application configuration.
Step 10: Server Components
| Component | Path |
|---|---|
| Application tree | /var/lib/tomcat10/webapps/ROOT |
| UniTime configuration | /etc/unitime/custom.properties |
| UniTime runtime data | /var/lib/unitime/data |
| Shipped schema and reference data | /usr/share/unitime/sql/ |
| Licence artifact | /usr/share/unitime/LICENSE |
| Tomcat configuration | /etc/tomcat10/server.xml |
| Tomcat JVM options | /etc/default/tomcat10 |
| Tomcat gate drop-in | /etc/systemd/system/tomcat10.service.d/10-cloudimg-unitime.conf |
| Tomcat log | /var/log/tomcat10/catalina.out |
| MySQL profile | /etc/mysql/mysql.conf.d/zz-cloudimg-unitime.cnf |
| nginx site | /etc/nginx/sites-available/cloudimg-unitime |
| First boot script | /usr/local/sbin/unitime-firstboot.sh |
| First boot sentinel | /var/lib/cloudimg/unitime-firstboot.done |
| Bootstrap marker | /var/lib/cloudimg/unitime-bootstrap-ready |
| Credentials file | /root/unitime-credentials.txt |
Confirm the component versions:
sudo grep -oE 'UniTime 4\.9\.[0-9]+ build on [^i]*' /var/log/tomcat10/catalina.out | head -1
dpkg-query -W -f='Apache Tomcat ${Version}\n' tomcat10
sudo mysql -N -B -e "SELECT CONCAT('MySQL ', VERSION())"
java -version 2>&1 | head -1
basename /usr/share/tomcat10/lib/mysql-connector-j-*.jar
Expected output:
UniTime 4.9.135 build on Sat, 23 May 2026
Apache Tomcat 10.1.55-1ubuntu2~24.04.1
MySQL 8.0.46-0ubuntu0.24.04.3
openjdk version "21.0.11" 2026-04-21
mysql-connector-j-9.7.0.jar
Step 11: Managing the Services
sudo systemctl status tomcat10.service --no-pager | head -5
sudo tail -n 20 /var/log/tomcat10/catalina.out
Restart or stop the application with sudo systemctl restart tomcat10.service and sudo systemctl stop tomcat10.service. nginx and MySQL are managed the same way. Because the whole UniTime application is redeployed on every restart, allow two to three minutes on a Standard_B2s before the sign in page answers again.
Step 12: Tuning the JVM Heap
The image ships with a heap sized so that Tomcat, MySQL and the operating system all fit in a 4 GB VM without swap. Azure certification does not allow a swap file or swap partition in a marketplace image, so the stack is sized to fit in RAM rather than paged out.
pgrep -a java | head -1 | grep -oE '\-Xm[sx][0-9]+m|\-XX:MaxMetaspaceSize=[0-9]+m'
free -m
Expected output:
-Xms256m
-Xmx1536m
-XX:MaxMetaspaceSize=384m
total used free shared buff/cache available
Mem: 3912 1702 480 3 1985 2209
Swap: 0 0 0
The timetabling solvers are the memory hungry part of UniTime, and tmtbl.solver.mem_limit refuses to start a solver instance when there is not enough free heap. On a larger VM, raise the maximum heap by editing JAVA_OPTS in /etc/default/tomcat10:
JAVA_OPTS="-Djava.awt.headless=true -Xms1g -Xmx8g -XX:MetaspaceSize=96m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -Dfile.encoding=UTF-8 -Duser.timezone=UTC -Dtmtbl.custom.properties=/etc/unitime/custom.properties -Dunitime.data.dir=/var/lib/unitime/data"
Then restart with sudo systemctl restart tomcat10.service. Keep the maximum heap at or below half the VM memory so MySQL and the operating system keep plenty of headroom. If you raise the heap substantially, also raise innodb_buffer_pool_size in /etc/mysql/mysql.conf.d/zz-cloudimg-unitime.cnf to match the larger machine.
Step 13: Backing Up the Timetable
Everything UniTime holds lives in the timetable database, so a logical dump is a complete backup:
sudo mysqldump --single-transaction --routines --events timetable > /tmp/unitime-backup.sql
ls -lh /tmp/unitime-backup.sql
head -3 /tmp/unitime-backup.sql
rm -f /tmp/unitime-backup.sql
Expected output:
-rw-r--r-- 1 root root 400K Aug 7 14:11 /tmp/unitime-backup.sql
-- MySQL dump 10.13 Distrib 8.0.46, for Linux (x86_64)
--
-- Host: localhost Database: timetable
Restore with sudo mysql timetable < unitime-backup.sql. Take the dump to Azure Blob Storage or a Recovery Services vault for anything you rely on. Back up /etc/unitime/custom.properties and /root/unitime-credentials.txt too, since both are unique to the instance.
Step 14: How the Security Model Works
This image is built so that no shared or default credential ever exists on a running instance.
- UniTime's documented demo login does not exist here. UniTime's own installation data seeds an administrator account whose password is the published demo credential. That statement is removed from the copy of the reference data shipped in this image, and the build fails closed if the password hash is found anywhere in the image. Upstream's demo dataset, which seeds many more published logins, is not shipped at all.
- Nothing is bootstrapped in the image. There is no
timetabledatabase, notimetabledatabase account, no UniTime user and no connection password in the image. First boot creates all of them. - The application cannot start unbootstrapped.
tomcat10.serviceandnginx.serviceboth carry aConditionPathExistsguard on/var/lib/cloudimg/unitime-bootstrap-ready. First boot writes that marker only after the database exists and both passwords are provisioned, so systemd will not start the application before it has its own credentials. - No management surface ships. The Tomcat manager and host-manager applications are not installed,
tomcat-users.xmldeclares zero users and zero roles, the Tomcat shutdown port is disabled outright, and there is no AJP connector. - The database is not published. MySQL binds
127.0.0.1only, and itsrootaccount usesauth_socketso no MySQL password exists anywhere.

You can verify all of this on your own instance:
sudo grep -E 'After=|ConditionPathExists=' /etc/systemd/system/tomcat10.service.d/10-cloudimg-unitime.conf
sudo systemctl show -p ConditionResult --value tomcat10.service
sudo /usr/local/sbin/unitime-login-check.sh
Expected output:
After=network-online.target mysql.service unitime-firstboot.service
ConditionPathExists=/var/lib/cloudimg/unitime-bootstrap-ready
yes
Proving the upstream default and common weak credentials are all rejected
rejected: admin/admin (UniTime's documented demo login)
rejected: admin with a blank password
rejected: admin/password
rejected: unitime/unitime
rejected: timetable/unitime (the upstream sample DB login)
rejected: root/root
rejected: Administrator/admin
LOGIN OK for 'admin'
LOGIN_CHECK_OK
Every default credential is refused by the real sign in form, and only this VM's own password is accepted. UniTime also locks an account for fifteen minutes after seven failed attempts, so the check deliberately spends only three failures on the real administrator account.
Step 15: UniTime's Registration Service
UniTime contacts register.unitime.org from the main page and offers to register the instance with the Apereo project. The banner in the page footer reports whether this instance is registered.
What UniTime sends is the product version, the number of sessions and active users reported by its own query log, and the URL the instance is reached on. The key it receives back is stored at /var/lib/unitime/data/unitime.reg. That file is not part of the image; each instance obtains its own.
This is upstream behaviour and cloudimg leaves it exactly as the project ships it. If you would like to register your institution, click the link in the footer. To hide the prompt without changing anything else, add the project's own setting to /etc/unitime/custom.properties and restart:
unitime.registration.obtrusiveness=none
If your policy does not allow the outbound call at all, block the host at the network layer, for example with a network security group egress rule or by adding 127.0.0.1 register.unitime.org to /etc/hosts.
Step 16: Security Recommendations
- Rotate the administrator password from Preferences in the web interface, or from the command line with
sudo mysql timetable -e "UPDATE users SET password=TO_BASE64(UNHEX(MD5('<new-password>'))) WHERE username='admin'" - Move to institutional single sign on for production. UniTime supports LDAP, Kerberos and OAuth2 authentication. Local accounts store their password as a base64 encoded MD5 digest, which is upstream's design, so an institution holding real student data should authenticate against its own directory rather than relying on local accounts
- Restrict the network security group so port 80 is reachable only from networks you trust
- Terminate TLS in front of the VM using Azure Application Gateway, or add a certificate to the nginx site for direct HTTPS. UniTime carries student and staff data and should not be served over plain HTTP on an untrusted network
- Keep the database on loopback. If you must reach MySQL from your workstation, use an SSH tunnel with
ssh -L 3306:127.0.0.1:3306 azureuser@<vm-ip>rather than rebinding it - Apply operating system updates monthly. Unattended security upgrades are enabled by default
Step 17: Troubleshooting
The sign in page returns 502 from nginx. Tomcat is still deploying the application or is not running. UniTime is a large application and takes two to three minutes to deploy on a Standard_B2s. Check sudo systemctl status tomcat10.service and sudo tail -n 50 /var/log/tomcat10/catalina.out.
Tomcat is inactive and will not start. Confirm the bootstrap marker exists with sudo test -f /var/lib/cloudimg/unitime-bootstrap-ready && echo present. If it is missing, first boot did not finish. Inspect it with sudo journalctl -u unitime-firstboot.service --no-pager.
The credentials file only contains a comment. First boot has not completed yet. Wait for /var/lib/cloudimg/unitime-firstboot.done to appear, then read the file again.
Sign in fails with the password from the credentials file. The password is unique to this specific VM, so make sure you are reading the file on the instance you are signing in to. If you have made several failed attempts, UniTime locks the account for fifteen minutes; wait and try again.
A page reports that access is denied because the user has no department. Some course timetabling pages are scoped to a department. Create your departments and subject areas under Administration first, then assign them to the manager under Administration then Timetable Managers.
The Rooms or Events page reports that it failed to load. Those modules are GWT clients. Do a hard refresh in the browser, and check sudo tail -n 50 /var/log/tomcat10/catalina.out for the underlying error.
Step 18: Support and Licensing
UniTime is licensed under the Apache License 2.0, with no per user or per institution fees. The licence artifact shipped with the application is at /usr/share/unitime/LICENSE. cloudimg provides commercial support for this image separately.
- Email: support@cloudimg.co.uk
- Website: www.cloudimg.co.uk
- Support hours: 24/7 with a 24 hour response SLA