# 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](#frontend-asset-optimization) 2. [Django Production Configuration](#django-production-configuration) 3. [Static & Media Files](#static--media-files) 4. [Performance Optimization](#performance-optimization) 5. [Security Configuration](#security-configuration) 6. [Deployment Setup](#deployment-setup) 7. [Additional Services](#additional-services) --- ## Frontend Asset Optimization ### Current Setup (Development) Your `base.html` currently uses CDN links for: ```html ``` **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 ```bash npm install -D tailwindcss postcss autoprefixer ``` #### Step 2: Create Tailwind Configuration Create `kaauh_ats/tailwind.config.js`: ```javascript /** @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`: ```javascript module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ``` #### Step 4: Create Main CSS File Create `kaauh_ats/static/css/styles.css`: ```css @tailwind base; @tailwind components; @tailwind utilities; ``` #### Step 5: Build Tailwind CSS for Production Add to `kaauh_ats/package.json` scripts: ```json { "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: ```bash npm run build:css ``` #### Step 6: Update base.html Template **Replace:** ```html ``` **With:** ```html {% load static %} ``` --- ### 2. Lucide Icons Production Setup #### Step 1: Install Lucide Icons Locally ```bash npm install lucide ``` #### Step 2: Create Icons Bundle Script Create `kaauh_ats/static/js/build-icons.js`: ```javascript // 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`: ```json { "scripts": { "build:icons": "esbuild kaauh_ats/static/js/build-icons.js --bundle --outfile=kaauh_ats/static/js/icons.min.js --minify" } } ``` Install esbuild: ```bash npm install -D esbuild ``` Build: ```bash npm run build:icons ``` #### Step 4: Update base.html Template **Replace:** ```html ``` **With:** ```html {% load static %} ``` --- ### 3. HTMX Production Setup #### Step 1: Install HTMX Locally ```bash npm install htmx.org ``` #### Step 2: Copy HTMX to Static Files ```bash cp node_modules/htmx.org/dist/htmx.min.js kaauh_ats/static/js/ ``` Or add to build script: ```json { "scripts": { "copy:htmx": "cp node_modules/htmx.org/dist/htmx.min.js kaauh_ats/static/js/" } } ``` #### Step 3: Update base.html Template **Replace:** ```html ``` **With:** ```html {% load static %} ``` --- ### 4. Combined Build Process Update `package.json` with all build commands: ```json { "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: ```bash npm install ``` Build everything: ```bash npm run build ``` --- ## Django Production Configuration ### 1. Update Settings Create `kaauh_ats/NorahUniversity/settings_production.py`: ```python 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): ```bash # 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 ```bash pip install python-decouple gunicorn psycopg2-binary django-storages[boto3] ``` --- ## Static & Media Files ### 1. Collect Static Files ```bash cd kaauh_ats python manage.py collectstatic --noinput --clear ``` This creates optimized, versioned static files in `staticfiles/`. ### 2. Configure Nginx for Static Files ```nginx 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: ```python # 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`: ```python 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 ```python # 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 ```python # 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: ```bash pip install whitenoise[brotli] ``` Update `wsgi.py`: ```python 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 ```bash # Let's Encrypt (Free SSL) sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com ``` Auto-renewal: ```bash sudo certbot renew --dry-run sudo systemctl status certbot.timer ``` ### 2. Firewall Configuration ```bash # 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`: ```python 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: ```bash pip install gunicorn ``` Create Gunicorn systemd service: `/etc/systemd/system/gunicorn.service`: ```ini [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: ```bash sudo systemctl start gunicorn sudo systemctl enable gunicorn sudo systemctl status gunicorn ``` ### 2. Celery Worker Service `/etc/systemd/system/celery.service`: ```ini [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`: ```ini [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: ```bash sudo systemctl start celery celerybeat sudo systemctl enable celery celerybeat ``` ### 3. Redis Service ```bash sudo apt install redis-server sudo systemctl start redis sudo systemctl enable redis ``` ### 4. PostgreSQL Setup ```bash 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`: ```bash #!/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: ```bash chmod +x deploy.sh ``` --- ## Additional Services ### 1. Monitoring & Logging Install Sentry for error tracking: ```bash pip install sentry-sdk[django] ``` Configure in `settings_production.py`: ```python 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`: ```bash #!/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`: ```python 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:** ```bash python manage.py collectstatic --clear --noinput sudo systemctl reload nginx ``` ### Issue: Permission denied errors **Solution:** ```bash sudo chown -R www-data:www-data /var/www/kaauh_ats sudo chmod -R 755 /var/www/kaauh_ats ``` ### Issue: Database connection errors **Solution:** ```bash # 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:** ```bash # Check Celery logs sudo tail -f /var/log/celery/worker.log # Restart Celery sudo systemctl restart celery ``` --- ## Resources - [Django Production Deployment](https://docs.djangoproject.com/en/stable/howto/deployment/) - [Tailwind CSS Documentation](https://tailwindcss.com/docs/installation) - [Gunicorn Documentation](https://docs.gunicorn.org/) - [Nginx Documentation](https://nginx.org/en/docs/) - [Let's Encrypt](https://letsencrypt.org/) --- **Last Updated:** February 1, 2026 **Version:** 1.0.0