- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner" - Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash - Zachowano historyczne odniesienia w timeline i dokumentacji Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
37 KiB
HTTP Request Flow
Document Version: 1.0 Last Updated: 2026-01-10 Status: Production LIVE 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 reverse proxy to Flask application and back. It covers:
- Complete request path (Internet → Fortigate → NPM → Flask → Response)
- Critical port configurations and routing decisions
- SSL/TLS termination and security boundaries
- Request/response transformation at each layer
- Common failure scenarios and troubleshooting
Key Infrastructure:
- Public Entry: 85.237.177.83:443 (Fortigate NAT)
- Reverse Proxy: NPM on 10.22.68.250:443 (SSL termination)
- Backend Application: Flask/Gunicorn on 10.22.68.249:5000
- Protocol Flow: HTTPS → NPM → HTTP → Flask → HTTP → NPM → HTTPS
⚠️ CRITICAL CONFIGURATION:
NPM MUST forward to port 5000, NOT port 80!
Port 80 on NORDABIZ-01 runs nginx that redirects to HTTPS
Forwarding to port 80 causes infinite redirect loop
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 Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
participant DB as 💾 PostgreSQL<br/>localhost:5432
Note over User,DB: SUCCESSFUL REQUEST FLOW
User->>Browser: Navigate to https://nordabiznes.pl/
Browser->>Fortigate: HTTPS GET / (Port 443)
Note over Fortigate: NAT Translation<br/>85.237.177.83:443 → 10.22.68.250:443
Fortigate->>NPM: HTTPS GET / (Port 443)
Note over NPM: SSL/TLS Termination<br/>Let's Encrypt Certificate<br/>nordabiznes.pl (Cert ID: 27)
Note over NPM: Request Processing<br/>• Validate certificate<br/>• Decrypt HTTPS<br/>• Extract headers<br/>• Check routing rules
NPM->>Flask: HTTP GET / (Port 5000) ✓
Note over NPM,Flask: ⚠️ CRITICAL: Port 5000<br/>NOT port 80!
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->>NPM: HTTP 200 OK<br/>Content-Type: text/html<br/>Set-Cookie: session=...<br/>HTML content
Note over NPM: Response Processing<br/>• Encrypt response (HTTPS)<br/>• Add security headers<br/>• HSTS, CSP, X-Frame-Options
NPM->>Fortigate: HTTPS 200 OK (Port 443)
Fortigate->>Browser: HTTPS 200 OK
Browser->>User: Display page
1.2 Failed Request (Wrong Port Configuration)
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)
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: Fortigate Firewall (NAT Gateway)
Server: Fortigate Firewall Public IP: 85.237.177.83 Function: Network Address Translation (NAT) + Firewall
Processing Steps:
-
Receive external request:
Source: User IP (e.g., 93.104.x.x) Destination: 85.237.177.83:443 Protocol: HTTPS -
NAT Translation:
External: 85.237.177.83:443 → Internal: 10.22.68.250:443 -
Firewall Rules:
- Allow TCP port 443 (HTTPS)
- Allow TCP port 80 (HTTP, redirects to HTTPS)
- Block all other inbound ports
- Stateful connection tracking
-
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: NPM Reverse Proxy (SSL Termination)
Server: R11-REVPROXY-01 (VM 119) IP: 10.22.68.250 Port: 443 (HTTPS) Technology: Nginx Proxy Manager (Docker)
Processing Steps:
-
Receive HTTPS request:
Source: Fortigate (10.22.68.250:443) Method: GET Host: nordabiznes.pl Protocol: HTTPS/1.1 or HTTP/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
-
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 -
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: "10.22.68.249" forward_port: 5000 ← CRITICAL! ssl_forced: true certificate_id: 27 -
⚠️ CRITICAL ROUTING DECISION:
✓ CORRECT: Forward to http://10.22.68.249:5000 ❌ WRONG: Forward to http://10.22.68.249:80 (causes redirect loop!) -
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 | 10.22.68.249 | NORDABIZ-01 backend server |
| 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"]|10.22.68.249|5000
2.3 Layer 3: Flask/Gunicorn Application (Request Processing)
Server: NORDABIZ-01 (VM 249) IP: 10.22.68.249 Port: 5000 Technology: Gunicorn 20.1.0 + Flask 3.0
Processing Steps:
-
Gunicorn Receives HTTP Request:
Binding: 0.0.0.0:5000 (all interfaces) Workers: 4 (Gunicorn worker processes) Worker Class: sync (synchronous workers) Timeout: 120 seconds -
Worker Selection:
- Gunicorn master process receives connection
- Distributes request to available worker (round-robin)
- Worker loads WSGI app (Flask application)
-
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 } -
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 headersb) 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 = Nonec) Route Matching:
# Flask router matches route @app.route('/') def index(): # Main catalog page passd) 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) -
Database Query (PostgreSQL):
# SQLAlchemy ORM generates SQL SELECT * FROM companies WHERE status = 'active' ORDER BY name; -
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) -
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=www-data
Group=www-data
WorkingDirectory=/var/www/nordabiznes
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
--bind 0.0.0.0: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://10.22.68.249:5000/health
# Expected: HTTP/1.1 200 OK
# Check Gunicorn workers
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
# Expected: 1 master + 4 worker processes
2.4 Layer 4: PostgreSQL Database (Data Retrieval)
Server: NORDABIZ-01 (same server as Flask) IP: 127.0.0.1 (localhost only) Port: 5432 Technology: PostgreSQL 14
Processing Steps:
-
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; -
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
-
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@10.22.68.249 "sudo systemctl status postgresql"
# Test connection (from server)
ssh maciejpi@10.22.68.249 "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/>10.22.68.249: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->>NPM: 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
NPM->>Fortigate: 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://10.22.68.249:5000 (CORRECT) │
│ ❌ DO NOT use: http://10.22.68.249:80 (WRONG!) │
└────────────────────────────┬────────────────────────────────────┘
│ HTTP (Port 5000) ✓
▼
┌─────────────────────────────────────────────────────────────────┐
│ FLASK/GUNICORN (NORDABIZ-01) │
│ IP: 10.22.68.249:5000 │
│ Binding: 0.0.0.0:5000 │
│ Workers: 4 (Gunicorn) │
│ Function: Application logic, template rendering │
└────────────────────────────┬────────────────────────────────────┘
│ SQL (localhost:5432)
▼
┌─────────────────────────────────────────────────────────────────┐
│ POSTGRESQL (NORDABIZ-01) │
│ IP: 127.0.0.1:5432 (localhost only) │
│ Database: nordabiz │
│ Function: Data storage │
└─────────────────────────────────────────────────────────────────┘
4.2 Port Table (NORDABIZ-01)
| Port | Service | Binding | User | Purpose | NPM Should Use? |
|---|---|---|---|---|---|
| 5000 | Gunicorn/Flask | 0.0.0.0 | www-data | 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 NORDABIZ-01 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 → NPM → 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 → NPM → 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 → NPM → 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_logintimestamp
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 → NPM → 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 |
| NPM → 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:
- Gunicorn worker count (4 workers)
- PostgreSQL connection pool (max 30)
- 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://10.22.68.249:80/
# If returns: HTTP 301 → Problem confirmed
curl -I http://10.22.68.249: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@10.22.68.249 "sudo systemctl status nordabiznes"
# Expected: Active (running)
# 2. Check if port 5000 is listening
ssh maciejpi@10.22.68.249 "sudo netstat -tlnp | grep 5000"
# Expected: 0.0.0.0:5000 ... gunicorn
# 3. Test direct connection
curl -I http://10.22.68.249:5000/health
# Expected: HTTP 200 OK
Solution:
# Restart Gunicorn
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"
# Check logs
ssh maciejpi@10.22.68.249 "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@10.22.68.249 "ps aux | grep gunicorn"
# Look for workers in state 'R' (running) vs 'S' (sleeping)
# 2. Check application logs
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"
# 3. Check database connections
ssh maciejpi@10.22.68.249 "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://10.22.68.249: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@10.22.68.249 "sudo systemctl status nordabiznes"
# Worker processes
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn | grep -v grep"
# Recent logs
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 20 --no-pager"
Database Status:
# PostgreSQL status
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"
# Connection count
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
\"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""
# Database size
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
\"SELECT pg_size_pretty(pg_database_size('nordabiz'));\""
Network Connectivity:
# Test NPM → Flask connectivity
ssh maciejpi@10.22.68.250 "curl -I http://10.22.68.249:5000/health"
# Test Flask → Database connectivity
ssh maciejpi@10.22.68.249 "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": "10.22.68.249",
"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": "10.22.68.249",
"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=www-data
Group=www-data
WorkingDirectory=/var/www/nordabiznes
Environment="PATH=/var/www/nordabiznes/venv/bin"
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
--bind 0.0.0.0: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 0.0.0.0: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@10.22.68.249 "sudo journalctl -u nordabiznes -f"
# Access logs (file-based)
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/access.log"
# Error logs
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"
PostgreSQL Logs:
# Query logs (if enabled)
ssh maciejpi@10.22.68.249 "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 (NORDABIZ-01):
- 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.