[UPDT] DOCKER: Standardise directory structure of Docker and related files for Horilla standards (#934)

This commit is contained in:
Horilla
2025-10-01 12:52:09 +05:30
committed by GitHub
parent 1df8e1137f
commit ffe9b70966
9 changed files with 184 additions and 273 deletions

View File

@@ -70,12 +70,20 @@ ENV/
node_modules/
# Git
.git/
.git
.gitignore
# Docker
Dockerfile*
README.md
Dockerfile
.dockerignore
# Migrations cache
**/migrations/__pycache__/
docker-compose.yml
.vscode
.idea
*.pyc
__pycache__
.pytest_cache
.coverage
*.log
.env
horillavenv
node_modules
.DS_Store

View File

@@ -1,77 +1,49 @@
# syntax=docker/dockerfile:1
FROM python:3.12-slim
# -------- Base image with dependencies layer --------
FROM python:3.11-slim AS base
# System deps
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
POETRY_VIRTUALENVS_CREATE=false
PYTHONUNBUFFERED=1
# Install build deps for common Python packages (incl. cairo)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
gcc \
curl \
pkg-config \
libcairo2-dev \
libpango1.0-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libxml2-dev \
libxslt1-dev \
# Install system 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 \
curl \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# -------- Builder for wheels --------
FROM base AS builder
COPY requirements.txt ./
RUN pip wheel --wheel-dir /wheels -r requirements.txt
# -------- Final runtime image --------
FROM python:3.11-slim AS runtime
# Create non-root user
RUN addgroup --system app && adduser --system --ingroup app app
# Install runtime deps for libraries like psycopg2, cairo, etc. (minimal)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
libcairo2 \
libpango-1.0-0 \
libjpeg62-turbo \
zlib1g \
libxml2 \
libxslt1.1 \
libffi8 \
libfreetype6 \
ghostscript \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --create-home --uid 1000 appuser
WORKDIR /app
# Copy wheels and install
COPY --from=builder /wheels /wheels
RUN pip install --no-index --find-links=/wheels /wheels/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn psycopg2-binary
# Copy project
# Copy application
COPY . .
# Ensure static dirs exist and are owned by app
RUN mkdir -p /app/staticfiles /app/static_root && chown -R app:app /app
# Set permissions
RUN mkdir -p staticfiles media \
&& chown -R appuser:appuser /app
# Gunicorn config
ENV PORT=8000 \
GUNICORN_CMD_ARGS="--config deploy/gunicorn.conf.py"
# Entrypoint
COPY deploy/entrypoint.sh /entrypoint.sh
# Copy entrypoint
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER app
USER appuser
EXPOSE 8000
CMD ["/entrypoint.sh"]
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "horilla.wsgi:application", "--config", "docker/gunicorn.conf.py"]

View File

@@ -1,12 +0,0 @@
#!/bin/sh
set -euo pipefail
# Default envs
: "${DJANGO_SETTINGS_MODULE:=horilla.settings}"
: "${PORT:=8000}"
: "${DATABASE_URL:=}"
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec gunicorn horilla.wsgi:application

View File

@@ -1,38 +0,0 @@
import multiprocessing
import os
# Server socket
bind = "0.0.0.0:8000"
backlog = 2048
# Worker processes
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
timeout = 120
keepalive = 5
# Logging
accesslog = "-"
errorlog = "-"
loglevel = os.getenv("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
# SSL (if needed)
# keyfile = "/path/to/keyfile"
# certfile = "/path/to/certfile"

View File

@@ -1,118 +0,0 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
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;
client_max_body_size 50M;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied any;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/x-javascript
application/xml+rss
application/javascript
application/json;
server_tokens off;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# Upstream for load balancing (if needed)
upstream app {
server web:8000 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name _;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Serve static files directly
location /static/ {
alias /staticfiles/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Serve media files
location /media/ {
alias /media/;
expires 30d;
add_header Cache-Control "public";
access_log off;
}
# API rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
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;
}
# Login rate limiting
location ~ ^/(login|auth)/ {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
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;
}
# Main application
location / {
proxy_pass http://app;
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_redirect off;
}
}
}

View File

@@ -1,53 +1,56 @@
version: "3.9"
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: ${POSTGRES_DB:-horilla}
POSTGRES_USER: ${POSTGRES_USER:-horilla}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-horilla}
POSTGRES_DB: horilla_db
POSTGRES_USER: horilla_user
POSTGRES_PASSWORD: horilla_pass
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-horilla}
command: redis-server --appendonly yes --requirepass horilla_pass
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
timeout: 3s
retries: 5
web:
build:
context: .
command: /entrypoint.sh
environment:
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-horilla.settings}
SECRET_KEY: ${SECRET_KEY:-change-me}
DEBUG: ${DEBUG:-False}
ALLOWED_HOSTS: ${ALLOWED_HOSTS:-*}
DATABASE_URL: postgresql://${POSTGRES_USER:-horilla}:${POSTGRES_PASSWORD:-horilla}@db:5432/${POSTGRES_DB:-horilla}
REDIS_URL: redis://:${REDIS_PASSWORD:-horilla}@redis:6379/0
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
- "6379:6379"
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- static_data:/app/staticfiles
- staticfiles:/static:ro
- media:/media:ro
- ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- web
profiles: ["production"]
volumes:
db_data:
staticfiles:
media:
postgres_data:
redis_data:
static_data:

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