Deployment Guide#

This guide covers production deployment options for the Nopayloaddb service.

Production Considerations#

Security Requirements#

Environment Variables

Never use default values in production:

# Required production settings
export SECRET_KEY='your-very-secure-secret-key-here'
export DEBUG=False
export DJANGO_LOGPATH='/var/log/nopayloaddb'

# Database credentials
export POSTGRES_DB_W=nopayloaddb_prod
export POSTGRES_USER_W=npdb_prod
export POSTGRES_PASSWORD_W='secure-password'
export POSTGRES_HOST_W=db.example.com
export POSTGRES_PORT_W=5432

Django Settings

Ensure production settings in nopayloaddb/settings.py:

# Security settings
DEBUG = False
ALLOWED_HOSTS = ['your-domain.com', 'api.example.com']

# Use secure cookies
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# Enable authentication if required
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

HTTPS/TLS Configuration

Always use HTTPS in production:

  • Configure SSL/TLS certificates

  • Use secure headers

  • Implement proper certificate management

Container Deployment#

Docker Production Setup#

Production Dockerfile

FROM python:3.9.16-slim

# Security updates
RUN apt-get update && apt-get upgrade -y && \
    apt-get install -y --no-install-recommends \
    libpq-dev gcc && \
    rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN groupadd -r npdb && useradd -r -g npdb npdb

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application
COPY . .
RUN chown -R npdb:npdb /app

USER npdb

# Production command
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "nopayloaddb.wsgi:application"]

Production Docker Compose

version: '3.8'

services:
  db:
    image: postgres:13
    environment:
      - POSTGRES_DB=${POSTGRES_DB_W}
      - POSTGRES_USER=${POSTGRES_USER_W}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD_W}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backup:/backup
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER_W}"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    build:
      context: .
      dockerfile: Dockerfile.prod
    depends_on:
      db:
        condition: service_healthy
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=False
      - POSTGRES_HOST_W=db
    volumes:
      - static_files:/app/static
      - media_files:/app/media
      - logs:/var/log/nopayloaddb
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    depends_on:
      - app
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_files:/static
      - ./ssl:/etc/nginx/ssl
    restart: unless-stopped

volumes:
  postgres_data:
  static_files:
  media_files:
  logs:

Kubernetes Deployment#

Namespace and ConfigMap

apiVersion: v1
kind: Namespace
metadata:
  name: nopayloaddb

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nopayloaddb-config
  namespace: nopayloaddb
data:
  DEBUG: "False"
  DJANGO_LOGPATH: "/var/log/nopayloaddb"
  POSTGRES_HOST_W: "postgresql"
  POSTGRES_PORT_W: "5432"
  POSTGRES_DB_W: "nopayloaddb"

Secrets

apiVersion: v1
kind: Secret
metadata:
  name: nopayloaddb-secrets
  namespace: nopayloaddb
type: Opaque
data:
  SECRET_KEY: <base64-encoded-secret>
  POSTGRES_USER_W: <base64-encoded-username>
  POSTGRES_PASSWORD_W: <base64-encoded-password>

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nopayloaddb
  namespace: nopayloaddb
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nopayloaddb
  template:
    metadata:
      labels:
        app: nopayloaddb
    spec:
      containers:
      - name: nopayloaddb
        image: nopayloaddb:latest
        ports:
        - containerPort: 8000
        envFrom:
        - configMapRef:
            name: nopayloaddb-config
        - secretRef:
            name: nopayloaddb-secrets
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /health/
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready/
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5

Service and Ingress

apiVersion: v1
kind: Service
metadata:
  name: nopayloaddb-service
  namespace: nopayloaddb
spec:
  selector:
    app: nopayloaddb
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nopayloaddb-ingress
  namespace: nopayloaddb
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - api.example.com
    secretName: nopayloaddb-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nopayloaddb-service
            port:
              number: 80

Helm Charts Deployment#

Note

Official Helm Charts: Nopayloaddb provides official Helm charts for production deployments on Kubernetes and OpenShift clusters. These charts are actively maintained and include configurations for different HEP experiments.

Repository: BNLNPPS/nopayloaddb-charts

The Helm charts repository provides pre-configured deployment templates for:

  • sPHENIX experiment: npdbchart_sphenix/

  • Belle2 Java backend: npdbchart_belle2_java/

Quick Start with Helm Charts

# Clone the charts repository
git clone https://github.com/BNLNPPS/nopayloaddb-charts.git
cd nopayloaddb-charts

For sPHENIX Deployment:

# Copy your configuration values
cp /path/to/your/values_sphenix.yaml npdbchart_sphenix/values.yaml

# Login to your cluster
oc login --token='YOUR_TOKEN'
oc project your-project-name

# Deploy or upgrade
helm upgrade --install sphenix-npdb npdbchart_sphenix/

# Check deployment status
oc get pods
helm list

For Belle2 Java Deployment:

# Copy your configuration values
cp /path/to/your/values_belle2-java.yaml npdbchart_belle2_java/values.yaml

# Deploy or upgrade
helm upgrade --install belle2-npdb npdbchart_belle2_java/

Helm Chart Configuration

The Helm charts support comprehensive configuration through values.yaml:

# Example values.yaml structure
image:
  repository: ghcr.io/plexoos/npdb
  tag: latest
  pullPolicy: Always

service:
  type: ClusterIP
  port: 8000

ingress:
  enabled: true
  annotations:
    kubernetes.io/ingress.class: nginx
  hosts:
    - host: npdb.example.com
      paths:
        - path: /
          pathType: Prefix

postgresql:
  enabled: true
  auth:
    postgresPassword: "secure-password"
    database: nopayloaddb

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 256Mi

Monitoring and Troubleshooting with Helm

# Monitor deployment
helm status your-release-name

# Get deployment logs
oc logs deployment/nopayloaddb

# Debug issues
oc describe pod <pod-name>
oc get events --sort-by='.metadata.creationTimestamp'

# Restart deployment (delete pod to force restart)
oc delete pod <pod-name>

Advantages of Helm Charts

  • Production-Ready: Pre-configured with best practices

  • Experiment-Specific: Tailored configurations for different HEP experiments

  • Version Management: Easy rollbacks and upgrades

  • Configuration Management: Centralized values management

  • Integration: Seamless OpenShift/Kubernetes integration

OpenShift Deployment#

Using the Provided Template

# Login to OpenShift
oc login https://your-openshift-cluster.com

# Create or select project
oc new-project nopayloaddb-prod

# Create template
oc create -f npdb_openshift_template.yaml

# Deploy application
oc new-app --template=npdb \
  -p DATABASE_SERVICE_NAME=postgresql \
  -p DATABASE_NAME=nopayloaddb \
  -p DATABASE_USER=npdb \
  -p DATABASE_PASSWORD=secure-password \
  -p SECRET_KEY=your-secure-secret-key

Custom OpenShift Configuration

apiVersion: template.openshift.io/v1
kind: Template
metadata:
  name: nopayloaddb-template
objects:
- apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: nopayloaddb
  spec:
    replicas: 2
    selector:
      matchLabels:
        app: nopayloaddb
    template:
      metadata:
        labels:
          app: nopayloaddb
      spec:
        containers:
        - name: nopayloaddb
          image: ghcr.io/plexoos/npdb:latest
          env:
          - name: SECRET_KEY
            valueFrom:
              secretKeyRef:
                name: nopayloaddb-secrets
                key: secret-key
          - name: POSTGRES_HOST_W
            value: postgresql
          ports:
          - containerPort: 8000
parameters:
- name: SECRET_KEY
  description: Django secret key
  required: true
- name: DATABASE_PASSWORD
  description: Database password
  required: true

Traditional Deployment#

WSGI Server Setup#

Using Gunicorn

# Install Gunicorn
pip install gunicorn

# Create Gunicorn configuration
cat > gunicorn.conf.py << 'EOF'
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
timeout = 30
keepalive = 2
user = "npdb"
group = "npdb"
preload_app = True

# Logging
accesslog = "/var/log/nopayloaddb/access.log"
errorlog = "/var/log/nopayloaddb/error.log"
loglevel = "info"

# Process naming
proc_name = "nopayloaddb"

# Worker recycling
max_requests = 1000
max_requests_jitter = 50
EOF

# Start Gunicorn
gunicorn --config gunicorn.conf.py nopayloaddb.wsgi:application

Using uWSGI

# Install uWSGI
pip install uwsgi

# Create uWSGI configuration
cat > uwsgi.ini << 'EOF'
[uwsgi]
module = nopayloaddb.wsgi:application
master = true
processes = 4
socket = /tmp/uwsgi.sock
chmod-socket = 666
vacuum = true
die-on-term = true
logto = /var/log/nopayloaddb/uwsgi.log
EOF

# Start uWSGI
uwsgi --ini uwsgi.ini

Reverse Proxy Configuration#

Nginx Configuration

upstream nopayloaddb {
    server 127.0.0.1:8000;
    # Add more servers for load balancing
    # server 127.0.0.1:8001;
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Client max body size
    client_max_body_size 10M;

    # Compression
    gzip on;
    gzip_vary on;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/json;

    location / {
        proxy_pass http://nopayloaddb;
        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;

        # Timeouts
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }

    location /static/ {
        alias /var/www/nopayloaddb/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location /health/ {
        access_log off;
        proxy_pass http://nopayloaddb;
    }
}

Apache Configuration

<VirtualHost *:80>
    ServerName api.example.com
    Redirect permanent / https://api.example.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName api.example.com

    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/api.example.com.crt
    SSLCertificateKeyFile /etc/ssl/private/api.example.com.key

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Frame-Options DENY
    Header always set X-Content-Type-Options nosniff

    # Proxy configuration
    ProxyPreserveHost On
    ProxyRequests Off

    ProxyPass /static/ !
    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/

    # Static files
    Alias /static /var/www/nopayloaddb/static
    <Directory /var/www/nopayloaddb/static>
        Require all granted
    </Directory>
</VirtualHost>

Database Setup#

PostgreSQL Configuration#

Production Database Setup

-- Create production database
CREATE DATABASE nopayloaddb_prod;

-- Create users
CREATE USER npdb_write WITH PASSWORD 'secure-write-password';
CREATE USER npdb_read WITH PASSWORD 'secure-read-password';

-- Grant permissions
GRANT ALL PRIVILEGES ON DATABASE nopayloaddb_prod TO npdb_write;
GRANT CONNECT ON DATABASE nopayloaddb_prod TO npdb_read;

-- Connect to database
\\c nopayloaddb_prod

-- Grant schema permissions
GRANT USAGE ON SCHEMA public TO npdb_read;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO npdb_read;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO npdb_read;

-- Set default privileges
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO npdb_read;

Database Optimization

-- Analyze tables for query optimization
ANALYZE;

-- Create additional indexes if needed
CREATE INDEX idx_payloadiov_major_minor ON "PayloadIOV" (major_iov, minor_iov);
CREATE INDEX idx_payloadlist_gt_type ON "PayloadList" (global_tag_id, payload_type_id);

-- Vacuum and analyze regularly
VACUUM ANALYZE;

Read Replicas Configuration

# On primary server
echo "wal_level = replica" >> /etc/postgresql/13/main/postgresql.conf
echo "max_wal_senders = 3" >> /etc/postgresql/13/main/postgresql.conf
echo "wal_keep_segments = 64" >> /etc/postgresql/13/main/postgresql.conf

# Add replica user
echo "host replication replica_user replica_ip/32 md5" >> /etc/postgresql/13/main/pg_hba.conf

# Restart PostgreSQL
systemctl restart postgresql

Monitoring and Logging#

Application Monitoring#

Health Check Endpoint

# Add to urls.py
from django.http import JsonResponse
from django.db import connection

def health_check(request):
    try:
        cursor = connection.cursor()
        cursor.execute("SELECT 1")
        return JsonResponse({
            'status': 'healthy',
            'database': 'connected',
            'timestamp': timezone.now().isoformat()
        })
    except Exception as e:
        return JsonResponse({
            'status': 'unhealthy',
            'error': str(e),
            'timestamp': timezone.now().isoformat()
        }, status=500)

Prometheus Metrics

# Install django-prometheus
pip install django-prometheus

# Add to INSTALLED_APPS
INSTALLED_APPS = [
    'django_prometheus',
    # ... other apps
]

# Add to MIDDLEWARE
MIDDLEWARE = [
    'django_prometheus.middleware.PrometheusBeforeMiddleware',
    # ... other middleware
    'django_prometheus.middleware.PrometheusAfterMiddleware',
]

Logging Configuration

# Production logging settings
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'handlers': {
        'file': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '/var/log/nopayloaddb/django.log',
            'maxBytes': 1024*1024*50,  # 50MB
            'backupCount': 5,
            'formatter': 'verbose',
        },
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file', 'console'],
            'level': 'INFO',
            'propagate': True,
        },
        'cdb_rest': {
            'handlers': ['file', 'console'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}

Database Monitoring#

PostgreSQL Monitoring

-- Monitor active connections
SELECT count(*) FROM pg_stat_activity;

-- Monitor query performance
SELECT query, mean_time, calls FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 10;

-- Monitor database size
SELECT pg_size_pretty(pg_database_size('nopayloaddb_prod'));

-- Monitor table sizes
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables WHERE schemaname = 'public' ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

Backup and Recovery#

Database Backups#

Automated Backup Script

#!/bin/bash

# Configuration
DB_NAME="nopayloaddb_prod"
DB_USER="npdb_write"
BACKUP_DIR="/backup/nopayloaddb"
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup directory
mkdir -p $BACKUP_DIR

# Create database backup
pg_dump -h localhost -U $DB_USER -d $DB_NAME -f "$BACKUP_DIR/nopayloaddb_$DATE.sql"

# Compress backup
gzip "$BACKUP_DIR/nopayloaddb_$DATE.sql"

# Remove old backups (keep last 7 days)
find $BACKUP_DIR -name "*.gz" -mtime +7 -delete

# Verify backup
if [ -f "$BACKUP_DIR/nopayloaddb_$DATE.sql.gz" ]; then
    echo "Backup successful: nopayloaddb_$DATE.sql.gz"
else
    echo "Backup failed"
    exit 1
fi

Cron Job for Automated Backups

# Add to crontab
0 2 * * * /usr/local/bin/backup_nopayloaddb.sh

Backup Verification

#!/bin/bash

# Test backup restoration
gunzip -c /backup/nopayloaddb/nopayloaddb_latest.sql.gz | psql -h localhost -U npdb_write -d nopayloaddb_test

Disaster Recovery#

Recovery Procedure

# 1. Stop application
systemctl stop nopayloaddb

# 2. Restore database
createdb nopayloaddb_prod_restored
gunzip -c /backup/nopayloaddb/nopayloaddb_YYYYMMDD.sql.gz | psql -h localhost -U npdb_write -d nopayloaddb_prod_restored

# 3. Verify data integrity
psql -h localhost -U npdb_write -d nopayloaddb_prod_restored -c "SELECT COUNT(*) FROM \"GlobalTag\";"

# 4. Update configuration to use restored database
# 5. Start application
systemctl start nopayloaddb

High Availability Setup

# Configure PostgreSQL streaming replication
# Primary server configuration
echo "hot_standby = on" >> /etc/postgresql/13/main/postgresql.conf
echo "wal_level = replica" >> /etc/postgresql/13/main/postgresql.conf
echo "max_wal_senders = 3" >> /etc/postgresql/13/main/postgresql.conf

# Standby server configuration
echo "hot_standby = on" >> /etc/postgresql/13/main/postgresql.conf
echo "primary_conninfo = 'host=primary_server port=5432 user=replication'" >> /etc/postgresql/13/main/recovery.conf

Performance Optimization#

Application Performance#

Django Optimization

# settings.py optimizations

# Database connection pooling
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'OPTIONS': {
            'MAX_CONNS': 20,
            'MIN_CONNS': 5,
        },
    }
}

# Caching
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
    }
}

# Session optimization
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

Load Balancing

upstream nopayloaddb {
    least_conn;
    server 127.0.0.1:8000 weight=3;
    server 127.0.0.1:8001 weight=2;
    server 127.0.0.1:8002 weight=1;
}

Security Hardening#

System Security#

Firewall Configuration

# Allow only necessary ports
ufw allow 22/tcp   # SSH
ufw allow 80/tcp   # HTTP
ufw allow 443/tcp  # HTTPS
ufw allow 5432/tcp from 10.0.0.0/8  # PostgreSQL (internal only)
ufw enable

SSL/TLS Configuration

# Strong SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

Regular Security Updates

# Automated security updates
echo 'Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};' > /etc/apt/apt.conf.d/50unattended-upgrades

systemctl enable unattended-upgrades