- ROADMAP: dodano funkcję #2 (e-deklaracja PZ) z analizą flow PDF + samodzielny podpis - architecture/03,07,08,09,11 + flows/06: aktualizacja pod OVH VPS (IP, user maciejpi zamiast www-data, brak NPM dla prod) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1286 lines
37 KiB
Markdown
1286 lines
37 KiB
Markdown
# HTTP Request Flow
|
|
|
|
**Document Version:** 1.0
|
|
**Last Updated:** 2026-04-04
|
|
**Status:** Production LIVE (OVH VPS)
|
|
**Flow Type:** HTTP Request Handling & Response Cycle
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document describes the **complete HTTP request flow** for the Norda Biznes Partner application, from external user through nginx reverse proxy to Flask application and back. It covers:
|
|
|
|
- **Complete request path** (Internet → Nginx → Flask → Response)
|
|
- **SSL/TLS termination** and security boundaries
|
|
- **Request/response transformation** at each layer
|
|
- **Common failure scenarios** and troubleshooting
|
|
|
|
**Key Infrastructure:**
|
|
- **Public Entry:** 57.128.200.27:443 (OVH VPS, direct)
|
|
- **Reverse Proxy:** Nginx on 57.128.200.27:443 (SSL termination)
|
|
- **Backend Application:** Flask/Gunicorn on 127.0.0.1:5000
|
|
- **Protocol Flow:** HTTPS → Nginx → HTTP (localhost) → Flask → HTTP → Nginx → HTTPS
|
|
|
|
> **Note:** The old on-prem setup used FortiGate NAT (85.237.177.83) and NPM (10.22.68.250) as intermediate layers. Production now runs directly on OVH VPS without FortiGate or NPM.
|
|
|
|
**Related Documentation:**
|
|
- Incident Report: `docs/INCIDENT_REPORT_20260102.md` (ERR_TOO_MANY_REDIRECTS)
|
|
- Container Diagram: `docs/architecture/02-container-diagram.md`
|
|
- Deployment Architecture: `docs/architecture/03-deployment-architecture.md`
|
|
|
|
---
|
|
|
|
## 1. Complete HTTP Request Flow
|
|
|
|
### 1.1 Successful Request Sequence Diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Browser
|
|
participant Nginx as 🔒 Nginx Reverse Proxy<br/>57.128.200.27:443
|
|
participant Flask as 🌐 Flask/Gunicorn<br/>127.0.0.1:5000
|
|
participant DB as 💾 PostgreSQL<br/>localhost:5432
|
|
|
|
Note over User,DB: SUCCESSFUL REQUEST FLOW
|
|
|
|
User->>Browser: Navigate to https://nordabiznes.pl/
|
|
Browser->>Nginx: HTTPS GET / (Port 443)
|
|
Note over Nginx: SSL/TLS Termination<br/>Let's Encrypt Certificate (certbot)
|
|
|
|
Note over Nginx: Request Processing<br/>• Validate certificate<br/>• Decrypt HTTPS<br/>• Extract headers<br/>• proxy_pass to localhost:5000
|
|
|
|
Nginx->>Flask: HTTP GET / (127.0.0.1:5000)
|
|
|
|
Note over Flask: Flask Request Handling<br/>• WSGI via Gunicorn<br/>• Route matching (app.py)<br/>• Session validation<br/>• CSRF check (if POST)
|
|
|
|
Flask->>DB: SELECT * FROM companies WHERE status='active'
|
|
DB->>Flask: Company data (80 rows)
|
|
|
|
Note over Flask: Template Rendering<br/>• Jinja2 template: index.html<br/>• Inject company data<br/>• Apply filters & sorting
|
|
|
|
Flask->>Nginx: HTTP 200 OK<br/>Content-Type: text/html<br/>Set-Cookie: session=...<br/>HTML content
|
|
|
|
Note over Nginx: Response Processing<br/>• Encrypt response (HTTPS)<br/>• Add security headers<br/>• HSTS, CSP, X-Frame-Options
|
|
|
|
Nginx->>Browser: HTTPS 200 OK
|
|
Browser->>User: Display page
|
|
```
|
|
|
|
### 1.2 Historical: Failed Request (Old NPM Setup - Port Misconfiguration)
|
|
|
|
> **Note:** This failure scenario applied to the old on-prem setup with NPM. It is no longer applicable to the current OVH VPS production setup.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
actor User
|
|
participant Browser
|
|
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
|
|
participant NginxSys as ⚠️ Nginx System<br/>10.22.68.249:80
|
|
participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
|
|
|
|
Note over User,Flask: FAILED REQUEST FLOW (REDIRECT LOOP) — HISTORICAL
|
|
|
|
User->>Browser: Navigate to https://nordabiznes.pl/
|
|
Browser->>NPM: HTTPS GET / (Port 443)
|
|
|
|
Note over NPM: SSL/TLS Termination<br/>Decrypt HTTPS
|
|
|
|
NPM->>NginxSys: ❌ HTTP GET / (Port 80)<br/>WRONG PORT!
|
|
|
|
Note over NginxSys: Nginx System Config<br/>Redirects ALL HTTP to HTTPS
|
|
|
|
NginxSys->>NPM: HTTP 301 Moved Permanently<br/>Location: https://nordabiznes.pl/
|
|
|
|
Note over NPM: Follow redirect
|
|
|
|
NPM->>NginxSys: HTTP GET / (Port 80)
|
|
NginxSys->>NPM: HTTP 301 Moved Permanently<br/>Location: https://nordabiznes.pl/
|
|
|
|
Note over NPM: Redirect loop detected<br/>After 20 redirects...
|
|
|
|
NPM->>Browser: ERR_TOO_MANY_REDIRECTS
|
|
Browser->>User: ❌ Error: Too many redirects
|
|
|
|
Note over User: Portal UNAVAILABLE<br/>30 minutes downtime<br/>See: INCIDENT_REPORT_20260102.md
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Layer-by-Layer Request Processing
|
|
|
|
### 2.1 Layer 1: DNS + Direct Connection (OVH VPS)
|
|
|
|
**Server:** OVH VPS
|
|
**Public IP:** 57.128.200.27
|
|
**Function:** Direct internet-facing server (no NAT, no FortiGate for production)
|
|
|
|
**Processing Steps:**
|
|
|
|
1. **Receive external request:**
|
|
```
|
|
Source: User IP (e.g., 93.104.x.x)
|
|
Destination: 85.237.177.83:443
|
|
Protocol: HTTPS
|
|
```
|
|
|
|
2. **NAT Translation:**
|
|
```
|
|
External: 85.237.177.83:443 → Internal: 10.22.68.250:443
|
|
```
|
|
|
|
3. **Firewall Rules:**
|
|
- Allow TCP port 443 (HTTPS)
|
|
- Allow TCP port 80 (HTTP, redirects to HTTPS)
|
|
- Block all other inbound ports
|
|
- Stateful connection tracking
|
|
|
|
4. **Forward to NPM:**
|
|
```
|
|
Destination: 10.22.68.250:443 (NPM reverse proxy)
|
|
Protocol: HTTPS (encrypted tunnel)
|
|
```
|
|
|
|
**Configuration:**
|
|
```
|
|
NAT Rule: DNAT 85.237.177.83:443 → 10.22.68.250:443
|
|
Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)
|
|
```
|
|
|
|
---
|
|
|
|
### 2.2 Layer 2: Nginx Reverse Proxy (SSL Termination)
|
|
|
|
**Server:** OVH VPS (inpi-vps-waw01)
|
|
**IP:** 57.128.200.27
|
|
**Port:** 443 (HTTPS)
|
|
**Technology:** Nginx with Let's Encrypt (certbot)
|
|
|
|
**Processing Steps:**
|
|
|
|
1. **Receive HTTPS request:**
|
|
```
|
|
Source: Fortigate (10.22.68.250:443)
|
|
Method: GET
|
|
Host: nordabiznes.pl
|
|
Protocol: HTTPS/1.1 or HTTP/2
|
|
```
|
|
|
|
2. **SSL/TLS Termination:**
|
|
- Load SSL certificate (Let's Encrypt, Certificate ID: 27)
|
|
- Validate certificate (nordabiznes.pl, www.nordabiznes.pl)
|
|
- Decrypt HTTPS traffic
|
|
- Establish secure connection with client
|
|
|
|
3. **Request Header Processing:**
|
|
```http
|
|
GET / HTTP/1.1
|
|
Host: nordabiznes.pl
|
|
User-Agent: Mozilla/5.0 ...
|
|
Accept: text/html,application/xhtml+xml
|
|
Accept-Language: pl-PL,pl;q=0.9
|
|
Accept-Encoding: gzip, deflate, br
|
|
Connection: keep-alive
|
|
```
|
|
|
|
4. **NPM Proxy Configuration Lookup:**
|
|
```sql
|
|
-- NPM internal database query
|
|
SELECT * FROM proxy_host WHERE id = 27;
|
|
-- Result:
|
|
domain_names: ["nordabiznes.pl", "www.nordabiznes.pl"]
|
|
forward_scheme: "http"
|
|
forward_host: "57.128.200.27"
|
|
forward_port: 5000 ← CRITICAL!
|
|
ssl_forced: true
|
|
certificate_id: 27
|
|
```
|
|
|
|
5. **⚠️ CRITICAL ROUTING DECISION:**
|
|
```
|
|
✓ CORRECT: Forward to http://57.128.200.27:5000
|
|
❌ WRONG: Forward to http://57.128.200.27:80 (causes redirect loop!)
|
|
```
|
|
|
|
6. **Forward to Backend (HTTP, unencrypted):**
|
|
```http
|
|
GET / HTTP/1.1
|
|
Host: nordabiznes.pl
|
|
X-Real-IP: 93.104.x.x (original client IP)
|
|
X-Forwarded-For: 93.104.x.x
|
|
X-Forwarded-Proto: https
|
|
X-Forwarded-Host: nordabiznes.pl
|
|
Connection: close
|
|
```
|
|
|
|
**NPM Configuration (Proxy Host ID: 27):**
|
|
|
|
| Parameter | Value | Notes |
|
|
|-----------|-------|-------|
|
|
| Domain Names | nordabiznes.pl, www.nordabiznes.pl | Primary + www alias |
|
|
| Forward Scheme | http | NPM→Backend uses HTTP (secure internal network) |
|
|
| Forward Host | 127.0.0.1 | Localhost (same OVH VPS) |
|
|
| **Forward Port** | **5000** | **Flask/Gunicorn port (CRITICAL!)** |
|
|
| SSL Certificate | 27 (Let's Encrypt) | Auto-renewal enabled |
|
|
| SSL Forced | Yes | Redirect HTTP→HTTPS |
|
|
| HTTP/2 Support | Yes | Modern protocol support |
|
|
| HSTS Enabled | Yes | max-age=31536000; includeSubDomains |
|
|
| Block Exploits | Yes | Nginx security module |
|
|
| Websocket Support | Yes | For future features |
|
|
|
|
**Verification Command:**
|
|
```bash
|
|
# Check NPM configuration
|
|
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
|
sqlite3 /data/database.sqlite \
|
|
\"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""
|
|
|
|
# Expected output:
|
|
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|57.128.200.27|5000
|
|
```
|
|
|
|
---
|
|
|
|
### 2.3 Layer 3: Flask/Gunicorn Application (Request Processing)
|
|
|
|
**Server:** OVH VPS (inpi-vps-waw01)
|
|
**IP:** 127.0.0.1 (localhost, via nginx proxy_pass)
|
|
**Port:** 5000
|
|
**Technology:** Gunicorn 20.1.0 + Flask 3.0
|
|
|
|
**Processing Steps:**
|
|
|
|
1. **Gunicorn Receives HTTP Request:**
|
|
```
|
|
Binding: 127.0.0.1:5000 (all interfaces)
|
|
Workers: 4 (Gunicorn worker processes)
|
|
Worker Class: sync (synchronous workers)
|
|
Timeout: 120 seconds
|
|
```
|
|
|
|
2. **Worker Selection:**
|
|
- Gunicorn master process receives connection
|
|
- Distributes request to available worker (round-robin)
|
|
- Worker loads WSGI app (Flask application)
|
|
|
|
3. **WSGI Interface:**
|
|
```python
|
|
# Gunicorn calls Flask's WSGI application
|
|
from app import app
|
|
|
|
# WSGI environ dict contains:
|
|
environ = {
|
|
'REQUEST_METHOD': 'GET',
|
|
'PATH_INFO': '/',
|
|
'QUERY_STRING': '',
|
|
'SERVER_NAME': 'nordabiznes.pl',
|
|
'SERVER_PORT': '5000',
|
|
'HTTP_HOST': 'nordabiznes.pl',
|
|
'HTTP_X_REAL_IP': '93.104.x.x',
|
|
'HTTP_X_FORWARDED_PROTO': 'https',
|
|
'wsgi.url_scheme': 'http',
|
|
# ... more headers
|
|
}
|
|
```
|
|
|
|
4. **Flask Request Handling (app.py):**
|
|
|
|
**a) Request Context Setup:**
|
|
```python
|
|
# Flask creates request context
|
|
from flask import request, session, g
|
|
|
|
# Parse request
|
|
request.method = 'GET'
|
|
request.path = '/'
|
|
request.url = 'https://nordabiznes.pl/'
|
|
request.args = {} # Query parameters
|
|
request.headers = {...} # HTTP headers
|
|
```
|
|
|
|
**b) Before Request Hooks:**
|
|
```python
|
|
@app.before_request
|
|
def load_logged_in_user():
|
|
# Check session for user_id
|
|
user_id = session.get('user_id')
|
|
if user_id:
|
|
g.user = db.session.query(User).get(user_id)
|
|
else:
|
|
g.user = None
|
|
```
|
|
|
|
**c) Route Matching:**
|
|
```python
|
|
# Flask router matches route
|
|
@app.route('/')
|
|
def index():
|
|
# Main catalog page
|
|
pass
|
|
```
|
|
|
|
**d) View Function Execution:**
|
|
```python
|
|
@app.route('/')
|
|
def index():
|
|
# Query companies from database
|
|
companies = db.session.query(Company)\
|
|
.filter_by(status='active')\
|
|
.order_by(Company.name)\
|
|
.all() # 80 companies
|
|
|
|
# Render template
|
|
return render_template('index.html', companies=companies)
|
|
```
|
|
|
|
5. **Database Query (PostgreSQL):**
|
|
```python
|
|
# SQLAlchemy ORM generates SQL
|
|
SELECT * FROM companies
|
|
WHERE status = 'active'
|
|
ORDER BY name;
|
|
```
|
|
|
|
6. **Template Rendering (Jinja2):**
|
|
```python
|
|
# templates/index.html
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
# Render template with context
|
|
html = render_template('index.html',
|
|
companies=companies,
|
|
user=g.user,
|
|
config=app.config)
|
|
```
|
|
|
|
7. **Response Construction:**
|
|
```python
|
|
# Flask creates HTTP response
|
|
response = Response(
|
|
response=html, # Rendered HTML
|
|
status=200, # HTTP status code
|
|
headers={
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Content-Length': len(html),
|
|
'Set-Cookie': 'session=...; HttpOnly; Secure; SameSite=Lax'
|
|
}
|
|
)
|
|
```
|
|
|
|
**Gunicorn Configuration:**
|
|
```ini
|
|
# /etc/systemd/system/nordabiznes.service
|
|
[Service]
|
|
User=maciejpi
|
|
Group=maciejpi
|
|
WorkingDirectory=/var/www/nordabiznes
|
|
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
|
--bind 127.0.0.1:5000 \
|
|
--workers 4 \
|
|
--timeout 120 \
|
|
--access-logfile /var/log/nordabiznes/access.log \
|
|
--error-logfile /var/log/nordabiznes/error.log \
|
|
--log-level info \
|
|
app:app
|
|
```
|
|
|
|
**Verification Command:**
|
|
```bash
|
|
# Test Flask directly (from server)
|
|
curl -I http://57.128.200.27:5000/health
|
|
# Expected: HTTP/1.1 200 OK
|
|
|
|
# Check Gunicorn workers
|
|
ssh maciejpi@57.128.200.27 "ps aux | grep gunicorn"
|
|
# Expected: 1 master + 4 worker processes
|
|
```
|
|
|
|
---
|
|
|
|
### 2.4 Layer 4: PostgreSQL Database (Data Retrieval)
|
|
|
|
**Server:** OVH VPS (same server as Flask)
|
|
**IP:** 127.0.0.1 (localhost only)
|
|
**Port:** 5432
|
|
**Technology:** PostgreSQL 14
|
|
|
|
**Processing Steps:**
|
|
|
|
1. **Receive SQL Query:**
|
|
```sql
|
|
-- SQLAlchemy ORM generates query
|
|
SELECT companies.id, companies.name, companies.slug,
|
|
companies.short_description, companies.category,
|
|
companies.nip, companies.city, companies.website
|
|
FROM companies
|
|
WHERE companies.status = 'active'
|
|
ORDER BY companies.name;
|
|
```
|
|
|
|
2. **Query Execution:**
|
|
- Parse SQL syntax
|
|
- Create execution plan (Query planner)
|
|
- Use indexes if available (idx_companies_status, idx_companies_name)
|
|
- Fetch rows from disk/cache
|
|
|
|
3. **Return Results:**
|
|
```python
|
|
# SQLAlchemy ORM returns Company objects
|
|
[
|
|
Company(id=1, name='ALMARES', slug='almares', ...),
|
|
Company(id=2, name='AMA', slug='ama-spolka-z-o-o', ...),
|
|
# ... 78 more companies
|
|
]
|
|
```
|
|
|
|
**Connection Configuration:**
|
|
```python
|
|
# app.py database connection
|
|
DATABASE_URL = 'postgresql://nordabiz_app:PASSWORD@localhost:5432/nordabiz'
|
|
|
|
# SQLAlchemy engine config
|
|
engine = create_engine(DATABASE_URL,
|
|
pool_size=10, # Connection pool
|
|
max_overflow=20, # Additional connections
|
|
pool_pre_ping=True, # Validate connections
|
|
pool_recycle=3600 # Recycle after 1 hour
|
|
)
|
|
```
|
|
|
|
**Security:**
|
|
- PostgreSQL listens on **127.0.0.1 ONLY** (no external access)
|
|
- Application connects via localhost socket
|
|
- User: `nordabiz_app` (limited privileges, no DROP/CREATE)
|
|
- SSL: Not required (localhost connection)
|
|
|
|
**Verification:**
|
|
```bash
|
|
# Check PostgreSQL status
|
|
ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
|
|
|
|
# Test connection (from server)
|
|
ssh maciejpi@57.128.200.27 "psql -U nordabiz_app -h 127.0.0.1 -d nordabiz -c 'SELECT COUNT(*) FROM companies;'"
|
|
# Expected: 80
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Response Flow (Flask → User)
|
|
|
|
### 3.1 Response Path Sequence
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Flask as 🌐 Flask/Gunicorn<br/>57.128.200.27:5000
|
|
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
|
|
participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
|
|
participant Browser
|
|
|
|
Note over Flask: Response Construction
|
|
Flask->>Flask: Render Jinja2 template<br/>HTML content (50 KB)
|
|
|
|
Flask->>Nginx: HTTP/1.1 200 OK<br/>Content-Type: text/html; charset=utf-8<br/>Content-Length: 51234<br/>Set-Cookie: session=...<br/><br/>[HTML content]
|
|
|
|
Note over NPM: Response Processing
|
|
NPM->>NPM: Add security headers:<br/>• Strict-Transport-Security<br/>• X-Frame-Options: SAMEORIGIN<br/>• X-Content-Type-Options: nosniff<br/>• Referrer-Policy: strict-origin
|
|
|
|
NPM->>NPM: Compress response (gzip)<br/>Size: 50 KB → 12 KB
|
|
|
|
NPM->>NPM: Encrypt response (TLS 1.3)<br/>Certificate: Let's Encrypt
|
|
|
|
Nginx->>Browser: HTTPS 200 OK<br/>Encrypted response<br/>Size: 12 KB (gzip)
|
|
|
|
Note over Fortigate: NAT reverse translation
|
|
Fortigate->>Browser: HTTPS 200 OK<br/>Source: 85.237.177.83
|
|
|
|
Note over Browser: Response Handling
|
|
Browser->>Browser: Decrypt HTTPS<br/>Decompress gzip<br/>Parse HTML<br/>Render page
|
|
```
|
|
|
|
### 3.2 Response Headers (NPM → User)
|
|
|
|
**Headers Added by NPM:**
|
|
```http
|
|
HTTP/2 200 OK
|
|
server: nginx/1.24.0 (Ubuntu)
|
|
date: Fri, 10 Jan 2026 10:30:00 GMT
|
|
content-type: text/html; charset=utf-8
|
|
content-length: 12345
|
|
strict-transport-security: max-age=31536000; includeSubDomains
|
|
x-frame-options: SAMEORIGIN
|
|
x-content-type-options: nosniff
|
|
referrer-policy: strict-origin-when-cross-origin
|
|
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'
|
|
```
|
|
|
|
**Headers from Flask (preserved):**
|
|
```http
|
|
set-cookie: session=eyJ...; HttpOnly; Path=/; SameSite=Lax; Secure
|
|
vary: Cookie
|
|
x-request-id: abc123def456
|
|
```
|
|
|
|
**Response Body:**
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Norda Biznes Partner - Katalog firm</title>
|
|
<!-- ... CSS, meta tags ... -->
|
|
</head>
|
|
<body>
|
|
<!-- Rendered company catalog -->
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Port Mapping Reference
|
|
|
|
### 4.1 Complete Port Flow
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ EXTERNAL USER │
|
|
│ Browser: https://nordabiznes.pl/ │
|
|
└────────────────────────────┬────────────────────────────────────┘
|
|
│ HTTPS (Port 443)
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ FORTIGATE FIREWALL │
|
|
│ Public IP: 85.237.177.83:443 │
|
|
│ NAT: 85.237.177.83:443 → 10.22.68.250:443 │
|
|
└────────────────────────────┬────────────────────────────────────┘
|
|
│ HTTPS (Port 443)
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ NPM REVERSE PROXY (R11-REVPROXY-01) │
|
|
│ IP: 10.22.68.250:443 │
|
|
│ Function: SSL Termination │
|
|
│ Certificate: Let's Encrypt (nordabiznes.pl) │
|
|
│ │
|
|
│ ⚠️ CRITICAL ROUTING DECISION: │
|
|
│ ✓ Forward to: http://57.128.200.27:5000 (CORRECT) │
|
|
│ ❌ DO NOT use: http://57.128.200.27:80 (WRONG!) │
|
|
└────────────────────────────┬────────────────────────────────────┘
|
|
│ HTTP (Port 5000) ✓
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ FLASK/GUNICORN (OVH VPS) │
|
|
│ IP: 57.128.200.27:5000 │
|
|
│ Binding: 127.0.0.1:5000 │
|
|
│ Workers: 4 (Gunicorn) │
|
|
│ Function: Application logic, template rendering │
|
|
└────────────────────────────┬────────────────────────────────────┘
|
|
│ SQL (localhost:5432)
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ POSTGRESQL (OVH VPS) │
|
|
│ IP: 127.0.0.1:5432 (localhost only) │
|
|
│ Database: nordabiz │
|
|
│ Function: Data storage │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 4.2 Port Table (OVH VPS)
|
|
|
|
| Port | Service | Binding | User | Purpose | NPM Should Use? |
|
|
|------|---------|---------|------|---------|-----------------|
|
|
| **5000** | **Gunicorn/Flask** | **0.0.0.0** | **maciejpi** | **Main Application** | **✓ YES** |
|
|
| 80 | Nginx (system) | 0.0.0.0 | root | HTTP→HTTPS redirect | ❌ NO (causes loop!) |
|
|
| 443 | Nginx (system) | 0.0.0.0 | root | HTTPS redirect | ❌ NO (NPM handles SSL) |
|
|
| 5432 | PostgreSQL | 127.0.0.1 | postgres | Database | ❌ NO (localhost only) |
|
|
| 22 | SSH | 0.0.0.0 | - | Administration | ❌ NO (not HTTP) |
|
|
|
|
**⚠️ CRITICAL WARNING:**
|
|
```
|
|
Port 80 and 443 on OVH VPS run a system nginx that:
|
|
1. Redirects ALL HTTP requests to HTTPS
|
|
2. Redirects ALL HTTPS requests to https://nordabiznes.pl
|
|
|
|
If NPM forwards to port 80:
|
|
NPM (HTTPS) → Nginx (port 80) → 301 redirect to HTTPS
|
|
→ NPM receives redirect → forwards to port 80 again
|
|
→ INFINITE LOOP → ERR_TOO_MANY_REDIRECTS
|
|
|
|
SOLUTION: NPM must ALWAYS forward to port 5000!
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Request Types and Routing
|
|
|
|
### 5.1 Static Assets
|
|
|
|
**Request:** `GET https://nordabiznes.pl/static/css/style.css`
|
|
|
|
**Flow:**
|
|
```
|
|
User → Nginx → Flask → Static file handler → Return CSS
|
|
```
|
|
|
|
**Flask Handling:**
|
|
```python
|
|
# Flask serves static files from /static directory
|
|
@app.route('/static/<path:filename>')
|
|
def static_files(filename):
|
|
return send_from_directory('static', filename)
|
|
```
|
|
|
|
**Optimization:** NPM could cache static assets (future enhancement)
|
|
|
|
---
|
|
|
|
### 5.2 API Endpoints
|
|
|
|
**Request:** `GET https://nordabiznes.pl/api/companies`
|
|
|
|
**Flow:**
|
|
```
|
|
User → Nginx → Flask → API route → Database → JSON response
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"companies": [
|
|
{
|
|
"id": 1,
|
|
"name": "PIXLAB",
|
|
"slug": "pixlab-sp-z-o-o",
|
|
"category": "IT"
|
|
}
|
|
],
|
|
"total": 80
|
|
}
|
|
```
|
|
|
|
**Headers:**
|
|
```http
|
|
Content-Type: application/json; charset=utf-8
|
|
Access-Control-Allow-Origin: * (if CORS enabled)
|
|
```
|
|
|
|
---
|
|
|
|
### 5.3 Form Submissions (POST)
|
|
|
|
**Request:** `POST https://nordabiznes.pl/login`
|
|
|
|
**Flow:**
|
|
```
|
|
User → Nginx → Flask → CSRF validation → Auth check → Database → Redirect
|
|
```
|
|
|
|
**Additional Processing:**
|
|
- **CSRF Token Validation:** Flask-WTF checks token in form
|
|
- **Session Creation:** Flask-Login creates session cookie
|
|
- **Database Update:** Update `last_login` timestamp
|
|
|
|
**Response:**
|
|
```http
|
|
HTTP/2 302 Found
|
|
Location: https://nordabiznes.pl/dashboard
|
|
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
|
|
```
|
|
|
|
---
|
|
|
|
### 5.4 Health Check Endpoint
|
|
|
|
**Request:** `GET https://nordabiznes.pl/health`
|
|
|
|
**Purpose:** Monitoring and verification
|
|
|
|
**Flow:**
|
|
```
|
|
Monitor → Nginx → Flask → Simple response (no DB query)
|
|
```
|
|
|
|
**Response:**
|
|
```http
|
|
HTTP/2 200 OK
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
**Use Cases:**
|
|
- Verify NPM configuration after changes
|
|
- External monitoring (Zabbix)
|
|
- Load balancer health checks (future)
|
|
|
|
---
|
|
|
|
## 6. Security Layers
|
|
|
|
### 6.1 Security Boundaries
|
|
|
|
```mermaid
|
|
graph TB
|
|
Internet["🌐 Internet<br/>(Untrusted)"]
|
|
|
|
subgraph "Security Zone 1: DMZ"
|
|
Fortigate["🛡️ Fortigate Firewall<br/>• NAT<br/>• DPI<br/>• IPS"]
|
|
NPM["🔒 NPM Reverse Proxy<br/>• SSL/TLS Termination<br/>• HSTS<br/>• Rate Limiting"]
|
|
end
|
|
|
|
subgraph "Security Zone 2: Application Layer"
|
|
Flask["🌐 Flask Application<br/>• CSRF Protection<br/>• Input Validation<br/>• XSS Prevention<br/>• Session Management"]
|
|
end
|
|
|
|
subgraph "Security Zone 3: Data Layer"
|
|
DB["💾 PostgreSQL<br/>• No external access<br/>• User privileges<br/>• SQL injection protection"]
|
|
end
|
|
|
|
Internet --> Fortigate
|
|
Fortigate --> NPM
|
|
NPM --> Flask
|
|
Flask --> DB
|
|
|
|
classDef zoneStyle fill:#e74c3c,stroke:#c0392b,color:#fff
|
|
classDef appStyle fill:#3498db,stroke:#2980b9,color:#fff
|
|
classDef dataStyle fill:#2ecc71,stroke:#27ae60,color:#fff
|
|
|
|
class Fortigate,NPM zoneStyle
|
|
class Flask appStyle
|
|
class DB dataStyle
|
|
```
|
|
|
|
### 6.2 Security Features by Layer
|
|
|
|
**Layer 1: Fortigate Firewall**
|
|
- Deep Packet Inspection (DPI)
|
|
- Intrusion Prevention System (IPS)
|
|
- Rate limiting (connection-level)
|
|
- Geo-blocking (if configured)
|
|
|
|
**Layer 2: NPM Reverse Proxy**
|
|
- SSL/TLS 1.3 encryption
|
|
- HSTS (HTTP Strict Transport Security)
|
|
- Certificate validation
|
|
- Security headers injection
|
|
- Request size limits
|
|
|
|
**Layer 3: Flask Application**
|
|
- Flask-Login session management
|
|
- CSRF token validation (Flask-WTF)
|
|
- Input sanitization (XSS prevention)
|
|
- SQL injection protection (SQLAlchemy ORM)
|
|
- Rate limiting (Flask-Limiter)
|
|
- Authentication & Authorization
|
|
|
|
**Layer 4: PostgreSQL Database**
|
|
- Localhost-only binding (127.0.0.1)
|
|
- User privilege separation (nordabiz_app)
|
|
- Parameterized queries (SQLAlchemy)
|
|
- Connection pooling with limits
|
|
|
|
---
|
|
|
|
## 7. Performance Metrics
|
|
|
|
### 7.1 Typical Request Latency
|
|
|
|
| Layer | Processing Time | Notes |
|
|
|-------|-----------------|-------|
|
|
| Fortigate NAT | < 1 ms | Hardware NAT, negligible latency |
|
|
| NPM SSL Termination | 10-20 ms | TLS handshake + decryption |
|
|
| Nginx → Flask Network | < 1 ms | Internal 10 Gbps network |
|
|
| Flask Request Handling | 50-150 ms | Route matching, template rendering |
|
|
| Database Query | 10-30 ms | Indexed queries, connection pool |
|
|
| Template Rendering | 20-50 ms | Jinja2 template compilation |
|
|
| NPM SSL Encryption | 5-10 ms | Response encryption |
|
|
| **Total (typical)** | **96-262 ms** | **Median: ~180 ms** |
|
|
|
|
**Factors Affecting Latency:**
|
|
- Database query complexity (JOIN operations, FTS)
|
|
- Template size (company catalog vs. simple page)
|
|
- Gunicorn worker availability (max 4 concurrent)
|
|
- Network congestion (internal or external)
|
|
|
|
### 7.2 Throughput
|
|
|
|
**Gunicorn Workers:** 4 synchronous workers
|
|
|
|
**Concurrent Requests:** Max 4 simultaneous requests
|
|
|
|
**Requests per Second (RPS):**
|
|
- Simple pages (health check): ~20-30 RPS
|
|
- Database queries (catalog): ~5-10 RPS
|
|
- Complex AI chat: ~2-3 RPS (limited by Gemini API)
|
|
|
|
**Bottlenecks:**
|
|
1. Gunicorn worker count (4 workers)
|
|
2. PostgreSQL connection pool (max 30)
|
|
3. AI API rate limits (1,500 req/day Gemini)
|
|
|
|
---
|
|
|
|
## 8. Troubleshooting Guide
|
|
|
|
### 8.1 Common Issues and Diagnostics
|
|
|
|
#### Issue 1: ERR_TOO_MANY_REDIRECTS
|
|
|
|
**Symptoms:**
|
|
- Browser error: "ERR_TOO_MANY_REDIRECTS"
|
|
- Portal inaccessible from external network
|
|
- Works from internal network (10.22.68.0/24)
|
|
|
|
**Root Cause:**
|
|
- NPM forwarding to port 80 instead of 5000
|
|
|
|
**Diagnosis:**
|
|
```bash
|
|
# 1. Check NPM configuration
|
|
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
|
sqlite3 /data/database.sqlite \
|
|
\"SELECT forward_port FROM proxy_host WHERE id = 27;\""
|
|
|
|
# Expected: 5000
|
|
# If shows: 80 ← PROBLEM!
|
|
|
|
# 2. Test direct backend access
|
|
curl -I http://57.128.200.27:80/
|
|
# If returns: HTTP 301 → Problem confirmed
|
|
|
|
curl -I http://57.128.200.27:5000/
|
|
# Should return: HTTP 200 OK
|
|
```
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Update NPM configuration via API or UI
|
|
# Set forward_port to 5000
|
|
|
|
# Verify fix
|
|
curl -I https://nordabiznes.pl/health
|
|
# Expected: HTTP 200 OK
|
|
```
|
|
|
|
**Reference:** `docs/INCIDENT_REPORT_20260102.md`
|
|
|
|
---
|
|
|
|
#### Issue 2: 502 Bad Gateway
|
|
|
|
**Symptoms:**
|
|
- NPM returns "502 Bad Gateway"
|
|
- Error in NPM logs: "connect() failed (111: Connection refused)"
|
|
|
|
**Root Cause:**
|
|
- Flask/Gunicorn not running
|
|
- Flask listening on wrong port
|
|
- Firewall blocking port 5000
|
|
|
|
**Diagnosis:**
|
|
```bash
|
|
# 1. Check if Gunicorn is running
|
|
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
|
# Expected: Active (running)
|
|
|
|
# 2. Check if port 5000 is listening
|
|
ssh maciejpi@57.128.200.27 "sudo netstat -tlnp | grep 5000"
|
|
# Expected: 127.0.0.1:5000 ... gunicorn
|
|
|
|
# 3. Test direct connection
|
|
curl -I http://57.128.200.27:5000/health
|
|
# Expected: HTTP 200 OK
|
|
```
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Restart Gunicorn
|
|
ssh maciejpi@57.128.200.27 "sudo systemctl restart nordabiznes"
|
|
|
|
# Check logs
|
|
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -n 50"
|
|
```
|
|
|
|
---
|
|
|
|
#### Issue 3: 504 Gateway Timeout
|
|
|
|
**Symptoms:**
|
|
- Request takes > 60 seconds
|
|
- NPM returns "504 Gateway Timeout"
|
|
|
|
**Root Cause:**
|
|
- Flask processing takes too long
|
|
- Database query timeout
|
|
- External API timeout (Gemini, PageSpeed)
|
|
|
|
**Diagnosis:**
|
|
```bash
|
|
# 1. Check Gunicorn worker status
|
|
ssh maciejpi@57.128.200.27 "ps aux | grep gunicorn"
|
|
# Look for workers in state 'R' (running) vs 'S' (sleeping)
|
|
|
|
# 2. Check application logs
|
|
ssh maciejpi@57.128.200.27 "tail -f /var/log/nordabiznes/error.log"
|
|
|
|
# 3. Check database connections
|
|
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c \
|
|
\"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
|
|
```
|
|
|
|
**Solution:**
|
|
- Increase Gunicorn timeout (default: 120s)
|
|
- Optimize slow database queries
|
|
- Add timeout to external API calls
|
|
|
|
---
|
|
|
|
#### Issue 4: SSL Certificate Error
|
|
|
|
**Symptoms:**
|
|
- Browser warning: "Your connection is not private"
|
|
- Certificate expired or invalid
|
|
|
|
**Root Cause:**
|
|
- Let's Encrypt certificate expired
|
|
- Certificate renewal failed
|
|
- Wrong certificate for domain
|
|
|
|
**Diagnosis:**
|
|
```bash
|
|
# Check certificate expiry
|
|
echo | openssl s_client -servername nordabiznes.pl \
|
|
-connect 85.237.177.83:443 2>/dev/null | openssl x509 -noout -dates
|
|
|
|
# Expected: notAfter date in the future
|
|
```
|
|
|
|
**Solution:**
|
|
```bash
|
|
# Renew certificate via NPM UI or API
|
|
# NPM auto-renews 30 days before expiry
|
|
```
|
|
|
|
---
|
|
|
|
### 8.2 Diagnostic Commands Reference
|
|
|
|
**Quick Health Check:**
|
|
```bash
|
|
# External access test
|
|
curl -I https://nordabiznes.pl/health
|
|
# Expected: HTTP/2 200 OK
|
|
|
|
# Internal access test (from INPI network)
|
|
curl -I http://57.128.200.27:5000/health
|
|
# Expected: HTTP/1.1 200 OK
|
|
```
|
|
|
|
**NPM Configuration Check:**
|
|
```bash
|
|
# Show proxy host configuration
|
|
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
|
|
sqlite3 /data/database.sqlite \
|
|
\"SELECT id, domain_names, forward_host, forward_port, ssl_forced \
|
|
FROM proxy_host WHERE id = 27;\""
|
|
```
|
|
|
|
**Application Status:**
|
|
```bash
|
|
# Service status
|
|
ssh maciejpi@57.128.200.27 "sudo systemctl status nordabiznes"
|
|
|
|
# Worker processes
|
|
ssh maciejpi@57.128.200.27 "ps aux | grep gunicorn | grep -v grep"
|
|
|
|
# Recent logs
|
|
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -n 20 --no-pager"
|
|
```
|
|
|
|
**Database Status:**
|
|
```bash
|
|
# PostgreSQL status
|
|
ssh maciejpi@57.128.200.27 "sudo systemctl status postgresql"
|
|
|
|
# Connection count
|
|
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c \
|
|
\"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
|
|
|
|
# Database size
|
|
ssh maciejpi@57.128.200.27 "sudo -u postgres psql -c \
|
|
\"SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
|
|
```
|
|
|
|
**Network Connectivity:**
|
|
```bash
|
|
# Test Nginx → Flask connectivity
|
|
ssh maciejpi@10.22.68.250 "curl -I http://57.128.200.27:5000/health"
|
|
|
|
# Test Flask → Database connectivity
|
|
ssh maciejpi@57.128.200.27 "psql -U nordabiz_app -h 127.0.0.1 \
|
|
-d nordabiz -c 'SELECT 1;'"
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Configuration Reference
|
|
|
|
### 9.1 NPM Proxy Host Configuration
|
|
|
|
**File Location (Docker):** `/data/database.sqlite` (inside NPM container)
|
|
|
|
**Proxy Host ID:** 27
|
|
|
|
**Complete Configuration:**
|
|
```json
|
|
{
|
|
"id": 27,
|
|
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
|
|
"forward_scheme": "http",
|
|
"forward_host": "57.128.200.27",
|
|
"forward_port": 5000,
|
|
"access_list_id": 0,
|
|
"certificate_id": 27,
|
|
"ssl_forced": true,
|
|
"caching_enabled": false,
|
|
"block_exploits": true,
|
|
"advanced_config": "",
|
|
"allow_websocket_upgrade": true,
|
|
"http2_support": true,
|
|
"hsts_enabled": true,
|
|
"hsts_subdomains": true
|
|
}
|
|
```
|
|
|
|
**How to Update (NPM API):**
|
|
```python
|
|
import requests
|
|
|
|
NPM_URL = "http://10.22.68.250:81/api"
|
|
# Login to get token first, then:
|
|
|
|
data = {
|
|
"domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
|
|
"forward_scheme": "http",
|
|
"forward_host": "57.128.200.27",
|
|
"forward_port": 5000, # CRITICAL!
|
|
"certificate_id": 27,
|
|
"ssl_forced": True,
|
|
"http2_support": True,
|
|
"hsts_enabled": True
|
|
}
|
|
|
|
response = requests.put(
|
|
f"{NPM_URL}/nginx/proxy-hosts/27",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
json=data
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
### 9.2 Gunicorn Configuration
|
|
|
|
**Systemd Unit File:** `/etc/systemd/system/nordabiznes.service`
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Norda Biznes Partner Flask Application
|
|
After=network.target postgresql.service
|
|
|
|
[Service]
|
|
Type=notify
|
|
User=maciejpi
|
|
Group=maciejpi
|
|
WorkingDirectory=/var/www/nordabiznes
|
|
Environment="PATH=/var/www/nordabiznes/venv/bin"
|
|
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
|
|
--bind 127.0.0.1:5000 \
|
|
--workers 4 \
|
|
--worker-class sync \
|
|
--timeout 120 \
|
|
--keep-alive 5 \
|
|
--access-logfile /var/log/nordabiznes/access.log \
|
|
--error-logfile /var/log/nordabiznes/error.log \
|
|
--log-level info \
|
|
--capture-output \
|
|
app:app
|
|
|
|
Restart=always
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
**Parameters Explained:**
|
|
- `--bind 127.0.0.1:5000` - Listen on all interfaces, port 5000
|
|
- `--workers 4` - 4 worker processes (matches CPU cores)
|
|
- `--worker-class sync` - Synchronous workers (default)
|
|
- `--timeout 120` - 120 second request timeout
|
|
- `--keep-alive 5` - 5 second keep-alive for connections
|
|
|
|
---
|
|
|
|
### 9.3 Flask Application Configuration
|
|
|
|
**Environment Variables (.env):**
|
|
```bash
|
|
# Database
|
|
DATABASE_URL=postgresql://nordabiz_app:PASSWORD@localhost:5432/nordabiz
|
|
|
|
# Flask
|
|
SECRET_KEY=random_secret_key_here
|
|
FLASK_ENV=production
|
|
|
|
# Security
|
|
SESSION_COOKIE_SECURE=True
|
|
SESSION_COOKIE_HTTPONLY=True
|
|
SESSION_COOKIE_SAMESITE=Lax
|
|
|
|
# External APIs
|
|
GOOGLE_API_KEY=AIza...
|
|
BRAVE_API_KEY=BSA...
|
|
MS_GRAPH_CLIENT_ID=abc...
|
|
MS_GRAPH_CLIENT_SECRET=def...
|
|
```
|
|
|
|
**Flask Config (app.py):**
|
|
```python
|
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
|
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
|
|
app.config['SESSION_COOKIE_SECURE'] = True
|
|
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Monitoring and Logging
|
|
|
|
### 10.1 Log Locations
|
|
|
|
**NPM Logs (Docker):**
|
|
```bash
|
|
# Access logs
|
|
ssh maciejpi@10.22.68.250 "docker logs nginx-proxy-manager_app_1 --tail 100"
|
|
|
|
# Follow logs in real-time
|
|
ssh maciejpi@10.22.68.250 "docker logs -f nginx-proxy-manager_app_1"
|
|
```
|
|
|
|
**Gunicorn Logs:**
|
|
```bash
|
|
# Application logs (systemd journal)
|
|
ssh maciejpi@57.128.200.27 "sudo journalctl -u nordabiznes -f"
|
|
|
|
# Access logs (file-based)
|
|
ssh maciejpi@57.128.200.27 "tail -f /var/log/nordabiznes/access.log"
|
|
|
|
# Error logs
|
|
ssh maciejpi@57.128.200.27 "tail -f /var/log/nordabiznes/error.log"
|
|
```
|
|
|
|
**PostgreSQL Logs:**
|
|
```bash
|
|
# Query logs (if enabled)
|
|
ssh maciejpi@57.128.200.27 "sudo tail -f /var/log/postgresql/postgresql-14-main.log"
|
|
```
|
|
|
|
### 10.2 Key Metrics to Monitor
|
|
|
|
**HTTP Metrics (NPM):**
|
|
- Requests per second (RPS)
|
|
- HTTP status codes (200, 301, 404, 500, 502, 504)
|
|
- Response time (p50, p95, p99)
|
|
- SSL certificate expiry
|
|
|
|
**Application Metrics (Gunicorn):**
|
|
- Worker utilization (all 4 workers busy?)
|
|
- Request timeout rate
|
|
- Error rate (5xx responses)
|
|
- Memory usage per worker
|
|
|
|
**Database Metrics (PostgreSQL):**
|
|
- Connection count (should be < 30)
|
|
- Query execution time
|
|
- Database size growth
|
|
- Table bloat
|
|
|
|
**System Metrics (OVH VPS):**
|
|
- CPU usage (should be < 80%)
|
|
- Memory usage (should be < 6 GB / 8 GB)
|
|
- Disk I/O (should be low)
|
|
- Network throughput
|
|
|
|
---
|
|
|
|
## 11. Future Enhancements
|
|
|
|
### 11.1 Planned Improvements
|
|
|
|
**1. CDN Integration**
|
|
- Cloudflare or custom CDN for static assets
|
|
- Reduces load on NPM and Flask
|
|
- Improves global latency
|
|
|
|
**2. Load Balancing**
|
|
- Multiple Flask backend instances
|
|
- NPM upstream configuration
|
|
- Session affinity handling
|
|
|
|
**3. Caching Layer**
|
|
- Redis cache for frequent queries
|
|
- Page cache for anonymous users
|
|
- API response caching
|
|
|
|
**4. Monitoring System**
|
|
- Zabbix integration for health checks
|
|
- Alert on 5xx errors, high latency
|
|
- Dashboard for key metrics
|
|
|
|
**5. Rate Limiting**
|
|
- NPM-level rate limiting by IP
|
|
- Protection against DDoS
|
|
- API endpoint throttling
|
|
|
|
---
|
|
|
|
## 12. Related Documentation
|
|
|
|
- **Incident Report:** `docs/INCIDENT_REPORT_20260102.md` - NPM port configuration incident
|
|
- **System Context:** `docs/architecture/01-system-context.md` - High-level system view
|
|
- **Container Diagram:** `docs/architecture/02-container-diagram.md` - Container architecture
|
|
- **Deployment Architecture:** `docs/architecture/03-deployment-architecture.md` - Infrastructure details
|
|
- **Flask Components:** `docs/architecture/04-flask-components.md` - Application structure
|
|
- **Authentication Flow:** `docs/architecture/flows/01-authentication-flow.md` - User authentication
|
|
- **Search Flow:** `docs/architecture/flows/02-search-flow.md` - Company search process
|
|
- **CLAUDE.md:** Main project documentation with NPM configuration reference
|
|
|
|
---
|
|
|
|
## Glossary
|
|
|
|
- **NPM**: Nginx Proxy Manager - Docker-based reverse proxy with web UI
|
|
- **NAT**: Network Address Translation - Fortigate translates external IP to internal
|
|
- **SSL Termination**: Decrypting HTTPS at NPM, forwarding HTTP to backend
|
|
- **HSTS**: HTTP Strict Transport Security - Forces browsers to use HTTPS
|
|
- **WSGI**: Web Server Gateway Interface - Python web application interface
|
|
- **Gunicorn**: Python WSGI HTTP server (Green Unicorn)
|
|
- **Round Robin**: Load balancing method distributing requests evenly
|
|
- **ERR_TOO_MANY_REDIRECTS**: Browser error when redirect loop detected (> 20 redirects)
|
|
|
|
---
|
|
|
|
**Document Status:** Production-ready, verified against live system
|
|
**Last Incident:** 2026-01-02 (ERR_TOO_MANY_REDIRECTS due to port 80 configuration)
|
|
**Next Review:** After any NPM configuration changes
|
|
|
|
---
|
|
|
|
*This document was created as part of the architecture documentation initiative to prevent configuration incidents and improve system understanding.*
|