Hh
Applications Azure

Horilla HRMS on Ubuntu 24.04 on Azure User Guide

| Product: Horilla HRMS on Ubuntu 24.04 LTS on Azure

Overview

Horilla is an open source HR management suite that covers the whole employee lifecycle in one system. It runs recruitment with a candidate pipeline and interview scheduling, structured onboarding, an employee directory holding work information and documents, attendance with shift and late-arrival tracking, leave with approval chains and allocation rules, payroll with contracts, payslips, bonuses and deductions, performance reviews, offboarding, asset assignment, an internal helpdesk and project tracking.

Everything the web interface does is also reachable through a versioned REST API, and the interface ships in twelve languages.

The cloudimg image delivers Horilla 2.1.7 on Ubuntu 24.04, served over HTTPS, with an empty database and every secret generated on the first boot of your VM. Backed by 24/7 cloudimg support.

What is included:

  • Horilla 2.1.7 in a dedicated Python 3.12 virtual environment at /opt/horilla, run by gunicorn as the unprivileged horilla user and bound to 127.0.0.1:8000 only
  • The web interface, served by nginx over HTTPS on port 443 with a certificate generated for your VM; port 80 only redirects to HTTPS
  • A separate APScheduler background job runner for scheduled tasks and notifications
  • PostgreSQL 16 and Redis 7, both on the loopback address only
  • No default login: the administrator password, the database password, the Redis password, the Django secret key, the database-setup password and the TLS key are all generated on your VM's first boot and written to a root-only file
  • The first-run setup wizard is closed on arrival rather than left open behind a published password, and brute-force protection is active on the sign-in form
  • postgresql, redis-server, horilla-web, horilla-scheduler and nginx systemd services, enabled and active

Horilla is a trademark of its respective owner. All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them. This image packages the unmodified open source software, which is distributed under the GNU Lesser General Public License v2.1.

The Horilla sign-in screen served over HTTPS

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU, 4 GiB RAM) runs Horilla comfortably for a small organisation; choose a larger size for many concurrent users or heavy payroll and reporting runs. Network security group inbound rules: 22/tcp from your management network, 443/tcp for the web interface, and optionally 80/tcp, which only redirects to HTTPS.

Step 1: Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Horilla HRMS by cloudimg and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTPS (443). Then select Review + create and Create.

Step 2: Deploy from the Azure CLI

az vm create \
  --resource-group my-resource-group \
  --name my-horilla \
  --image cloudimg:horilla-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --storage-sku StandardSSD_LRS \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then open the ports the web interface needs:

az vm open-port --resource-group my-resource-group --name my-horilla --port 443 --priority 1001
az vm open-port --resource-group my-resource-group --name my-horilla --port 80 --priority 1002

Step 3: Connect to your VM

ssh azureuser@<vm-public-ip>

The first boot generates this VM's secrets and initialises the database. That takes a few minutes on a Standard_B2s — the database schema is applied from scratch — and the web interface deliberately does not answer until it has finished, so there is never a window in which Horilla is reachable without a password. If the site is not up yet, wait and retry.

Step 4: Confirm the Horilla stack is running

All five services should report active:

sudo systemctl is-active postgresql redis-server horilla-web horilla-scheduler nginx
active active active active active

The Horilla stack running, with component versions

Horilla exposes two probes. /health/ is a cheap liveness check, and /ready/ additionally verifies the database, the cache and the background job runner:

curl -sk https://127.0.0.1/health/
{"status": "ok"}
curl -sk https://127.0.0.1/ready/
{"status": "ok", "database": "ok", "cache": "ok", "scheduler": "ok"}

"scheduler": "ok" confirms the horilla-scheduler service has registered its jobs. These are the two endpoints to point an Azure Load Balancer or Application Gateway health probe at.

Step 5: Check what is listening

Only SSH and the two web ports are reachable from outside the VM. The application server, PostgreSQL and Redis are bound to the loopback address:

sudo ss -Hltnp | awk '{print $4, $6}' | sed 's/users://' | sort
0.0.0.0:22 (("sshd",pid=1537,fd=3),("systemd",pid=1,fd=128))
0.0.0.0:443 (("nginx",pid=2941,fd=7),("nginx",pid=2940,fd=7),("nginx",pid=2939,fd=7))
0.0.0.0:80 (("nginx",pid=2941,fd=5),("nginx",pid=2940,fd=5),("nginx",pid=2939,fd=5))
127.0.0.1:5432 (("postgres",pid=1254,fd=6))
127.0.0.1:6379 (("redis-server",pid=1239,fd=6))
127.0.0.1:8000 (("gunicorn",pid=2948,fd=5),("gunicorn",pid=2945,fd=5),("gunicorn",pid=2943,fd=5),("gunicorn",pid=2935,fd=5))
127.0.0.53%lo:53 (("systemd-resolve",pid=578,fd=15))
127.0.0.54:53 (("systemd-resolve",pid=578,fd=17))
[::1]:6379 (("redis-server",pid=1239,fd=7))
[::]:22 (("sshd",pid=1537,fd=4),("systemd",pid=1,fd=129))
[::]:443 (("nginx",pid=2941,fd=8),("nginx",pid=2940,fd=8),("nginx",pid=2939,fd=8))
[::]:80 (("nginx",pid=2941,fd=6),("nginx",pid=2940,fd=6),("nginx",pid=2939,fd=6))

The process IDs will differ on your VM. A helper asserts the off-box set for you:

sudo /usr/local/sbin/horilla-port-check.sh
PORTCHECK_OK off-box TCP = {22 80 443}

The exact listening sockets, with the off-box set asserted

Step 6: Retrieve your administrator password

Every secret is unique to your VM and is written to a root-only file at first boot:

sudo ls -l /root/horilla-credentials.txt
-rw------- 1 root root 813 Sep 20 07:11 /root/horilla-credentials.txt
sudo cat /root/horilla-credentials.txt

The file gives you the sign-in URL, the administrator username and password, the password that gates the first-run setup wizard, and the PostgreSQL and Redis passwords. The username is horilla-admin:

sudo grep -E '^HORILLA_(URL|ADMIN_USER)=' /root/horilla-credentials.txt
HORILLA_URL=https://<vm-public-ip>/
HORILLA_ADMIN_USER=horilla-admin

The per-VM credentials file, with secret values redacted

None of these values exist in the published image. Two VMs launched from the same image receive entirely different secrets, and a password taken from one will not sign in on the other.

Step 7: Verify the sign-in path

A helper signs in through nginx exactly as a browser does — fetching the form, carrying the CSRF token and cookies, and sending matching Origin and Referer headers — then confirms the session reaches the dashboard. It also proves the published default credentials are refused:

sudo /usr/local/sbin/horilla-verify-login.sh
  probing published default credentials
  probing the real account with a wrong and a blank password
  default, weak and blank credentials all refused
  per-VM credential authenticated and reached the dashboard
HORILLA_LOGIN_OK

The credential round-trip, with published defaults refused

The check takes about a minute: it paces its requests to stay within Horilla's own sign-in rate limit, and it runs the successful sign-in last so the deliberate failures it just made are cleared from the brute-force counter.

Step 8: Sign in

Open https://<vm-public-ip>/ in a browser. The image ships a self-signed certificate generated for your VM's address, so your browser will warn on first visit — Step 12 replaces it with a certificate from a public authority.

Sign in with horilla-admin and the password from Step 6. Horilla opens on the dashboard, which shows headline counts and a setup checklist:

The Horilla dashboard after signing in

Horilla offers a short guided tour on first sign-in. Work through it or dismiss it — it does not affect the configuration.

Step 9: Set up your organisation

Horilla files every employee under a company, a department and a job position, so create those first. Go to Settings (the gear icon) → OrganizationDepartments and add your departments, then Job Positions and add the roles within each one.

Departments and the job positions within them

Then choose Employee in the sidebar and select Create to add people. Each employee record holds personal details, work information (department, job position, reporting manager, shift and contract), bank details and documents. The directory supports search, filtering and both list and card views, and can import from a spreadsheet via ActionsImport.

The employee directory, filed by department

With employees in place, the remaining modules become useful: Attendance for clock-in and shift tracking, Leave for allocation and approval, Payroll for contracts and payslips, Recruitment for the candidate pipeline, and Performance for objectives and reviews.

Step 10: Use the REST API

Horilla exposes a versioned REST API at /api/v1/. Authenticate with a JSON Web Token obtained from the login endpoint. Replace <HORILLA_ADMIN_PASSWORD> with the password from Step 6:

TOKEN=$(curl -fsSk -X POST https://127.0.0.1/api/v1/auth/login/ \
  -H 'Content-Type: application/json' \
  -d '{"username":"horilla-admin","password":"<HORILLA_ADMIN_PASSWORD>"}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["access"])')
curl -fsSk https://127.0.0.1/api/v1/employee/list/employees/ \
  -H "Authorization: Bearer ${TOKEN}" | head -c 280
{"count":1,"next":null,"previous":null,"results":[{"id":1,"employee_first_name":"Horilla","employee_last_name":"Administrator","email":"admin@localhost.localdomain","job_position_name":"Administrator","employee_work_info_id":"1","employee_profile":null,"employee_bank_details_id":null}]}

Both curl calls use -f, so an unexpected HTTP error stops the pipeline instead of quietly feeding an HTML error page into the next command.

Without a token the same endpoint is refused:

curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/api/v1/employee/list/employees/
401

Interactive Swagger and ReDoc documentation is published at /api/v1/swagger/ and /api/v1/redoc/. In production mode those pages require an authenticated administrator session, so open them in the browser you signed in with in Step 8 rather than with curl:

curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1/api/v1/swagger/
401

That 401 is expected and correct — it is the API documentation refusing an unauthenticated caller.

Access tokens are valid for one hour, and changing or resetting a password immediately invalidates every token issued before it.

Step 11: Back up and restore

Horilla's state is the PostgreSQL database plus the uploaded files under /opt/horilla/app/media. Back up both together:

sudo -u postgres pg_dump -Fc horilla > "horilla-$(date +%F).dump"
sudo tar czf "horilla-media-$(date +%F).tar.gz" -C /opt/horilla/app media

Copy both files off the VM — for example to Azure Blob Storage — and keep them with the same retention as your other HR records.

To restore onto a fresh VM, stop the application, drop and recreate the database, then load the dump. This destroys the current contents of the database, so only run it on a VM you intend to overwrite:

# DESTRUCTIVE — overwrites all Horilla data on this VM
sudo systemctl stop horilla-web horilla-scheduler
sudo -u postgres dropdb horilla
sudo -u postgres createdb -O horilla -E UTF8 horilla
sudo -u postgres pg_restore -d horilla --no-owner --role=horilla horilla-2026-09-20.dump
sudo tar xzf horilla-media-2026-09-20.tar.gz -C /opt/horilla/app
sudo chown -R horilla:horilla /opt/horilla/app/media
sudo systemctl start horilla-web horilla-scheduler

The administrator password on the restored VM is the one from the VM the dump came from, not the one in the new VM's credentials file.

Step 12: Use your own domain and certificate

Point a DNS A record at your VM's public IP, then add the hostname to Horilla's allow-list. Django validates the Host header against ALLOWED_HOSTS and compares browser form posts against CSRF_TRUSTED_ORIGINS, so both need the new name:

Replace <your-domain> with your hostname:

sudo sed -i "s#^ALLOWED_HOSTS='\(.*\)'#ALLOWED_HOSTS='\1,<your-domain>'#" /etc/horilla/horilla.env
sudo sed -i "s#^CSRF_TRUSTED_ORIGINS='\(.*\)'#CSRF_TRUSTED_ORIGINS='\1,https://<your-domain>'#" /etc/horilla/horilla.env
sudo grep -E '^(ALLOWED_HOSTS|CSRF_TRUSTED_ORIGINS)=' /etc/horilla/horilla.env
sudo systemctl restart horilla-web horilla-scheduler

Both values are single-quoted in that file, which is what the sed expressions above preserve — the file is read both by systemd and by shell, and an unquoted Django secret key breaks the shell reader.

Then request a certificate. This needs the DNS name to already resolve to this VM and port 80 to be reachable from the internet:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> --redirect --agree-tos -m <your-email>

Certbot edits the nginx site in place and installs a renewal timer. The X-Forwarded-Proto header the site already sets is what tells Django the request arrived over TLS, so sign-in keeps working unchanged.

Step 13: Keep the VM patched

Ubuntu's unattended-upgrades is enabled in the image, so security updates install automatically. To apply everything pending immediately:

sudo apt-get update && sudo apt-get upgrade

To move to a newer Horilla release, back up first (Step 11), then replace the application directory and reapply migrations:

# Replaces the installed application version
sudo systemctl stop horilla-web horilla-scheduler
sudo tar czf /root/horilla-app-backup.tar.gz -C /opt/horilla app
sudo -u postgres pg_dump -Fc horilla > /root/horilla-preupgrade.dump
# fetch and unpack the new release over /opt/horilla/app, then:
sudo /opt/horilla/venv/bin/pip install --no-cache-dir -r /opt/horilla/app/requirements.txt
sudo systemctl start horilla-web horilla-scheduler

Read the release notes before upgrading: Horilla 2.1.7's own notes flag a migration that drops five columns and is not reversible.

Security notes

No shared secrets. The administrator password, the Django secret key, the PostgreSQL and Redis passwords, the setup-wizard password and the TLS key pair are all generated on your VM's first boot. The published image contains none of them and no administrator account at all.

The setup wizard is closed. Horilla ships a first-run wizard that creates the initial administrator, gated by a DB_INIT_PASSWORD whose upstream default is a published string. On this image the wizard is already complete on arrival — an administrator with an employee record exists, which is the condition Horilla uses to consider setup finished — and the password that gates it is per-VM regardless.

Production mode is enforced. DEBUG is off and Horilla's own production checks are armed, so the application refuses to start with a known-insecure secret key, an ALLOWED_HOSTS of *, or the default setup password. Session and CSRF cookies are marked Secure.

Brute-force protection. The sign-in form locks an account and address pair after five failed attempts, with a thirty-minute cool-off, and the API applies its own rate limits. nginx overwrites the forwarded-address headers from the real connection, so a client cannot forge the address its failures are counted against.

Restrict access. Keep 22/tcp limited to your management network in the network security group, and put 443/tcp behind your VPN or a restricted source range if Horilla does not need to be public. HR data is personal data: treat the credentials file, the database dumps and the media archive with the same care as the records themselves.

Rotate the administrator password after your first sign-in, from the user menu in the top right.

Support

cloudimg provides 24/7 support for this image, covering deployment, upgrades, TLS certificates and custom domains, SSO and LDAP integration, API automation and scaling. Contact us at cloudimg.co.uk.