Deployment guide

UniTime 4.9

Overview

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. It is a sponsored project of the Apereo Foundation.

This guide covers deploying UniTime 4.9 on Ubuntu 24.04 LTS on Azure. The image installs UniTime 4.9.135 from the official GitHub release archive, verified against the published SHA-256 digest. It lays UniTime 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. It 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 you get:

  • 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, patched at build time, with unattended security upgrades enabled
  • 24/7 SecureAnyCloud 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 in Step 12.

1

Deploy from the Azure Portal

Search for UniTime in the Azure Marketplace, select the SecureAnyCloud offering and choose 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.

2

Deploy from the Azure CLI

Create the resource group, network and security rules, accept the Marketplace terms, then create the VM:

RG="unitime-prod"; LOCATION="eastus"; VM_NAME="unitime-01" IMAGE_URN="sac1647283583153:unitime-ubuntu-24-04:default:latest" 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
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:unitime-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --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
3

Connect via SSH

Connect to the VM:

ssh azureuser@<vm-ip>

First boot creates the timetabling database from the shipped schema before Tomcat is allowed to start. 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.

4

Verify the services

Check that all four services are active:

sudo systemctl is-active mysql.service tomcat10.service nginx.service unitime-firstboot.service

Expected output:

active active active active

Confirm that first-boot initialisation completed, and check the listeners:

sudo test -f /var/lib/sac/data/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 nobody can probe the servlet container or the database from the internet.

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.

Read the credentials file:

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 | +------+-----------+-------------+
6

Confirm the database was built

First boot loads the UniTime schema into a freshly created timetable database. UniTime then applies its own schema updates as it deploys.

Count the tables, read the database version and count the accounts:

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.

7

Open the application

nginx serves the application on port 80, so no tunnel is needed. Check the entry page, the login form and the Server header:

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:

8

Name your institution

UniTime ships with one sample academic session, so the application has a working term from the first sign-in. Name it after your own institution first. This is also the quickest way to confirm that the whole data path works end to end.

Go to Administration, then Academic Sessions. Select the session row, set Academic Initiative to your institution's name and select Update. The session table then shows the value you saved, read straight back out of MySQL:

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 terms. Then switch between them with the session selector in the page header.

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.
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-sac-unitime.conf

Tomcat log /var/log/tomcat10/catalina.out

MySQL profile /etc/mysql/mysql.conf.d/zz-sac-unitime.cnf

nginx site /etc/nginx/sites-available/sac-unitime

First boot script /usr/local/sbin/unitime-firstboot.sh

First boot sentinel /var/lib/sac/data/unitime-firstboot.done

Bootstrap marker /var/lib/sac/data/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
11

Manage the services

Check the application service and read its log:

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. Manage nginx and MySQL the same way.

UniTime is redeployed on every restart. On a Standard_B2s, allow two to three minutes before the sign-in page answers again.

12

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

Check the current heap settings and memory:

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. The tmtbl.solver.mem_limit setting 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-sac-unitime.cnf to match the larger machine.

13

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

Copy 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, because both are unique to the instance.

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/sac/data/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.

Verify all of this on your own instance:

sudo grep -E 'After=|ConditionPathExists=' /etc/systemd/system/tomcat10.service.d/10-sac-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/sac/data/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

The real sign-in form refuses every default credential, and only this VM's own password is accepted. UniTime locks an account for fifteen minutes after seven failed attempts, so the check deliberately spends only three failures on the real administrator account.

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.

UniTime sends 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, and each instance obtains its own.

This is upstream behaviour, and SecureAnyCloud leaves it exactly as the project ships it. To register your institution, select 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, use a network security group egress rule, or add 127.0.0.1 register.unitime.org to /etc/hosts.

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. An institution holding real student data should authenticate against its own directory rather than rely 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, so do not serve it 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
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/sac/data/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/sac/data/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.

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. SecureAnyCloud provides commercial support for this image separately.

  • Email: sales@secureanycloud.com
  • Website: secureanycloud.com
  • Support hours: 24/7 with a 24 hour response SLA

Deploy on Azure

Launch UniTime 4.9 on Ubuntu 24.04 LTS with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure