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 unprivilegedhorillauser and bound to127.0.0.1:8000only - 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-schedulerandnginxsystemd 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.
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.
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 StandardOpen 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 1002Connect 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.
Confirm the Horilla stack is running
All five services should report active:
sudo systemctl is-active postgresql redis-server horilla-web horilla-scheduler nginxactive active active active activeHorilla 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.
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://' | sort0.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.shPORTCHECK_OK off-box TCP = {22 80 443}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.txtsudo cat /root/horilla-credentials.txtThe 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.txtHORILLA_URL=https://<vm-public-ip>/ HORILLA_ADMIN_USER=horilla-adminNone 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.
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.shprobing 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_OKThe 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.
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.
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
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/401Interactive 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/401That 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.
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 mediaCopy 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-schedulerOn 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.
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-schedulerBoth 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.
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 upgradeTo 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-schedulerRead 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