Sign inSign up

horilla/horilla

By horilla

Updated 7 days ago

[Moved to horilla/horilla-hr] Free and open source HR software.

Image
1

10K+

horilla/horilla repository overview

Horilla HR

Free and open source HR software. Recruitment, onboarding, attendance, leave, payroll, performance, assets and helpdesk in one Django application.

⚠️ This repository has moved

The official image is now horilla/horilla-hr.

This mirror still receives every stable release, so existing docker pull horilla/horilla deployments keep working — nothing breaks today. But new tags, documentation and support all target horilla/horilla-hr first.

To switch, change the image name in your compose file or deployment:

- image: horilla/horilla:latest
+ image: horilla/horilla-hr:latest

Both names are built from the same commit and are byte-identical. The examples below use the new name.

Following an older guide? The instruction to run horilla/horilla:1.4 with manage.py runserver is obsolete — it pins a January 2026 image and uses Django's development server, which is not suitable for production. Use the Docker Compose setup below.


Supported tags

TagMeaning
latestNewest stable release. Currently 2.1.7.
X.Y.ZExact release, immutable once published. Pin this in production.
X.YNewest patch within a minor line. Moves as patch releases ship.

Architectures: linux/amd64, linux/arm64 (single multi-arch manifest — Docker picks the right one).

docker pull horilla/horilla-hr
# or pin an exact version:
docker pull horilla/horilla-hr:2.1.7

Quick start

Horilla needs PostgreSQL. The fastest way to a working instance is Compose:

# compose.yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: horilla_db
      POSTGRES_USER: horilla_user
      POSTGRES_PASSWORD: change-me-db-password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U horilla_user -d horilla_db"]
      interval: 5s
      retries: 10

  web:
    image: horilla/horilla-hr:2.1.7
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8000:8000"
    environment:
      DEBUG: "0"
      SECRET_KEY: "replace-with-50-plus-random-characters"
      ALLOWED_HOSTS: "localhost,127.0.0.1"
      CSRF_TRUSTED_ORIGINS: "http://localhost:8000"
      DATABASE_URL: "postgres://horilla_user:change-me-db-password@db:5432/horilla_db"
      DB_HOST: db
      DB_PORT: "5432"
      DB_INIT_PASSWORD: "replace-with-your-own-init-password"
    volumes:
      - media:/app/media
      - staticfiles:/app/staticfiles

volumes:
  postgres_data:
  media:
  staticfiles:
docker compose up -d

Then open http://localhost:8000.

First boot takes several minutes (around 3-4 minutes on a typical runner). The container applies the full migration set against the empty database before the web server binds. Watch it with docker compose logs -f web; the app is ready when /health/ responds.

Generate a real SECRET_KEY:

docker run --rm horilla/horilla-hr:2.1.7 \
  python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Environment variables

Required

The container refuses to start in production without these. There are no usable defaults — that is deliberate.

VariableNotes
SECRET_KEYDjango signing key. 50+ random characters. Changing it invalidates all sessions.
DEBUG0 in production. 1 exposes tracebacks and disables security checks.
ALLOWED_HOSTSComma-separated hostnames. Do not use * in production.
CSRF_TRUSTED_ORIGINSComma-separated, must include the scheme (https://hr.example.com).
DB_INIT_PASSWORDGates the first-run database setup screen. Startup aborts if left at the shipped default.
Database
VariableDefaultNotes
DATABASE_URLPreferred. postgres://user:pass@host:5432/dbname.
DB_HOSTdbUsed by the startup wait loop. Set it to your Postgres hostname.
DB_PORT5432
DB_ENGINE / DB_NAME / DB_USER / DB_PASSWORDDiscrete alternative when DATABASE_URL is unset.

DB_HOST and DB_PORT are read separately from DATABASE_URL: the entrypoint waits on that host:port before running migrations. If they disagree, the container waits on the wrong address.

Optional
VariableDefaultNotes
HORILLA_ENVSet to production to force security checks even if DEBUG is accidentally 1.
REDIS_URLunsetEnables Redis caching.
SECURE_SSL_REDIRECTFalseTurn on when TLS terminates in front of the app.
TIME_ZONEAsia/Kolkata
GUNICORN_WORKERSauto

What the container does at startup

  1. Waits for DB_HOST:DB_PORT (30 attempts, 1s apart)
  2. Generates and persists a random SECRET_KEY to /app/media/.generated_secret_key if the supplied one is empty or a known public default
  3. Runs migrate --noinput
  4. Runs collectstatic --noinput --clear
  5. Starts gunicorn on port 8000

Step 4 uses --clear deliberately: STATIC_ROOT is a named volume that outlives the image, and stale pre-compressed .gz/.br files would otherwise be served to browsers after an upgrade.


Volumes

PathContents
/app/mediaUser uploads, and the generated SECRET_KEY if one was auto-created. Back this up.
/app/staticfilesCollected static assets. Rebuilt on every start; safe to discard.

Health checks

EndpointReturns
/health/{"status": "ok"} once the web server is serving
/ready/{"status": "ok", "database": "ok"} — also proves DB connectivity

The image ships a HEALTHCHECK that polls /health/ every 30s with a 60s start period. On a first boot the container may report starting for several minutes while migrations run — that is expected, not a failure.


Creating the first user

docker compose exec web python manage.py createsuperuser

Production notes

  • Run behind a reverse proxy that terminates TLS. Set CSRF_TRUSTED_ORIGINS to the public https:// origin and enable SECURE_SSL_REDIRECT.
  • Pin an exact version (horilla/horilla-hr:2.1.7), not latest, so a deploy cannot pick up a new major release unattended.
  • Back up /app/media and your database together. The auto-generated SECRET_KEY lives in the media volume; losing it invalidates every session and signed token.
  • Migrations run on every container start. With more than one replica, start one first and let it finish before scaling up — there is no advisory lock coordinating concurrent migrations.
  • The image runs as a non-root user (appuser, uid 1000). Mounted volumes must be writable by uid 1000.

Image details

  • Base: python:3.12-slim (Debian), multi-stage build
  • Python dependencies installed into /opt/venv; build toolchain is not present in the final image
  • Runs as appuser (uid 1000), never root
  • Exposes port 8000
  • Locale catalogs (.mo) compiled at build time, so non-English UI languages work out of the box

Each image carries OCI labels — org.opencontainers.image.version, .revision, .source — so any published image traces back to the exact commit it was built from:

docker inspect horilla/horilla-hr:2.1.7 \
  --format '{{json .Config.Labels}}' | python3 -m json.tool

How these images are built

Built and published by GitHub Actions from horilla/horilla-hr on every release tag. Before any image is pushed, the pipeline:

  1. Asserts the code's __version__ matches the release tag
  2. Builds for amd64 and arm64
  3. Boots the built image against a real PostgreSQL and waits for /health/ and /ready/
  4. Fails the release on any CRITICAL vulnerability found by Trivy

An image that builds but does not run cannot be published.


Support

Tag summary

Content type

Image

Digest

sha256:22e139b40

Size

340.2 MB

Last updated

7 days ago

docker pull horilla/horilla