MUHAMMAD
FARI
MADYAN
[ Press ESC or Click to Skip ]

Python FastAPI + React E-Learning Rebuild

1

Part 1: Introduction & Architecture: Monolith to Decoupled Async Micro-Services

2

Part 2: Async ORM & Type Safety: SQLAlchemy 2.0 Async, Pydantic v2 & N+1 Prevention

3

Part 3: High-Precision CBT Engine: Anti-Cheat & Native WebSocket Telemetry

4

Part 4: Enterprise Security Hardening: Stateless JWT, RBAC & 1-Click Impersonation

5

Part 5: High-Throughput Excel Pipelines & Async Automated Testing

6

Part 6: Single VPS Production Deployment: Docker Compose, Nginx SSL & Health Monitoring

This article is available in Indonesian

🇮🇩 Baca dalam Bahasa Indonesia
🇬🇧 English📚 Python FastAPI + React E-Learning Rebuild

Python FastAPI E-Learning Part 6: Single VPS Production Deployment – Docker Compose, Nginx SSL & Health Monitoring

Deploy a high-concurrency Python FastAPI and React 19 application to a single Linux VPS: multi-container Docker Compose orchestration, Host Nginx reverse proxy with Let's Encrypt SSL, WebSocket upgrade tuning, and auto-recovery health monitoring scripts.

Muhammad Fari MadyanAuthor

7 min read

·

Oct 9, 2026


The Production Deployment Blueprint

Moving an asynchronous FastAPI + React application from a local development environment to an enterprise production server introduces infrastructure demands that code-level optimizations alone cannot solve:

  1. WebSocket Proxy Timeouts: Default Nginx reverse proxies drop idle TCP connections after 60 seconds. In an examination hall, this silently severs teacher proctoring screens mid-exam.
  2. Resource Isolation on Modest Hardware: Educational institutions frequently operate on cost-effective virtual private servers (e.g., 2 vCPU / 4GB RAM or 4 vCPU / 8GB RAM). The stack must comfortably orchestrate FastAPI, React, PostgreSQL 16, and Redis 7 without kernel OOM killer panics.
  3. Automated Self-Healing & Alerting: If an unexpected memory spike or database deadlock knocks the backend container offline at 08:00 AM, the system must self-heal automatically within seconds while notifying the engineering team via webhook.

In mfarim/fastapi-elearning-react, we engineered a resilient, single-VPS production architecture.

┌────────────────────────────────────────────────────────────────────────────────────────┐ │ SINGLE VPS PRODUCTION INFRASTRUCTURE │ └────────────────────────────────────────────────────────────────────────────────────────┘ HTTPS (:443) / WSS (:443) │ ▼ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ HOST SYSTEM: NGINX REVERSE PROXY & CERTBOT SSL │ │ • Let's Encrypt Auto-Renewing TLS 1.3 Certificate │ │ • WebSocket Upgrade Tunneling (proxy_read_timeout 86400s) │ │ • Gzip / Brotli Static Asset Compression │ │ • Hardened Security Headers: HSTS, XSS-Protection, X-Frame-Options │ └─────────────────────────┬───────────────────────────────┬────────────────────────┘ │ │ Proxy Pass http:// │ :3001 │ Proxy Pass http:// :8000 (Static Frontend SPA) │ │ (REST API & WebSockets) ▼ ▼ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ DOCKER COMPOSE ORCHESTRATION NETWORK (bridge: elearning-network) │ │ │ │ ┌───────────────────────┐ ┌───────────────────────────────┐ │ │ │ frontend container │ │ backend container │ │ │ │ • Nginx Alpine │ │ • Python 3.12-slim │ │ │ │ • React 19 Build │ │ • Uvicorn ASGI Workers │ │ │ └───────────────────────┘ └───────────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ ┌───────────────────────────────┐ │ │ │ postgres container │ │ redis container │ │ │ │ • PostgreSQL 16 │ │ • Redis 7 Alpine │ │ │ │ • Healthcheck pg_isready │ • Healthcheck redis-cli ping│ │ │ └───────────────────────┘ └───────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────────┘ ▲ │ Checks every 5 minutes via cron ┌─────────────────────────┴────────────────────────────────────────────────────────┐ │ HOST CRONJOB MONITOR (scripts/health-monitor.sh) │ │ • Probes GET http://localhost:8000/health │ │ • On failure: Triggers 'docker compose restart backend' │ │ • Dispatches Discord / Telegram / Slack Outage Webhook Alert │ └──────────────────────────────────────────────────────────────────────────────────┘

1. Multi-Container Docker Compose Orchestration

In docker-compose.yml, every service is defined with health checks, restart policies, and persistent storage volumes:

YAML

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    container_name: fastapi-elearning-postgres
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-elearning}
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
    ports:
      - "127.0.0.1:5434:5432"  # Bind only to localhost for security
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - elearning-network

  redis:
    image: redis:7-alpine
    container_name: fastapi-elearning-redis
    restart: always
    ports:
      - "127.0.0.1:6380:6379"  # Bind only to localhost
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - elearning-network

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: fastapi-elearning-backend
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-elearning}
      REDIS_URL: redis://redis:6379/0
      SECRET_KEY: ${SECRET_KEY}
      ACCESS_TOKEN_EXPIRE_MINUTES: 120
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - ./backend/storage:/app/storage
    networks:
      - elearning-network

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: fastapi-elearning-frontend
    restart: always
    ports:
      - "127.0.0.1:3001:80"
    networks:
      - elearning-network

volumes:
  postgres_data:
  redis_data:

networks:
  elearning-network:
    driver: bridge

Key Security Practices:

  1. Localhost Port Binding (127.0.0.1:5434): Database and Redis ports are bound strictly to loopback interfaces, preventing external scanners from accessing the database directly from the public internet.
  2. depends_on with service_healthy: The backend container waits until PostgreSQL passes its internal pg_isready probe before booting, eliminating initialization race condition crashes.

2. Production Dockerfile: Lean & Non-Root Python 3.12

In backend/Dockerfile, we construct a clean, multi-layer image based on python:3.12-slim:

DOCKERFILE

# backend/Dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install system dependencies required for asyncpg compilation & health checking
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Cache Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# Copy application source code
COPY . .

# Ensure storage directories exist
RUN mkdir -p /app/storage/uploads

EXPOSE 8000

# Launch Uvicorn with optimized worker processes
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

3. Host Nginx Reverse Proxy with WebSocket Upgrades & SSL

On the host VPS, Nginx acts as the public-facing edge gateway. The configuration below provides SSL termination, security headers, and most importantly, WebSocket connection persistence:

NGINX

# /etc/nginx/sites-available/elearning.conf

# 1. HTTP to HTTPS redirection
server {
    listen 80;
    server_name elearning.sekolah.id;
    return 301 https://$host$request_uri;
}

# 2. Production HTTPS Server
server {
    listen 443 ssl http2;
    server_name elearning.sekolah.id;

    # SSL Certificates managed by Certbot
    ssl_certificate /etc/letsencrypt/live/elearning.sekolah.id/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/elearning.sekolah.id/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Client upload limits for Excel & Assignment PDFs
    client_max_body_size 30M;

    # Hardened Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # --- REST API Proxy ---
    location /api/ {
        proxy_pass http://127.0.0.1:8000/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # --- Interactive API Documentation ---
    location /docs {
        proxy_pass http://127.0.0.1:8000/docs;
        proxy_set_header Host $host;
    }
    location /openapi.json {
        proxy_pass http://127.0.0.1:8000/openapi.json;
        proxy_set_header Host $host;
    }

    # --- WebSocket Proctoring Tunneling ---
    location /ws/ {
        proxy_pass http://127.0.0.1:8000/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # CRITICAL: Prevent Nginx from terminating idle proctoring sockets after 60s
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    # --- Frontend SPA (React 19) ---
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

The Critical WebSocket Directive: proxy_read_timeout 86400s

By default, Nginx enforces a 60-second read timeout (proxy_read_timeout 60s). If a candidate is thinking quietly about a difficult math question and no telemetry is sent over the WebSocket for 60 seconds, Nginx unilaterally severs the TCP connection.

Configuring proxy_read_timeout 86400s (24 hours) keeps the proctoring stream persistent for the entire duration of the school day.


4. Automated Health Monitoring & Auto-Recovery Script

Even the most meticulously designed applications can encounter unexpected outages. In scripts/health-monitor.sh, we implement an automated watchdog script:

BASH

#!/usr/bin/env bash
# ==============================================================================
# Production Health Monitor & Auto-Recovery Watchdog
# scripts/health-monitor.sh
# ==============================================================================
set -euo pipefail

TARGET_URL="${HEALTH_CHECK_URL:-http://localhost:8000/health}"
WEBHOOK_URL="${ALERT_WEBHOOK_URL:-https://discord.com/api/webhooks/your_webhook_here}"
TIMEOUT_SECONDS=5
PROJECT_DIR="/var/www/python-fastapi-react-elearning"
LOG_FILE="/var/log/elearning-health.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

send_alert() {
    local message="$1"
    if [ -n "$WEBHOOK_URL" ]; then
        curl -s -X POST -H "Content-Type: application/json" \
            -d "{\"content\": \"⚠️ **ALERT: E-Learning System Outage**\n$message\"}" \
            "$WEBHOOK_URL" > /dev/null 2>&1 || true
    fi
}

# 1. Probe the API health endpoint
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT_SECONDS" "$TARGET_URL" || echo "000")

if [ "$HTTP_STATUS" -eq 200 ]; then
    log "HEALTH OK: API returned HTTP 200"
    exit 0
else
    log "HEALTH FAILURE: Endpoint returned HTTP $HTTP_STATUS! Initiating recovery..."
    send_alert "FastAPI backend returned HTTP $HTTP_STATUS at $(hostname). Auto-restarting container..."

    # 2. Execute automatic container restart
    cd "$PROJECT_DIR"
    docker compose restart backend

    # 3. Wait and re-verify recovery
    sleep 10
    RETRY_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT_SECONDS" "$TARGET_URL" || echo "000")

    if [ "$RETRY_STATUS" -eq 200 ]; then
        log "AUTO-RECOVERY SUCCESS: Backend restored to healthy status (HTTP 200)"
        send_alert "✅ **RECOVERY SUCCESS**: FastAPI backend successfully restored at $(hostname)."
    else
        log "CRITICAL: Auto-recovery failed! Manual engineer intervention required."
        send_alert "🚨 **CRITICAL ALERT**: Auto-recovery failed! Backend remains down (HTTP $RETRY_STATUS)."
    fi
fi

Scheduling via Host Cron

Configure the watchdog to run every 5 minutes:

BASH

# Add to host crontab (crontab -e):
*/5 * * * * /var/www/python-fastapi-react-elearning/scripts/health-monitor.sh >> /var/log/elearning-cron.log 2>&1

If an outage occurs during off-hours, the watchdog automatically restarts the backend container within 5 minutes, restores normal service, and alerts the engineering team on Slack or Discord with the exact downtime window.


Complete Series Summary & Architectural Recap

Over the course of this 6-part series, we took a monolithic Laravel examination platform and transformed it into a cloud-ready, asynchronous microservice-capable architecture:

PartTopicArchitectural Highlight
Part 1Architecture & Monolith MigrationSynchronous PHP-FPM vs Asynchronous ASGI runtime, full stack comparison (Laravel vs Java vs Python).
Part 2Async ORM & Type SafetySQLAlchemy 2.0 Async, selectinload N+1 prevention, and expire_on_commit=False greenlet safety.
Part 3High-Precision CBT EngineServer-authoritative start times, 5-strike anti-cheat threshold, and native ASGI WebSockets with Redis Pub/Sub.
Part 4Enterprise Security HardeningStateless JWT tokens, declarative RBAC, 1-Click admin impersonation with zero-reauth session recovery.
Part 5Data Pipelines & Quality AssuranceStreaming openpyxl ingestion (<15MB RAM), atomic batch database transactions, and Pytest-Asyncio.
Part 6Single VPS Production DeploymentMulti-container Docker Compose, Nginx SSL WebSocket reverse proxying, and auto-recovery monitoring scripts.

All source code, Docker configs, and automated test suites are fully open-source and available on GitHub at mfarim/fastapi-elearning-react.

Continue Reading

Previous article

← Previous Article

Python FastAPI E-Learning Part 5: High-Throughput Excel Pipelines & Async Automated Testing with Pytest

Next Article →

Rebuilding Laravel E-Learning to Ruby on Rails & React: Convention over Configuration, Multithreaded Puma & Cloud-Ready Migration

Next article