# Productie Deployment Guide Deze guide beschrijft hoe je de Zuyderland CMDB GUI applicatie veilig en betrouwbaar in productie kunt draaien. ## 📋 Inhoudsopgave 1. [Security Best Practices](#security-best-practices) 2. [Production Docker Setup](#production-docker-setup) 3. [Environment Configuratie](#environment-configuratie) 4. [Reverse Proxy (Nginx)](#reverse-proxy-nginx) 5. [SSL/TLS Certificaten](#ssltls-certificaten) 6. [Database & Cache](#database--cache) 7. [Session Management](#session-management) 8. [Monitoring & Logging](#monitoring--logging) 9. [Backup Strategie](#backup-strategie) 10. [Deployment Checklist](#deployment-checklist) --- ## 🔒 Security Best Practices ### 1. Environment Variabelen **ALTIJD** gebruik een `.env` bestand dat NIET in git staat: ```bash # .env (niet committen!) JIRA_HOST=https://jira.zuyderland.nl JIRA_SCHEMA_ID=your-schema-id 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= 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:** ```bash 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`: ```dockerfile 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`: ```dockerfile 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`: ```nginx 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`: ```yaml 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`: ```nginx 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) ```bash # 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: ```bash # 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:** ```bash # 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:** ```bash # 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:** ```yaml 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: ``` 2. **Update authService.ts om Redis te gebruiken:** ```typescript 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 { const ttl = Math.floor((session.expiresAt - Date.now()) / 1000); await redis.setex(`session:${sessionId}`, ttl, JSON.stringify(session)); } async function getSession(sessionId: string): Promise { const data = await redis.get(`session:${sessionId}`); return data ? JSON.parse(data) : null; } ``` --- ## 📊 Monitoring & Logging ### 1. Logging De applicatie gebruikt Winston. Configureer log rotation: ```typescript // 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: ```bash # 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 ```bash #!/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 ```bash # 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 ```bash # 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 ```bash # 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 - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/) - [Docker Security Best Practices](https://docs.docker.com/engine/security/) - [Nginx Security Guide](https://nginx.org/en/docs/http/configuring_https_servers.html)