- Add PostgreSQL and SQLite database adapters with factory pattern - Add migration script for SQLite to PostgreSQL - Add production Dockerfiles and docker-compose configs - Add deployment documentation and scripts - Add BIA sync dashboard and matching service - Add data completeness configuration and components - Add new dashboard components (BusinessImportanceComparison, ComplexityDynamics, etc.) - Update various services and routes - Remove deprecated management-parameters.json and taxonomy files
18 KiB
Productie Deployment Guide
Deze guide beschrijft hoe je de Zuyderland CMDB GUI applicatie veilig en betrouwbaar in productie kunt draaien.
📋 Inhoudsopgave
- Security Best Practices
- Production Docker Setup
- Environment Configuratie
- Reverse Proxy (Nginx)
- SSL/TLS Certificaten
- Database & Cache
- Session Management
- Monitoring & Logging
- Backup Strategie
- 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_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=<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
- 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:
- 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
- Sessions verlopen te snel: Check SESSION_SECRET en Redis configuratie
- CORS errors: Check FRONTEND_URL en CORS configuratie
- Database locks: SQLite is niet geschikt voor hoge concurrency
- 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