Applications Azure

UniTime 4.9 on Ubuntu 24.04 LTS on Azure User Guide

| Product: UniTime 4.9 on Ubuntu 24.04 LTS

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.service provisioning 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

  1. An active Azure subscription
  2. A subscription to the UniTime 4.9 listing in the Azure Marketplace
  3. An SSH key pair
  4. 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

The four appliance units, the component versions for UniTime, Tomcat, MySQL, OpenJDK and the JDBC driver, and the listener table showing that only port 80 leaves the VM

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

The credentials file mode and owner, the MySQL root account on the auth_socket plugin with no password, and every default and common credential rejected by the real login form

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.

The timetabling schema table count and database version, the academic session read out of MySQL, and the application answering HTTP 200 through nginx with no version in the Server header

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:

The UniTime sign in page served on port 80, showing the University Timetabling banner with username and password fields

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:

The UniTime main page after signing in, showing the Courses, Curricula, Students, Events and Administration modules and the signed in System Administrator role

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:

The Academic Sessions administration page listing the academic session with the institution name that was saved through the web interface, alongside the term dates, status and student scheduling deadlines

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:

The UniTime Rooms module with its resource filter, search control and the Add New and Edit Room Sharing actions, scoped to the active academic session

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 UniTime Events module showing the academic session selector with the session date range read from the database, plus the event and room filters

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 timetable database, no timetable database account, no UniTime user and no connection password in the image. First boot creates all of them.
  • The application cannot start unbootstrapped. tomcat10.service and nginx.service both carry a ConditionPathExists guard 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.xml declares 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.1 only, and its root account uses auth_socket so no MySQL password exists anywhere.

The systemd bootstrap gate on Tomcat and nginx, the marker directory, the enabled units and the shipped schema files that first boot builds the database from

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