Merge branch 'fix-docker' of https://github.com/horilla-opensource/Horilla into fix-docker

This commit is contained in:
Horilla
2025-10-23 14:54:49 +05:30
17 changed files with 313 additions and 16288 deletions

View File

@@ -1,33 +1,89 @@
# Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Django stuff:
*.log
local_settings.py
media/
staticfiles/
static_root/
# Environments
.env
.venv/
venv/
.venv
env/
venv/
ENV/
# VCS
.git/
.gitignore
# IDEs
.vscode/
.idea/
*.swp
# OS files
# MacOS
.DS_Store
# Node
node_modules/
# Django static/media
staticfiles/
media/
# Bytecode & caches
# Git
.git
.gitignore
README.md
Dockerfile
.dockerignore
docker-compose.yml
.vscode
.idea
*.pyc
*.pyo
*.pyd
.pytest_cache/
# Docker
.wheels/
__pycache__
.pytest_cache
.coverage
*.log
.env
horillavenv
node_modules
.DS_Store

19
.env.example Normal file
View File

@@ -0,0 +1,19 @@
# Django
DJANGO_SETTINGS_MODULE=horilla.settings
SECRET_KEY=change-me
DEBUG=False
ALLOWED_HOSTS=*
# Database (used by compose to form DATABASE_URL)
POSTGRES_DB=horilla
POSTGRES_USER=horilla
POSTGRES_PASSWORD=horilla
# Optional: storage/other
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_STORAGE_BUCKET_NAME=
# Redis Cache
REDIS_PASSWORD=horilla
REDIS_URL=redis://:horilla@redis:6379/0

View File

@@ -1,65 +1,85 @@
# Production Dockerfile (multi-stage) for Horilla v2.0
# Builds Python wheels in a builder image, then installs them into a slim runtime.
FROM python:3.11-slim-bookworm AS builder
# Build stage - for compiling dependencies
FROM python:3.12-slim as builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# System deps required to build certain Python packages and render PDFs/images
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
libffi-dev \
libjpeg-dev \
zlib1g-dev \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libmagic1 \
libssl-dev \
gettext \
git \
&& rm -rf /var/lib/apt/lists/*
# Install build dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
libjpeg-dev \
zlib1g-dev \
libcairo2-dev \
libpango1.0-dev \
libgdk-pixbuf-xlib-2.0-dev \
libxml2-dev \
libxslt1-dev \
libffi-dev \
pkg-config \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Leverage layer caching for deps
COPY requirements.txt ./
RUN python -m pip install --upgrade pip \
&& pip wheel --no-cache-dir --no-deps -r requirements.txt -w /wheels
# Install Python dependencies
COPY requirements.txt .
RUN pip install --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt gunicorn psycopg2-binary
# ---------------------------- Runtime image ----------------------------
FROM python:3.11-slim-bookworm AS runtime
# Production stage - minimal runtime image
FROM python:3.12-slim as production
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH"
# Runtime libs only (no compilers)
RUN apt-get update && apt-get install -y --no-install-recommends \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libjpeg62-turbo \
zlib1g \
libmagic1 \
libpq5 \
gettext \
&& rm -rf /var/lib/apt/lists/*
# Install only runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libpq5 \
libjpeg62-turbo \
zlib1g \
libcairo2 \
libpango-1.0-0 \
libgdk-pixbuf-xlib-2.0-0 \
libxml2 \
libxslt1.1 \
libffi8 \
curl \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create non-root user
RUN useradd --create-home --uid 1000 appuser
# Copy virtual environment from builder stage
COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
# Install built wheels
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
# Copy application code
COPY --chown=appuser:appuser . .
# Copy project source
COPY . .
# Copy entrypoint script
COPY --chown=appuser:appuser docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Ensure entrypoint is executable
RUN chmod +x /app/entrypoint.sh
# Create necessary directories and set permissions
RUN mkdir -p staticfiles media \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
# Entrypoint runs migrations, collectstatic, admin creation, and gunicorn
CMD ["sh", "./entrypoint.sh"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health/ || exit 1
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "horilla.wsgi:application", "--config", "docker/gunicorn.conf.py"]

View File

@@ -1,36 +0,0 @@
# Development Dockerfile for Horilla v2.0
# Includes compilers and dev tools, runs Django with autoreload.
FROM python:3.11-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Dev system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
libffi-dev \
libjpeg-dev \
zlib1g-dev \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libmagic1 \
libssl-dev \
gettext \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt ./
RUN python -m pip install --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
# Use Django dev server; compose binds volumes for live reload
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

View File

@@ -1,10 +0,0 @@
import multiprocessing
bind = "0.0.0.0:8000"
workers = max(2, multiprocessing.cpu_count() * 2 + 1)
worker_class = "gthread"
threads = 4
accesslog = "-"
errorlog = "-"
loglevel = "info"
keepalive = 120

View File

@@ -1,58 +0,0 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
server {
listen 80;
server_name _;
client_max_body_size 50m;
# Serve static files directly
location /static/ {
alias /staticfiles/;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Serve media files
location /media/ {
alias /media/;
add_header Cache-Control "public, max-age=3600";
}
# Proxy pass to Gunicorn
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_pass http://web:8000;
}
}
}

View File

@@ -1,42 +0,0 @@
name: horilla-dev
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/horilla
DEBUG: "true"
CREATE_SUPERUSER: "true"
volumes:
- ./:/app
- media:/app/media
command: sh ./entrypoint.sh
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: horilla
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5432:5432"
volumes:
- horilla-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
horilla-data:
media:

View File

@@ -1,54 +0,0 @@
name: horilla
services:
web:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/horilla
CREATE_SUPERUSER: "true"
command: sh ./entrypoint.sh
depends_on:
db:
condition: service_healthy
volumes:
- media:/app/media
- staticfiles:/app/staticfiles
db:
image: postgres:16
environment:
POSTGRES_DB: horilla
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
PGDATA: /var/lib/postgresql/data/pgdata
# Expose DB only within the compose network (no host port)
restart: unless-stopped
volumes:
- horilla-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
depends_on:
web:
condition: service_started
ports:
- "80:80"
volumes:
- ./deploy/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- staticfiles:/staticfiles:ro
- media:/media:ro
volumes:
horilla-data:
media:
staticfiles:

56
docker-compose.yml Normal file
View File

@@ -0,0 +1,56 @@
services:
web:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
- staticfiles:/app/staticfiles
- media:/app/media
environment:
- DEBUG=1
- SECRET_KEY=dev-secret-key
- ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
- CSRF_TRUSTED_ORIGINS=http://localhost:8000
- DATABASE_URL=postgres://horilla_user:horilla_pass@db:5432/horilla_db
- REDIS_URL=redis://:horilla_pass@redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: horilla_db
POSTGRES_USER: horilla_user
POSTGRES_PASSWORD: horilla_pass
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass horilla_pass
volumes:
- redis_data:/data
ports:
- "6379:6379"
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- staticfiles:/static:ro
- media:/media:ro
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- web
profiles: ["production"]
volumes:
staticfiles:
media:
postgres_data:
redis_data:

128
docker.md
View File

@@ -1,128 +0,0 @@
## Horilla v2.0 Docker Guide
This guide explains how to run Horilla with Docker in development and production (Nginx + Gunicorn).
### Prerequisites
- Docker 24+ and Docker Compose v2
- 2 CPU / 2 GB RAM minimum recommended
### Project layout (Docker-related)
- `Dockerfile`: Production multi-stage image (Python 3.11, slim)
- `Dockerfile.dev`: Development image with compilers and auto-reload
- `docker-compose.yaml`: Production stack (web + db + nginx)
- `docker-compose.dev.yaml`: Development stack (web + db) with live reload
- `deploy/nginx/nginx.conf`: Nginx reverse proxy config (serves `/static/` and `/media/`)
- `deploy/gunicorn.conf.py`: Gunicorn config for production
- `entrypoint.sh`: App lifecycle (migrate, collectstatic, start Gunicorn)
- `.dockerignore`: Reduces build context
## Development
### Start
```bash
# Build and run (autoreload)
docker compose -f docker-compose.dev.yaml up --build
# Open the app
open http://localhost:8000
```
### Useful dev commands
```bash
# Shell into the web container
docker compose -f docker-compose.dev.yaml exec web bash
# Run migrations
docker compose -f docker-compose.dev.yaml exec web python manage.py migrate
# Create admin user (example)
docker compose -f docker-compose.dev.yaml exec web \
python manage.py createhorillauser \
--first_name admin --last_name admin \
--username admin --password admin \
--email admin@example.com --phone 1234567890
```
## Production (Nginx + Gunicorn)
### Start
```bash
# Build and start in the background
docker compose up --build -d
# Open the app (served by Nginx)
open http://localhost
```
### Services
- `web`: Django app via Gunicorn on port 8000 (internal)
- `db`: PostgreSQL 16 (internal only)
- `nginx`: Reverse proxy on port 80; serves static and media directly
### Volumes
- `staticfiles`: `collectstatic` output served at `/static/`
- `media`: user uploads served at `/media/`
- `horilla-data`: Postgres data
### Environment variables
- `DATABASE_URL`: e.g. `postgres://postgres:postgres@db:5432/horilla`
- `DEBUG`: should be `false` in production
- `ALLOWED_HOSTS`: e.g. `["yourdomain.com"]`
- `CSRF_TRUSTED_ORIGINS`: e.g. `["https://yourdomain.com"]`
- `TIME_ZONE`: e.g. `Asia/Kolkata`
- `SECRET_KEY`: set a strong value (read from `.env` by Django)
Django reads `.env` from the project root via `django-environ`. Create one if needed.
### Common operations
```bash
# View logs
docker compose logs -f web
# Run management commands
docker compose exec web python manage.py migrate
docker compose exec web python manage.py collectstatic --noinput
# Create admin user
docker compose exec web \
python manage.py createhorillauser \
--first_name admin --last_name admin \
--username admin --password 'CHANGE_ME' \
--email admin@example.com --phone 1234567890
# Stop the stack
docker compose down
```
### TLS (HTTPS)
- Terminate TLS at Nginx. Provide your certs and update `deploy/nginx/nginx.conf` to listen on 443 and reference your certificate and key.
- Then publish `443:443` in the `nginx` service and set `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` accordingly.
### Database backup/restore
```bash
# Backup (inside db container)
docker compose exec db pg_dump -U postgres -d horilla > horilla_backup.sql
# Restore
cat horilla_backup.sql | docker compose exec -T db psql -U postgres -d horilla
```
## Troubleshooting
- Static files not loading: ensure `collectstatic` ran successfully; check `staticfiles` volume is mounted to Nginx.
- 502 from Nginx: check `web` logs and Gunicorn is listening on 0.0.0.0:8000.
- CSRF/host errors: set `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` for your domain.
- DB connection errors: confirm `DATABASE_URL` and that `db` service is healthy.
- Permissions on `media`: ensure Docker user can write; by default, containers run as root in this setup.
## Upgrades
```bash
# Rebuild images after changes to requirements or Dockerfiles
docker compose build --no-cache
# Apply migrations
docker compose exec web python manage.py migrate
# Reload Nginx (if config changed)
docker compose exec nginx nginx -s reload
```
## Notes
- Production images are based on Python 3.11 slim and use a multi-stage build for smaller, reliable artifacts.
- The database is not exposed to the host in production compose; connect from `web` or via `docker compose exec db psql`.
- For multi-host deployments, consider externalizing Postgres and object storage (e.g., S3) via `django-storages`.

20
docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -e
echo "Starting Horilla HR..."
# Wait for PostgreSQL to be ready
echo "Waiting for PostgreSQL..."
while ! nc -z db 5432; do
sleep 0.1
done
echo "PostgreSQL is ready!"
# Run migrations
python manage.py migrate --noinput
# Collect static files
python manage.py collectstatic --noinput
echo "Starting server..."
exec "$@"

45
docker/gunicorn.conf.py Normal file
View File

@@ -0,0 +1,45 @@
# Gunicorn configuration for Horilla-HR
# This file provides advanced configuration options for the WSGI server
import os
import multiprocessing
# Bind settings
bind = f"0.0.0.0:{os.environ.get('PORT', '8000')}"
host = "0.0.0.0"
port = int(os.environ.get('PORT', '8000'))
# Worker settings
workers = int(os.environ.get('GUNICORN_WORKERS', max(2, min(multiprocessing.cpu_count() * 2 + 1, 8))))
worker_class = "gthread"
threads = 4
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
preload_app = True
# Timeout settings
timeout = 120
keepalive = 5
# Logging
accesslog = "-"
errorlog = "-"
loglevel = os.environ.get('GUNICORN_LOG_LEVEL', 'info')
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = "horilla-hrms"
# Server mechanics
pidfile = "/tmp/gunicorn.pid"
user = None # Run as current user in container
group = None
tmp_upload_dir = None
# Development settings
reload = os.environ.get('GUNICORN_RELOAD', 'false').lower() == 'true'
# SSL settings (if needed)
# ssl_keyfile = os.environ.get('SSL_KEYFILE')
# ssl_certfile = os.environ.get('SSL_CERTFILE')

31
docker/nginx.conf Normal file
View File

@@ -0,0 +1,31 @@
events {
worker_connections 1024;
}
http {
upstream django {
server web:8000;
}
server {
listen 80;
client_max_body_size 50M;
location /static/ {
alias /static/;
expires 1y;
}
location /media/ {
alias /media/;
}
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@@ -1,17 +0,0 @@
#!/bin/sh
set -e
echo "Creating migrations..."
python3 manage.py makemigrations
echo "Applying database migrations..."
python3 manage.py migrate --noinput
echo "Collecting static files..."
python3 manage.py collectstatic --noinput
echo "Compiling translations..."
python3 manage.py compilemessages || true
echo "Starting Gunicorn..."
exec gunicorn -c ./deploy/gunicorn.conf.py horilla.wsgi:application

15844
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
{
"name": "openhrms-core",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev":"npm run development",
"development":"mix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ArjunCybro/OpenHRMS-Dashboard.git"
},
"author": "OpenHRMS",
"license": "ISC",
"bugs": {
"url": "https://github.com/ArjunCybro/OpenHRMS-Dashboard/issues"
},
"homepage": "https://github.com/ArjunCybro/OpenHRMS-Dashboard#readme",
"devDependencies": {
"laravel-mix": "^6.0.49"
},
"dependencies": {
"alpinejs": "^3.10.5",
"ionicons": "^7.1.0",
"jquery": "^3.6.3",
"jquery-ui": "^1.13.2",
"jquery-ui-touch-punch": "^0.2.3",
"js-datepicker": "^5.18.2",
"select2": "^4.1.0-rc.0",
"uuid": "^9.0.0"
}
}

View File

@@ -29,6 +29,7 @@ drf-yasg
et-xmlfile
geopy
google-api-python-client
google-auth-oauthlib
google-cloud-storage==3.0.0
html5lib
Jinja2