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

1011 lines
21 KiB
Markdown

# 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
<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
```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
<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:**
```html
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.min.css' %}">
```
---
### 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
<script src="https://unpkg.com/lucide@latest"></script>
```
**With:**
```html
{% load static %}
<script src="{% static 'js/icons.min.js' %}"></script>
```
---
### 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
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
```
**With:**
```html
{% load static %}
<script src="{% static 'js/htmx.min.js' %}"></script>
```
---
### 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