nordabiz/docs/architecture/flows/06-http-request-flow.md
Maciej Pienczyn bfd48a6e20 docs: aktualizacja architektury po migracji na OVH VPS + e-deklaracja w roadmap
- 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>
2026-04-15 07:00:14 +02:00

37 KiB

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

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.

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:

    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:

    -- 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):

    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:

# 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:

    # 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:

    # 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:

    @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:

    # Flask router matches route
    @app.route('/')
    def index():
        # Main catalog page
        pass
    

    d) View Function Execution:

    @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):

    # SQLAlchemy ORM generates SQL
    SELECT * FROM companies
    WHERE status = 'active'
    ORDER BY name;
    
  6. Template Rendering (Jinja2):

    # 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:

    # 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:

# /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:

# 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:

    -- 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:

    # 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:

# 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:

# 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

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/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):

set-cookie: session=eyJ...; HttpOnly; Path=/; SameSite=Lax; Secure
vary: Cookie
x-request-id: abc123def456

Response Body:

<!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:

# 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:

{
  "companies": [
    {
      "id": 1,
      "name": "PIXLAB",
      "slug": "pixlab-sp-z-o-o",
      "category": "IT"
    }
  ],
  "total": 80
}

Headers:

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/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/2 200 OK
Content-Type: application/json

{
  "status": "healthy",
  "timestamp": "2026-01-10T10:30:00Z"
}

Use Cases:

  • Verify NPM configuration after changes
  • External monitoring (Zabbix)
  • Load balancer health checks (future)

6. Security Layers

6.1 Security Boundaries

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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# Renew certificate via NPM UI or API
# NPM auto-renews 30 days before expiry

8.2 Diagnostic Commands Reference

Quick Health Check:

# 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:

# 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:

# 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:

# 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:

# 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:

{
  "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):

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

[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):

# 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):

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):

# 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:

# 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:

# 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

  • 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.