Files
cmdb-insight/docs/PRODUCTION-DEPLOYMENT.md
Bert Hausmans cdee0e8819 UI styling improvements: dashboard headers and navigation
- Restore blue PageHeader on Dashboard (/app-components)
- Update homepage (/) with subtle header design without blue bar
- Add uniform PageHeader styling to application edit page
- Fix Rapporten link on homepage to point to /reports overview
- Improve header descriptions spacing for better readability
2026-01-21 03:24:56 +01:00

760 lines
18 KiB
Markdown

# Productie Deployment Guide
Deze guide beschrijft hoe je de CMDB Insight 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=<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:**
```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<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:
```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)