Deployment guide

Horilla

Overview

Horilla is an open-source HR management suite that covers the whole employee lifecycle in one system. It handles recruitment with a candidate pipeline and interview scheduling, structured onboarding, and an employee directory with work information and documents. It also covers attendance, leave, payroll, performance reviews, offboarding, asset assignment, an internal helpdesk and project tracking.

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

This SecureAnyCloud image delivers Horilla 2.1.7 on Ubuntu 24.04, served over HTTPS, with an empty database. Every secret is generated on the first boot of your VM.

What you get:

  • 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
  • 24/7 SecureAnyCloud support

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.

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a 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.

Configure these network security group inbound rules: 22/tcp from your management network and 443/tcp for the web interface. Optionally allow 80/tcp, which only redirects to HTTPS.

1

Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Horilla HRMS by SecureAnyCloud and select Create.

On the Basics tab, 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.

2

Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:horilla-ubuntu-24-04:default:latest
az vm create \
  --resource-group my-resource-group \
  --name my-horilla \
  --image sac1647283583153:horilla-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --storage-sku StandardSSD_LRS \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

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
3

Connect to your VM

ssh azureuser@<vm-public-ip>

The first boot generates this VM's secrets and initialises the database. On a Standard_B2s, this takes a few minutes because the database schema is applied from scratch.

The web interface deliberately does not answer until the first boot has finished. This means Horilla is never reachable without a password. If the site is not up yet, wait and retry.

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

Horilla exposes two probes. /health/ is a cheap liveness check. /ready/ also 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 that the horilla-scheduler service has registered its jobs.

Point an Azure Load Balancer or Application Gateway health probe at these two endpoints.

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.

List the listening sockets:

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

Retrieve your administrator password

Every secret is unique to your VM. The first boot writes them to a root-only file:

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 lists 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 administrator 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

None of these values exist in the published image. Two VMs launched from the same image receive entirely different secrets. A password taken from one VM does not sign in on the other.

7

Verify the sign-in path

A helper signs in through nginx exactly as a browser does. It fetches the form, carries the CSRF token and cookies, and sends matching Origin and Referer headers. It then confirms that the session reaches the dashboard.

The helper also proves that 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 check takes about a minute. It paces its requests to stay within Horilla's own sign-in rate limit. It runs the successful sign-in last, so the deliberate failures it made are cleared from the brute-force counter.

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 shows a warning on the first visit. Step 12 replaces the certificate with one 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.

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

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) > Organization > Departments and add your departments. Then open Job Positions and add the roles within each department.

Next, 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. To import from a spreadsheet, choose Actions > Import.

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
  • Performance for objectives and reviews
10

Use the REST API

Horilla exposes a versioned REST API at /api/v1/. Authenticate with a JSON Web Token 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. An unexpected HTTP error therefore stops the pipeline instead of 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. Open them in the browser you signed in with in Step 8, not with curl:

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

That 401 is expected. It is the API documentation refusing an unauthenticated caller.

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

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.

Run the backup:

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. 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 run it only 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

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

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. 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, and the sed expressions above preserve the quotes. Both systemd and the shell read the file, and an unquoted Django secret key breaks the shell reader.

Request a certificate. The DNS name must already resolve to this VM, and port 80 must 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 site already sets the X-Forwarded-Proto header, which tells Django that the request arrived over TLS. Sign-in therefore keeps working unchanged.

13

Keep the VM patched

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

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 you upgrade. The notes for Horilla 2.1.7 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 it contains no administrator account.

The setup wizard is closed. Horilla ships a first-run wizard that creates the initial administrator. The wizard is 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. The password that gates the wizard is per-VM regardless.

Production mode is enforced. DEBUG is off and Horilla's own production checks are armed. 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. 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. If Horilla does not need to be public, put 443/tcp behind your VPN or a restricted source range.

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

SecureAnyCloud provides 24/7 support for this image. Support covers deployment, upgrades, TLS certificates and custom domains, SSO and LDAP integration, API automation and scaling. Contact us at secureanycloud.com.

Deploy on Azure

Launch Horilla HRMS on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure