ATS/PRODUCTION_SETUP.md
2026-02-01 19:47:32 +03:00

21 KiB

Production Setup Guide for University ATS

This guide covers the complete setup for deploying the University ATS application to production, with special focus on frontend asset optimization.

Table of Contents

  1. Frontend Asset Optimization
  2. Django Production Configuration
  3. Static & Media Files
  4. Performance Optimization
  5. Security Configuration
  6. Deployment Setup
  7. Additional Services

Frontend Asset Optimization

Current Setup (Development)

Your base.html currently uses CDN links for:

<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>

This is NOT suitable for production because:

  • Depends on external CDNs (reliability & privacy concerns)
  • Slower initial page load
  • No optimization/minification control
  • Potential CSP violations
  • No offline capability

1. Tailwind CSS Production Setup

Step 1: Install Tailwind CSS Locally

npm install -D tailwindcss postcss autoprefixer

Step 2: Create Tailwind Configuration

Create kaauh_ats/tailwind.config.js:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./kaauh_ats/templates/**/*.html",
    "./kaauh_ats/templates/**/*.django",
    "./kaauh_ats/static/**/*.html",
  ],
  theme: {
    extend: {
      colors: {
        'temple-red': '#9d2235',
        'temple-dark': '#1a1a1a',
        'temple-cream': '#f8f7f2',
        'dashboard-blue': '#4e73df',
      }
    }
  },
  plugins: [],
}

Step 3: Create PostCSS Configuration

Create kaauh_ats/postcss.config.js:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Step 4: Create Main CSS File

Create kaauh_ats/static/css/styles.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 5: Build Tailwind CSS for Production

Add to kaauh_ats/package.json scripts:

{
  "scripts": {
    "build:css": "tailwindcss -i ./kaauh_ats/static/css/styles.css -o ./kaauh_ats/static/css/styles.min.css --minify",
    "watch:css": "tailwindcss -i ./kaauh_ats/static/css/styles.css -o ./kaauh_ats/static/css/styles.css --watch"
  }
}

Build command:

npm run build:css

Step 6: Update base.html Template

Replace:

<script src="https://cdn.tailwindcss.com"></script>
<script>
    tailwind.config = {
        theme: {
            extend: {
                colors: {
                    'temple-red': '#9d2235',
                    'temple-dark': '#1a1a1a',
                    'temple-cream': '#f8f7f2',
                    'dashboard-blue': '#4e73df',
                }
            }
        }
    }
</script>

With:

{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.min.css' %}">

2. Lucide Icons Production Setup

Step 1: Install Lucide Icons Locally

npm install lucide

Step 2: Create Icons Bundle Script

Create kaauh_ats/static/js/build-icons.js:

// Import only the icons you use (tree-shaking)
import { 
  createIcons, 
  LayoutGrid, 
  Briefcase, 
  Building2, 
  Users, 
  User, 
  Calendar, 
  Mail, 
  Globe, 
  Settings, 
  LogOut, 
  Menu, 
  PanelLeft, 
  Maximize, 
  Minimize, 
  X, 
  Tag, 
  Clock, 
  Timer, 
  Hash, 
  Lock, 
  Link, 
  DoorOpen, 
  MapPin, 
  Video,
  CheckCircle,
  AlertCircle,
  AlertTriangle,
  Info,
  Save
} from 'lucide';

// Register icons globally
createIcons({
  icons: {
    'layout-grid': LayoutGrid,
    'briefcase': Briefcase,
    'building-2': Building2,
    'users': Users,
    'user': User,
    'calendar': Calendar,
    'mail': Mail,
    'globe': Globe,
    'settings': Settings,
    'log-out': LogOut,
    'menu': Menu,
    'panel-left': PanelLeft,
    'maximize': Maximize,
    'minimize': Minimize,
    'x': X,
    'tag': Tag,
    'clock': Clock,
    'timer': Timer,
    'hash': Hash,
    'lock': Lock,
    'link': Link,
    'door-open': DoorOpen,
    'map-pin': MapPin,
    'video': Video,
    'check-circle': CheckCircle,
    'alert-circle': AlertCircle,
    'alert-triangle': AlertTriangle,
    'info': Info,
    'save': Save
  }
});

Step 3: Build Icons Bundle

Add to package.json:

{
  "scripts": {
    "build:icons": "esbuild kaauh_ats/static/js/build-icons.js --bundle --outfile=kaauh_ats/static/js/icons.min.js --minify"
  }
}

Install esbuild:

npm install -D esbuild

Build:

npm run build:icons

Step 4: Update base.html Template

Replace:

<script src="https://unpkg.com/lucide@latest"></script>

With:

{% load static %}
<script src="{% static 'js/icons.min.js' %}"></script>

3. HTMX Production Setup

Step 1: Install HTMX Locally

npm install htmx.org

Step 2: Copy HTMX to Static Files

cp node_modules/htmx.org/dist/htmx.min.js kaauh_ats/static/js/

Or add to build script:

{
  "scripts": {
    "copy:htmx": "cp node_modules/htmx.org/dist/htmx.min.js kaauh_ats/static/js/"
  }
}

Step 3: Update base.html Template

Replace:

<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>

With:

{% load static %}
<script src="{% static 'js/htmx.min.js' %}"></script>

4. Combined Build Process

Update package.json with all build commands:

{
  "name": "kaauh-ats",
  "version": "1.0.0",
  "scripts": {
    "build": "npm run build:css && npm run build:icons && npm run copy:htmx",
    "build:css": "tailwindcss -i ./kaauh_ats/static/css/styles.css -o ./kaauh_ats/static/css/styles.min.css --minify",
    "build:icons": "esbuild kaauh_ats/static/js/build-icons.js --bundle --outfile=kaauh_ats/static/js/icons.min.js --minify",
    "copy:htmx": "cp node_modules/htmx.org/dist/htmx.min.js kaauh_ats/static/js/",
    "watch": "npm run watch:css",
    "watch:css": "tailwindcss -i ./kaauh_ats/static/css/styles.css -o ./kaauh_ats/static/css/styles.css --watch"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.17",
    "esbuild": "^0.19.11",
    "htmx.org": "^1.9.10",
    "lucide": "^0.294.0",
    "postcss": "^8.4.33",
    "tailwindcss": "^3.4.1"
  }
}

Install all dependencies:

npm install

Build everything:

npm run build

Django Production Configuration

1. Update Settings

Create kaauh_ats/NorahUniversity/settings_production.py:

from .settings import *

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = [
    'yourdomain.com',
    'www.yourdomain.com',
    'api.yourdomain.com',
]

# Email Configuration
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', '587'))
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@yourdomain.com')

# Database Configuration
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
        'CONN_MAX_AGE': 600,
        'OPTIONS': {
            'sslmode': 'require',
        }
    }
}

# Static Files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

# Media Files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# Security Settings
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_PROXY_SSL_HEADER = 'HTTP_X_FORWARDED_PROTO'

# Redis Configuration
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': f"redis://{os.environ.get('REDIS_HOST', '127.0.0.1')}:{os.environ.get('REDIS_PORT', '6379')}/1",
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

# Celery Configuration
CELERY_BROKER_URL = f"redis://{os.environ.get('REDIS_HOST', '127.0.0.1')}:{os.environ.get('REDIS_PORT', '6379')}/0"
CELERY_RESULT_BACKEND = 'django-db'

# Logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '/var/log/django/django.log',
            'formatter': 'verbose',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

2. Environment Variables

Create .env file (do NOT commit to Git):

# Django
DJANGO_SECRET_KEY=your-super-secret-key-change-this
DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com

# Database
DB_NAME=your_database_name
DB_USER=your_database_user
DB_PASSWORD=your_database_password
DB_HOST=localhost
DB_PORT=5432

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379

# Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=noreply@yourdomain.com

# Social Login (LinkedIn)
LINKEDIN_CLIENT_ID=your-linkedin-client-id
LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret

# Zoom
ZOOM_API_KEY=your-zoom-api-key
ZOOM_API_SECRET=your-zoom-api-secret
ZOOM_WEBHOOK_API_KEY=your-webhook-api-key

3. Install Production Dependencies

pip install python-decouple gunicorn psycopg2-binary django-storages[boto3]

Static & Media Files

1. Collect Static Files

cd kaauh_ats
python manage.py collectstatic --noinput --clear

This creates optimized, versioned static files in staticfiles/.

2. Configure Nginx for Static Files

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    
    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;
    
    root /var/www/kaauh_ats;
    
    # SSL Configuration
    ssl_certificate /etc/ssl/certs/yourdomain.com.crt;
    ssl_certificate_key /etc/ssl/private/yourdomain.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # Static Files with Caching
    location /static/ {
        alias /var/www/kaauh_ats/staticfiles/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
        gzip_static on;
    }
    
    # Media Files
    location /media/ {
        alias /var/www/kaauh_ats/media/;
        expires 30d;
        add_header Cache-Control "public";
        access_log off;
        gzip_static on;
    }
    
    # Django Application
    location / {
        proxy_pass http://127.0.0.1:8000;
        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_redirect off;
    }
}

3. Media Files with S3 (Optional)

If using S3 for media storage:

# settings_production.py
import os

DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.environ.get('AWS_S3_REGION_NAME', 'us-east-1')
AWS_S3_ENDPOINT_URL = os.environ.get('AWS_S3_ENDPOINT_URL')
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
AWS_DEFAULT_ACL = None

Performance Optimization

1. Enable Gzip Compression

In settings_production.py:

MIDDLEWARE = [
    'django.middleware.gzip.GZipMiddleware',
    # ... other middleware
]

GZIP_CONTENT_TYPES = [
    'text/plain',
    'text/css',
    'text/xml',
    'text/javascript',
    'application/javascript',
    'application/json',
    'application/xml',
]

2. Browser Caching

# Cache configuration
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        },
        'TIMEOUT': 300,
        'OPTIONS': {
            'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
        }
    },
    'staticfiles': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/cache/django/staticfiles/',
    }
}

# Static files storage with manifest for cache busting
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'

3. Connection Pooling

# Database connection pooling
DATABASES['default']['OPTIONS'] = {
    'MAX_CONNS': 20,
    'MAX_CONN_AGE': 0,
    'CONN_MAX_AGE': 600,
}

4. Whitenoise for Static Files (Alternative)

Install Whitenoise:

pip install whitenoise[brotli]

Update wsgi.py:

from whitenoise.middleware import WhiteNoiseMiddleware
from django.core.wsgi import get_wsgi_application

application = get_wsgi_application()
application = WhiteNoiseMiddleware(application, static_root='/var/www/kaauh_ats/staticfiles/')

Security Configuration

1. SSL/TLS Setup

# Let's Encrypt (Free SSL)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Auto-renewal:

sudo certbot renew --dry-run
sudo systemctl status certbot.timer

2. Firewall Configuration

# UFW Firewall
sudo ufw allow 22/tcp    # SSH
sudo ufw allow 80/tcp    # HTTP
sudo ufw allow 443/tcp   # HTTPS
sudo ufw enable

3. Content Security Policy

Add to settings_production.py:

CSP_DEFAULT_SRC = ("'self'", "https://cdn.tailwindcss.com")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

Deployment Setup

1. Gunicorn WSGI Server

Install Gunicorn:

pip install gunicorn

Create Gunicorn systemd service:

/etc/systemd/system/gunicorn.service:

[Unit]
Description=gunicorn daemon for Kaauh ATS
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/kaauh_ats
ExecStart=/var/www/kaauh_ats/venv/bin/gunicorn \
          --workers 3 \
          --bind unix:/var/www/kaauh_ats/gunicorn.sock \
          --access-logfile /var/log/gunicorn/access.log \
          --error-logfile /var/log/gunicorn/error.log \
          --log-level info \
          --timeout 120 \
          --keepalive 5 \
          --max-requests 1000 \
          --max-requests-jitter 100 \
          NorahUniversity.wsgi:application

[Install]
WantedBy=multi-user.target

Start service:

sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn

2. Celery Worker Service

/etc/systemd/system/celery.service:

[Unit]
Description=Celery Worker
After=network.target

[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/www/kaauh_ats
Environment="PATH=/var/www/kaauh_ats/venv/bin"
ExecStart=/var/www/kaauh_ats/venv/bin/celery -A NorahUniversity worker \
           --loglevel=info \
           --logfile=/var/log/celery/worker.log \
           --pidfile=/var/run/celery/worker.pid \
           --concurrency=4

[Install]
WantedBy=multi-user.target

/etc/systemd/system/celerybeat.service:

[Unit]
Description=Celery Beat
After=network.target

[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/www/kaauh_ats
Environment="PATH=/var/www/kaauh_ats/venv/bin"
ExecStart=/var/www/kaauh_ats/venv/bin/celery -A NorahUniversity beat \
           --loglevel=info \
           --logfile=/var/log/celery/beat.log \
           --pidfile=/var/run/celery/beat.pid \
           --scheduler django_celery_beat.schedulers:DatabaseScheduler

[Install]
WantedBy=multi-user.target

Start services:

sudo systemctl start celery celerybeat
sudo systemctl enable celery celerybeat

3. Redis Service

sudo apt install redis-server
sudo systemctl start redis
sudo systemctl enable redis

4. PostgreSQL Setup

sudo apt install postgresql postgresql-contrib
sudo -u postgres psql

CREATE DATABASE kaauh_ats;
CREATE USER kaauh_ats_user WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE kaauh_ats TO kaauh_ats_user;
\q

5. Deployment Script

Create deploy.sh:

#!/bin/bash
set -e

echo "Starting deployment..."

# Pull latest code
cd /var/www/kaauh_ats
git pull origin main

# Activate virtual environment
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Install and build frontend assets
npm install
npm run build

# Run migrations
python manage.py migrate --noinput

# Collect static files
python manage.py collectstatic --noinput --clear

# Restart services
sudo systemctl restart gunicorn
sudo systemctl restart celery
sudo systemctl restart celerybeat

echo "Deployment completed successfully!"

Make executable:

chmod +x deploy.sh

Additional Services

1. Monitoring & Logging

Install Sentry for error tracking:

pip install sentry-sdk[django]

Configure in settings_production.py:

import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn=os.environ.get('SENTRY_DSN'),
    integrations=[DjangoIntegration()],
    traces_sample_rate=1.0,
    profiles_sample_rate=1.0,
)

2. Backup Strategy

Create backup script backup.sh:

#!/bin/bash
BACKUP_DIR="/var/backups/kaauh-ats"
DATE=$(date +%Y%m%d_%H%M%S)

# Backup Database
pg_dump -U kaauh_ats_user kaauh_ats > $BACKUP_DIR/db_$DATE.sql

# Backup Media Files
tar -czf $BACKUP_DIR/media_$DATE.tar.gz /var/www/kaauh_ats/media/

# Delete backups older than 30 days
find $BACKUP_DIR -type f -mtime +30 -delete

3. Health Checks

Create health check endpoint in urls.py:

from django.http import JsonResponse

def health_check(request):
    try:
        # Check database
        from django.db import connections
        connections['default'].cursor()
        
        # Check Redis
        from django.core.cache import cache
        cache.set('health_check', 'ok', 10)
        cache.get('health_check')
        
        return JsonResponse({'status': 'healthy'})
    except Exception as e:
        return JsonResponse({'status': 'unhealthy', 'error': str(e)}, status=503)

Pre-Deployment Checklist

  • All CDNs replaced with local assets
  • Tailwind CSS built and minified
  • Lucide icons bundled and minified
  • HTMX downloaded and minified
  • DEBUG = False in production settings
  • SECRET_KEY changed from development
  • ALLOWED_HOSTS configured
  • Database connection configured
  • Redis connection configured
  • Static files collected
  • SSL/TLS certificates installed
  • Firewall configured
  • Gunicorn service running
  • Celery workers running
  • Redis service running
  • Nginx configured
  • Environment variables set
  • Email backend configured
  • Logging configured
  • Monitoring setup (Sentry)
  • Backup strategy in place
  • Health check endpoint accessible
  • All tests passing

Troubleshooting

Issue: Static files not loading

Solution:

python manage.py collectstatic --clear --noinput
sudo systemctl reload nginx

Issue: Permission denied errors

Solution:

sudo chown -R www-data:www-data /var/www/kaauh_ats
sudo chmod -R 755 /var/www/kaauh_ats

Issue: Database connection errors

Solution:

# Check PostgreSQL is running
sudo systemctl status postgresql

# Check connection settings in .env
# Test connection
psql -h localhost -U kaauh_ats_user -d kaauh_ats

Issue: Celery tasks not executing

Solution:

# Check Celery logs
sudo tail -f /var/log/celery/worker.log

# Restart Celery
sudo systemctl restart celery

Resources


Last Updated: February 1, 2026 Version: 1.0.0