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
- Frontend Asset Optimization
- Django Production Configuration
- Static & Media Files
- Performance Optimization
- Security Configuration
- Deployment Setup
- 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
- Django Production Deployment
- Tailwind CSS Documentation
- Gunicorn Documentation
- Nginx Documentation
- Let's Encrypt
Last Updated: February 1, 2026 Version: 1.0.0