- Multi-stage Dockerfile (builder+runner, <500MB target) - docker-compose.yml: app(x4) + postgres + redis + prometheus + grafana + nginx - .env.example with all required secrets (never hardcoded) - requirements.txt with all dependencies including prometheus-client, alembic - GitHub Actions CI: lint (flake8+bandit+safety) + tests + Docker build/push - GitHub Actions CD: staging deploy -> smoke tests -> production deploy + rollback - Alembic migration setup + initial PostgreSQL schema (001_initial_schema) - SQLite→PostgreSQL data migration script - Prometheus metrics module (HTTP, ML, DB, business metrics) - Prometheus alert rules (5xx >1%, latency >2s, disk >80%, ML accuracy) - Grafana dashboard (overview: req/s, p95, ML accuracy, error rate) - Nginx reverse proxy config (HTTPS/TLS, rate limiting, security headers) - Structured JSON logging module - Automated daily DB backup script (pg_dump + 30-day retention) Branch: feature/devops-cicd Co-Authored-By: Paperclip <noreply@paperclip.ing>
158 lines
4.9 KiB
Plaintext
158 lines
4.9 KiB
Plaintext
# ============================================================
|
|
# Nginx Virtual Host — Turf SaaS
|
|
# ============================================================
|
|
|
|
# Upstream service pools
|
|
upstream combined_api {
|
|
server combined-api:8790;
|
|
keepalive 32;
|
|
}
|
|
|
|
upstream dashboard_api {
|
|
server dashboard-api:8791;
|
|
keepalive 16;
|
|
}
|
|
|
|
upstream portal {
|
|
server portal:8792;
|
|
keepalive 16;
|
|
}
|
|
|
|
upstream grafana {
|
|
server grafana:3000;
|
|
keepalive 4;
|
|
}
|
|
|
|
# ----------------------------------------------------------
|
|
# HTTP → HTTPS redirect
|
|
# ----------------------------------------------------------
|
|
server {
|
|
listen 80;
|
|
server_name _;
|
|
|
|
# Let's Encrypt ACME challenge
|
|
location /.well-known/acme-challenge/ {
|
|
root /var/www/certbot;
|
|
}
|
|
|
|
location / {
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
}
|
|
|
|
# ----------------------------------------------------------
|
|
# HTTPS main server
|
|
# ----------------------------------------------------------
|
|
server {
|
|
listen 443 ssl;
|
|
http2 on;
|
|
server_name ${DOMAIN};
|
|
|
|
# TLS configuration
|
|
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
|
|
ssl_session_cache shared:SSL:10m;
|
|
ssl_session_timeout 10m;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
|
ssl_prefer_server_ciphers on;
|
|
ssl_stapling on;
|
|
ssl_stapling_verify on;
|
|
|
|
# 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;
|
|
add_header Referrer-Policy strict-origin-when-cross-origin always;
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
|
|
|
|
# Limits
|
|
client_max_body_size 10M;
|
|
limit_conn conn_limit 20;
|
|
|
|
# ----------------------------------------------------------
|
|
# Portal (root)
|
|
# ----------------------------------------------------------
|
|
location / {
|
|
limit_req zone=global burst=50 nodelay;
|
|
proxy_pass http://portal;
|
|
proxy_http_version 1.1;
|
|
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 Connection "";
|
|
proxy_read_timeout 60s;
|
|
}
|
|
|
|
# ----------------------------------------------------------
|
|
# Combined API
|
|
# ----------------------------------------------------------
|
|
location /api/ {
|
|
limit_req zone=api burst=20 nodelay;
|
|
proxy_pass http://combined_api;
|
|
proxy_http_version 1.1;
|
|
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 Connection "";
|
|
proxy_read_timeout 120s;
|
|
}
|
|
|
|
# ----------------------------------------------------------
|
|
# Dashboard API
|
|
# ----------------------------------------------------------
|
|
location /dashboard-api/ {
|
|
limit_req zone=api burst=20 nodelay;
|
|
proxy_pass http://dashboard_api/;
|
|
proxy_http_version 1.1;
|
|
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 Connection "";
|
|
proxy_read_timeout 120s;
|
|
}
|
|
|
|
# ----------------------------------------------------------
|
|
# Grafana (restricted to internal/admin)
|
|
# ----------------------------------------------------------
|
|
location /grafana/ {
|
|
# Restrict to admin IPs in production
|
|
# allow 10.0.0.0/8;
|
|
# deny all;
|
|
|
|
proxy_pass http://grafana;
|
|
proxy_http_version 1.1;
|
|
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 Connection "";
|
|
}
|
|
|
|
# ----------------------------------------------------------
|
|
# Health check (no rate limiting)
|
|
# ----------------------------------------------------------
|
|
location /health {
|
|
proxy_pass http://combined_api/health;
|
|
proxy_http_version 1.1;
|
|
access_log off;
|
|
}
|
|
|
|
# Block common attack vectors
|
|
location ~ /\. {
|
|
deny all;
|
|
access_log off;
|
|
log_not_found off;
|
|
}
|
|
|
|
location ~* \.(env|git|bak|sql|log)$ {
|
|
deny all;
|
|
access_log off;
|
|
log_not_found off;
|
|
}
|
|
}
|