Files
cmdb-insight/docs/PRODUCTION-DEPLOYMENT.md
Bert Hausmans 57e4adc69c Remove JIRA_SCHEMA_ID from entire application
- Remove JIRA_SCHEMA_ID from all documentation, config files, and scripts
- Update generate-schema.ts to always auto-discover schemas dynamically
- Runtime application already discovers schemas via /objectschema/list API
- Build script now automatically selects schema with most objects
- Remove JIRA_SCHEMA_ID from docker-compose.yml, Azure setup scripts, and all docs
- Application is now fully schema-agnostic and discovers schemas automatically
2026-01-22 22:56:29 +01:00

18 KiB

Productie Deployment Guide

Deze guide beschrijft hoe je de CMDB Insight applicatie veilig en betrouwbaar in productie kunt draaien.

📋 Inhoudsopgave

  1. Security Best Practices
  2. Production Docker Setup
  3. Environment Configuratie
  4. Reverse Proxy (Nginx)
  5. SSL/TLS Certificaten
  6. Database & Cache
  7. Session Management
  8. Monitoring & Logging
  9. Backup Strategie
  10. Deployment Checklist

🔒 Security Best Practices

1. Environment Variabelen

ALTIJD gebruik een .env bestand dat NIET in git staat:

# .env (niet committen!)
JIRA_HOST=https://jira.zuyderland.nl
JIRA_AUTH_METHOD=oauth  # of 'pat'
JIRA_OAUTH_CLIENT_ID=your-client-id
JIRA_OAUTH_CLIENT_SECRET=your-client-secret
JIRA_OAUTH_CALLBACK_URL=https://cmdb.zuyderland.nl/api/auth/callback
SESSION_SECRET=<generate-random-64-char-string>
ANTHROPIC_API_KEY=your-key
NODE_ENV=production
PORT=3001
FRONTEND_URL=https://cmdb.zuyderland.nl
JIRA_API_BATCH_SIZE=15

Genereer een veilige SESSION_SECRET:

openssl rand -hex 32

2. Secrets Management

Gebruik een secrets management systeem:

  • Kubernetes: Secrets
  • Docker Swarm: Docker Secrets
  • Cloud: AWS Secrets Manager, Azure Key Vault, Google Secret Manager
  • On-premise: HashiCorp Vault

3. Network Security

  • Firewall: Alleen poorten 80/443 open voor reverse proxy
  • Internal Network: Backend alleen bereikbaar via reverse proxy
  • VPN: Overweeg VPN voor interne toegang

🐳 Production Docker Setup

Backend Production Dockerfile

Maak backend/Dockerfile.prod:

FROM node:20-alpine AS builder

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy source
COPY . .

# Build TypeScript
RUN npm run build

# Production stage
FROM node:20-alpine

WORKDIR /app

# Install only production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy built files
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/src/generated ./src/generated

# Create data directory with proper permissions
RUN mkdir -p /app/data && chown -R node:node /app/data

# Switch to non-root user
USER node

# Expose port
EXPOSE 3001

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# Start production server
CMD ["node", "dist/index.js"]

Frontend Production Dockerfile

Maak frontend/Dockerfile.prod:

FROM node:20-alpine AS builder

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Copy source and build
COPY . .
RUN npm run build

# Production stage with nginx
FROM nginx:alpine

# Copy built files
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Expose port
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1

CMD ["nginx", "-g", "daemon off;"]

Frontend Nginx Config

Maak frontend/nginx.conf:

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Cache static assets
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # API proxy
    location /api {
        proxy_pass http://backend:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;
    }

    # SPA routing
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Production Docker Compose

Maak docker-compose.prod.yml:

version: '3.8'

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
    environment:
      - NODE_ENV=production
      - PORT=3001
    env_file:
      - .env.production
    volumes:
      - backend_data:/app/data
    restart: unless-stopped
    networks:
      - internal
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.prod
    depends_on:
      - backend
    restart: unless-stopped
    networks:
      - internal
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - nginx_cache:/var/cache/nginx
    depends_on:
      - frontend
      - backend
    restart: unless-stopped
    networks:
      - internal

volumes:
  backend_data:
  nginx_cache:

networks:
  internal:
    driver: bridge

🌐 Reverse Proxy (Nginx)

Main Nginx Config

Maak nginx/nginx.conf:

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 10M;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
    limit_req_zone $binary_remote_addr zone=general_limit:10m rate=200r/m;

    # SSL Configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    # Upstream backend
    upstream backend {
        server backend:3001;
        keepalive 32;
    }

    # Upstream frontend
    upstream frontend {
        server frontend:80;
    }

    # HTTP to HTTPS redirect
    server {
        listen 80;
        server_name cmdb.zuyderland.nl;
        return 301 https://$server_name$request_uri;
    }

    # HTTPS server
    server {
        listen 443 ssl http2;
        server_name cmdb.zuyderland.nl;

        # SSL certificates
        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        # Security headers
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://jira.zuyderland.nl;" always;

        # API routes with rate limiting
        location /api {
            limit_req zone=api_limit burst=20 nodelay;
            
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            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_set_header X-Forwarded-Host $host;
            proxy_cache_bypass $http_upgrade;
            
            # Timeouts
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 300s;
        }

        # Frontend
        location / {
            limit_req zone=general_limit burst=50 nodelay;
            
            proxy_pass http://frontend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            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_cache_bypass $http_upgrade;
        }

        # Health check (no rate limit)
        location /health {
            access_log off;
            proxy_pass http://backend/health;
        }
    }
}

🔐 SSL/TLS Certificaten

Let's Encrypt (Gratis)

# Install certbot
sudo apt-get update
sudo apt-get install certbot

# Generate certificate
sudo certbot certonly --standalone -d cmdb.zuyderland.nl

# Certificates worden opgeslagen in:
# /etc/letsencrypt/live/cmdb.zuyderland.nl/fullchain.pem
# /etc/letsencrypt/live/cmdb.zuyderland.nl/privkey.pem

# Auto-renewal
sudo certbot renew --dry-run

Certificaten kopiëren naar Docker volume:

# Maak directory
mkdir -p nginx/ssl

# Kopieer certificaten (of gebruik bind mount)
cp /etc/letsencrypt/live/cmdb.zuyderland.nl/fullchain.pem nginx/ssl/
cp /etc/letsencrypt/live/cmdb.zuyderland.nl/privkey.pem nginx/ssl/

💾 Database & Cache

SQLite Cache Database

De cache database wordt opgeslagen in /app/data/cmdb-cache.db.

Backup strategie:

# Dagelijkse backup script
#!/bin/bash
BACKUP_DIR="/backups/cmdb"
DATE=$(date +%Y%m%d_%H%M%S)
docker exec cmdb-gui_backend_1 sqlite3 /app/data/cmdb-cache.db ".backup '$BACKUP_DIR/cmdb-cache-$DATE.db'"
# Bewaar laatste 30 dagen
find $BACKUP_DIR -name "cmdb-cache-*.db" -mtime +30 -delete

Volume backup:

# Backup hele volume
docker run --rm -v cmdb-gui_backend_data:/data -v $(pwd)/backups:/backup \
  alpine tar czf /backup/backend-data-$(date +%Y%m%d).tar.gz /data

🔑 Session Management

Huidige Implementatie

De applicatie gebruikt momenteel in-memory session storage. Voor productie is dit NIET geschikt.

Aanbeveling: Redis voor Sessions

  1. Voeg Redis toe aan docker-compose.prod.yml:
  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - internal
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 30s
      timeout: 3s
      retries: 3

volumes:
  redis_data:
  1. Update authService.ts om Redis te gebruiken:
import Redis from 'ioredis';

const redis = new Redis({
  host: process.env.REDIS_HOST || 'redis',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
});

// Vervang sessionStore met Redis calls
async function setSession(sessionId: string, session: UserSession): Promise<void> {
  const ttl = Math.floor((session.expiresAt - Date.now()) / 1000);
  await redis.setex(`session:${sessionId}`, ttl, JSON.stringify(session));
}

async function getSession(sessionId: string): Promise<UserSession | null> {
  const data = await redis.get(`session:${sessionId}`);
  return data ? JSON.parse(data) : null;
}

📊 Monitoring & Logging

1. Logging

De applicatie gebruikt Winston. Configureer log rotation:

// backend/src/services/logger.ts
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new DailyRotateFile({
      filename: 'logs/application-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '14d',
    }),
    new DailyRotateFile({
      filename: 'logs/error-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      level: 'error',
      maxSize: '20m',
      maxFiles: '30d',
    }),
  ],
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

2. Health Checks

De applicatie heeft al een /health endpoint. Monitor dit:

# Monitoring script
#!/bin/bash
HEALTH_URL="https://cmdb.zuyderland.nl/health"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" $HEALTH_URL)

if [ $STATUS -ne 200 ]; then
  # Alert via email/Slack/etc
  echo "Health check failed: $STATUS"
  exit 1
fi

3. Application Monitoring

Overweeg:

  • Prometheus + Grafana: Metrics en dashboards
  • Sentry: Error tracking
  • New Relic / Datadog: APM (Application Performance Monitoring)

💾 Backup Strategie

1. Database Backups

#!/bin/bash
# backup.sh - Run dagelijks via cron

BACKUP_DIR="/backups/cmdb"
DATE=$(date +%Y%m%d_%H%M%S)

# Backup SQLite database
docker exec cmdb-gui_backend_1 sqlite3 /app/data/cmdb-cache.db \
  ".backup '$BACKUP_DIR/cmdb-cache-$DATE.db'"

# Compress
gzip "$BACKUP_DIR/cmdb-cache-$DATE.db"

# Upload naar remote storage (optioneel)
# aws s3 cp "$BACKUP_DIR/cmdb-cache-$DATE.db.gz" s3://your-backup-bucket/

# Cleanup oude backups (bewaar 30 dagen)
find $BACKUP_DIR -name "*.db.gz" -mtime +30 -delete

2. Configuration Backups

Backup .env.production en andere configuratie bestanden naar een veilige locatie.


Deployment Checklist

Pre-Deployment

  • Alle environment variabelen geconfigureerd
  • SESSION_SECRET gegenereerd (64+ karakters)
  • SSL certificaten geconfigureerd
  • Firewall regels ingesteld
  • Reverse proxy geconfigureerd
  • Health checks getest
  • Backup strategie geïmplementeerd

Security

  • Alle secrets in environment variabelen (niet in code)
  • HTTPS geforceerd (HTTP → HTTPS redirect)
  • Security headers geconfigureerd (HSTS, CSP, etc.)
  • Rate limiting geactiveerd
  • CORS correct geconfigureerd
  • Session cookies secure en httpOnly
  • OAuth callback URL correct geconfigureerd

Performance

  • Production builds getest
  • Docker images geoptimaliseerd (multi-stage builds)
  • Caching geconfigureerd (nginx, browser)
  • Gzip compression geactiveerd
  • Database indexes gecontroleerd

Monitoring

  • Logging geconfigureerd met rotation
  • Health checks geïmplementeerd
  • Monitoring alerts ingesteld
  • Error tracking geconfigureerd (optioneel)

Operations

  • Backup scripts getest
  • Restore procedure gedocumenteerd
  • Rollback procedure gedocumenteerd
  • Deployment procedure gedocumenteerd
  • Incident response plan

🚀 Deployment Commands

Build en Start

# Build production images
docker-compose -f docker-compose.prod.yml build

# Start services
docker-compose -f docker-compose.prod.yml up -d

# Check logs
docker-compose -f docker-compose.prod.yml logs -f

# Check health
curl https://cmdb.zuyderland.nl/health

Updates

# Pull nieuwe code
git pull

# Rebuild en restart
docker-compose -f docker-compose.prod.yml up -d --build

# Zero-downtime deployment (met meerdere instances)
# Gebruik rolling updates of blue-green deployment

🔧 Aanvullende Aanbevelingen

1. Container Orchestration

Voor grotere deployments, overweeg:

  • Kubernetes: Voor schaalbaarheid en high availability
  • Docker Swarm: Simpelere alternatief voor Kubernetes

2. Load Balancing

Voor high availability, gebruik meerdere backend instances met load balancer.

3. CDN

Overweeg een CDN (CloudFlare, AWS CloudFront) voor statische assets.

4. Database Scaling

Voor grote datasets, overweeg:

  • PostgreSQL in plaats van SQLite
  • Database replicatie voor read scaling
  • Connection pooling

5. Caching Layer

Overweeg Redis voor:

  • Session storage
  • API response caching
  • Query result caching

📞 Support & Troubleshooting

Veelvoorkomende Issues

  1. Sessions verlopen te snel: Check SESSION_SECRET en Redis configuratie
  2. CORS errors: Check FRONTEND_URL en CORS configuratie
  3. Database locks: SQLite is niet geschikt voor hoge concurrency
  4. Memory issues: Monitor container memory usage

Logs Bekijken

# Backend logs
docker-compose -f docker-compose.prod.yml logs backend

# Frontend logs
docker-compose -f docker-compose.prod.yml logs frontend

# Nginx logs
docker-compose -f docker-compose.prod.yml logs nginx

📚 Referenties