MUHAMMAD
FARI
MADYAN
[ Press ESC or Click to Skip ]

Rebuild Ruby on Rails + React E-Learning

1

Part 1: Pengenalan & Arsitektur: Dari Monolit ke Arsitektur Multithreaded Terpisah

2

Part 2: Penerapan Clean Architecture di Ruby on Rails & React: Repository Pattern, Use Cases, Result Monad & Typed Services (PR #1)

3

Part 3: Engine Ujian CBT Presisi Tinggi & Telemetri Pengawasan Real-Time Action Cable

4

Part 4: Pipeline Data Berkecepatan Tinggi dengan Roo & Penguatan Keamanan DevSecOps

5

Part 5: Single VPS Production Deployment: Docker Compose, Tuning Multithreading Puma & Monitoring Pemulihan Otomatis

Artikel ini tersedia dalam Bahasa Inggris

🇬🇧 Read in English
🇮🇩 Bahasa Indonesia📚 Rebuild Ruby on Rails + React E-Learning

Ruby on Rails 8 & React E-Learning Part 5: Single VPS Production Deployment – Docker Compose, Tuning Multithreading Puma & Monitoring Pemulihan Otomatis

Rilis aplikasi performa tinggi Ruby on Rails 8 API dan React 19 ke server Linux VPS tunggal: orkestrasi multi-kontainer Docker Compose, tuning konkurensi Puma, Host Nginx reverse proxy dengan SSL & Action Cable WebSockets, serta skrip watchdog monitoring auto-recovery.

Muhammad Fari MadyanPenulis

8 menit baca

·

16 Oktober 2026


Tantangan Infrastruktur Lingkungan Produksi

Merilis aplikasi modern berbasis Ruby on Rails 8 API dan React 19 Single Page Application (SPA) ke server produksi membutuhkan pengelolaan infrastruktur yang tidak dapat diselesaikan hanya melalui optimasi kode semata:

  1. Tunneling Koneksi Persisten WebSocket: Action Cable membutuhkan koneksi TCP berumur panjang yang terus terbuka. Konfigurasi reverse proxy standar akan memutus koneksi idle setelah 60 detik, yang tanpa sengaja mematikan layar telemetri pengawas guru di tengah ujian.
  2. Efisiensi Alokasi Memori pada Server VPS Terjangkau: Institusi pendidikan umumnya menggunakan Virtual Private Server (VPS) dengan kapasitas hardware terukur (misal: 2 vCPU / 4GB RAM atau 4 vCPU / 8GB RAM). Seluruh stack (Rails, Puma, PostgreSQL 16, Redis 7, dan React) harus mampu berjalan berdampingan tanpa memicu insiden Out-Of-Memory (OOM) killer Linux.
  3. Mekanisme Pemulihan Otomatis (Self-Healing): Jika lonjakan konkurensi memicu lonjakan memori atau crash pada kontainer Rails, sistem harus sanggup mendeteksi kegagalan tersebut dan merestart kontainer dalam hitungan detik sembari mengirimkan alert webhook ke tim teknis.

Pada proyek mfarim/ruby-rails-elearning-react, kami merancang arsitektur produksi tangguh berbiaya efisien pada satu server VPS Linux (single VPS).

┌────────────────────────────────────────────────────────────────────────────────────────┐ │ INFRASTRUKTUR PRODUKSI SERVER VPS TUNGGAL │ └────────────────────────────────────────────────────────────────────────────────────────┘ HTTPS (:443) / WSS (:443) │ ▼ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ SISTEM HOST: NGINX REVERSE PROXY & CERTBOT SSL │ │ • Sertifikat TLS 1.3 Otomatis dari Let's Encrypt │ │ • Tunneling Action Cable WebSocket Upgrade (proxy_read_timeout 86400s) │ │ • Kompresi Aset Statis Gzip / Brotli │ │ • Header Keamanan Ketat: HSTS, X-Frame-Options, XSS Protection │ └─────────────────────────┬───────────────────────────────┬────────────────────────┘ │ │ Proxy Pass http:// │ :3001 │ Proxy Pass http:// :8000 (Aplikasi Frontend SPA)│ │ (REST API & Action Cable) ▼ ▼ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ JARINGAN ORKESTRASI DOCKER COMPOSE (bridge: elearning-network) │ │ │ │ ┌───────────────────────┐ ┌───────────────────────────────┐ │ │ │ frontend container │ │ backend container │ │ │ │ • Nginx Alpine │ │ • Ruby 3.3-slim │ │ │ │ • React 19 Build │ │ • Server Puma Rails 8 │ │ │ └───────────────────────┘ └───────────────┬───────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ ┌───────────────────────────────┐ │ │ │ postgres container │ │ redis container │ │ │ │ • PostgreSQL 16 │ │ • Redis 7 Alpine │ │ │ │ • Healthcheck pg_isready │ • Pub/Sub Action Cable │ │ │ └───────────────────────┘ └───────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────────────┘ ▲ │ Pengecekan berkala tiap 5 menit via cron ┌─────────────────────────┴────────────────────────────────────────────────────────┐ │ SKRIP WATCHDOG HOST CRONJOB (scripts/health-monitor.sh) │ │ • Menguji endpoint GET http://localhost:8000/api/v1/health │ │ • Jika gagal: Mengeksekusi 'docker compose restart backend' │ │ • Mengirim notifikasi darurat via Webhook Discord / Telegram / Slack │ └──────────────────────────────────────────────────────────────────────────────────┘

1. Orkestrasi Multi-Kontainer dengan Docker Compose

Pada berkas docker-compose.yml, setiap layanan didefinisikan secara modular dengan health check, kebijakan restart otomatis, dan persistent storage volume:

YAML

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    container_name: rails-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:5433:5432"  # Ikat khusus ke localhost
    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: rails-elearning-redis
    restart: always
    ports:
      - "127.0.0.1:6381:6379"  # Ikat khusus ke 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: rails-elearning-backend
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-elearning}
      REDIS_URL: redis://redis:6379/1
      SECRET_KEY: ${SECRET_KEY}
      RAILS_ENV: production
      RAILS_MAX_THREADS: 5
      WEB_CONCURRENCY: 2
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - ./backend/storage:/app/storage
    networks:
      - elearning-network

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

volumes:
  postgres_data:
  redis_data:

networks:
  elearning-network:
    driver: bridge

Kebijakan Keamanan Produksi:

  1. Isolasi Port Localhost (127.0.0.1:5433): Seluruh port database PostgreSQL dan Redis hanya mendengarkan request dari dalam localhost VPS, mencegah pemindaian port terbuka secara langsung dari internet publik.
  2. Pengecekan Kesiapan Layanan (service_healthy): Kontainer Rails menunda proses booting hingga PostgreSQL benar-benar siap menerima koneksi via pg_isready, mengeliminasi error database connection refused saat server di-reboot.

2. Dockerfile Produksi: Ruby 3.3 Slim yang Ramping

Pada backend/Dockerfile, kami mengonstruksi image multi-layer berbasis ruby:3.3-slim:

DOCKERFILE

# backend/Dockerfile
FROM ruby:3.3-slim

WORKDIR /app

# Pasang pustaka sistem yang dibutuhkan untuk kompilasi gem native (pg, nokogiri, roo)
RUN apt-get update -qq && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

# Pasang bundler
RUN gem install bundler --no-document

# Cache dependensi Gemfile
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local without 'development test' && \
    bundle install --jobs 4 --retry 3

# Salin kode aplikasi
COPY . .

# Buat direktori penyimpanan
RUN mkdir -p storage log tmp/pids

EXPOSE 8000

# Eksekusi migrasi database otomatis dan nyalakan server Puma
CMD ["bin/docker-entrypoint"]

3. Tuning Konkurensi & Multithreading Server Puma

Pada berkas backend/config/puma.rb, server Puma dikonfigurasi untuk memanfaatkan multithreading dan mode klaster (clustered mode):

RUBY

# backend/config/puma.rb
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

port ENV.fetch("PORT") { 8000 }
environment ENV.fetch("RAILS_ENV") { "production" }
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

# Aktifkan mode worker ganda pada server multi-core
workers ENV.fetch("WEB_CONCURRENCY") { 2 }

# Muat aplikasi sebelum forking untuk efisiensi RAM Copy-on-Write (CoW)
preload_app!

Memahami Multithreading Ruby MRI & Mekanisme GVL

Mitos umum yang sering beredar di kalangan developer adalah bahwa "Ruby tidak bisa menjalankan multithreading karena adanya Global VM Lock (GVL)."

Kenyataannya, pada aplikasi web yang sarat I/O (I/O bound), setiap kali sebuah thread menunggu hasil query PostgreSQL, pembacaan Redis, atau operasi disk, Ruby secara otomatis melepas GVL. Dengan menyetel RAILS_MAX_THREADS=5, satu proses worker Puma dapat melayani 5 request bersamaan secara efisien dengan konsumsi RAM hanya ~120MB—menghasilkan throughput 3x lebih tinggi dibanding arsitektur lawas satu proses per request.


4. Host Nginx Reverse Proxy dengan WSS & SSL

Pada sistem operasi host VPS, Nginx bertindak sebagai gerbang terdepan untuk terminasi SSL dan perutean lalu lintas jaringan:

NGINX

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

# 1. Redirection Otomatis HTTP ke HTTPS
server {
    listen 80;
    server_name rails-elearning.sekolah.id;
    return 301 https://$host$request_uri;
}

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

    # Sertifikat SSL dari Let's Encrypt via Certbot
    ssl_certificate /etc/letsencrypt/live/rails-elearning.sekolah.id/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/rails-elearning.sekolah.id/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Batas ukuran upload berkas Excel & PDF
    client_max_body_size 30M;

    # Header Keamanan Tingkat Lanjut
    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;

    # --- Proxy REST API ---
    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;
    }

    # --- Tunneling Action Cable WebSocket ---
    location /cable {
        proxy_pass http://127.0.0.1:8000/cable;
        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;
        
        # KRUSIAL: Mencegah Nginx memutuskan koneksi WebSocket pengawas ujian setelah 60s idle
        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;
    }
}

Konfigurasi Krusial: proxy_read_timeout 86400s

Secara default, Nginx memiliki batas waktu baca koneksi sebesar 60 detik (proxy_read_timeout 60s). Jika siswa sedang berkonsentrasi membaca soal esai dan tidak ada aktivitas transfer data selama 60 detik, Nginx akan memutus koneksi socket. Penyetelan proxy_read_timeout 86400s memastikan sesi telemetri pengawas tetap aktif sepanjang jam ujian sekolah.


5. Skrip Monitoring Kesehatan & Pemulihan Otomatis (Auto-Recovery)

Untuk menjamin tingkat ketersediaan tinggi (high availability), skrip watchdog otomatis dijalankan melalui cron host server:

BASH

#!/usr/bin/env bash
# ==============================================================================
# Skrip Watchdog Monitoring Kesehatan & Auto-Recovery Produksi
# scripts/health-monitor.sh
# ==============================================================================
set -euo pipefail

TARGET_URL="${HEALTH_CHECK_URL:-http://localhost:8000/api/v1/health}"
WEBHOOK_URL="${ALERT_WEBHOOK_URL:-https://discord.com/api/webhooks/your_webhook_here}"
TIMEOUT_SECONDS=5
PROJECT_DIR="/var/www/ruby-rails-elearning-react"
LOG_FILE="/var/log/rails-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: Insiden Sistem E-Learning Rails**\n$message\"}" \
            "$WEBHOOK_URL" > /dev/null 2>&1 || true
    fi
}

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 "KONDISI NORMAL: API mengembalikan HTTP 200"
    exit 0
else
    log "KEGAGALAN SISTEM: API mengembalikan kode HTTP $HTTP_STATUS! Memulai prosedur auto-recovery..."
    send_alert "Rails API backend tidak merespons (HTTP $HTTP_STATUS) di server $(hostname). Melakukan restart kontainer otomatis..."

    cd "$PROJECT_DIR"
    docker compose restart backend

    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 "PEMULIHAN BERHASIL: Backend kembali normal (HTTP 200)"
        send_alert "✅ **PEMULIHAN BERHASIL**: Rails API berhasil pulih dan aktif kembali di server $(hostname)."
    else
        log "KRITIKAL: Auto-recovery gagal! Membutuhkan intervensi manual tim engineer."
        send_alert "🚨 **PERINGATAN KRITIKAL**: Auto-recovery gagal! Backend tetap mati (HTTP $RETRY_STATUS)."
    fi
fi

Penjadwalan Berkala via Cron Host

Daftarkan skrip ke crontab server VPS untuk berjalan setiap 5 menit:

BASH

*/5 * * * * /var/www/ruby-rails-elearning-react/scripts/health-monitor.sh >> /var/log/rails-health-cron.log 2>&1

Rekapitulasi Lengkap 5 Bab Series Rebuild

Melalui rangkaian 5 bab komprehensif ini, kita telah mentransformasikan platform ujian monolitik Laravel menjadi arsitektur modern berkecepatan tinggi berbasis Ruby on Rails 8 API:

PartTopik PembahasanIntisari Rekayasa Arsitektur
Part 1Arsitektur & Migrasi MonolitKonsep Convention over Configuration, multithreading Puma, serta decoupling backend dan frontend.
Part 2Penerapan Clean Architecture & Use CasesMenuntaskan dilema model gemuk (fat models), Repository Pattern, Use Cases berprinsip SRP, serta Result Monad (PR #1).
Part 3Engine Ujian CBT Presisi TinggiSisa waktu otoritatif server, ambang batas 5x pelanggaran curang, dan telemetri pengawasan Action Cable.
Part 4Pipeline Data & Penguatan DevSecOpsStreaming spreadsheet massal hemat memori dengan Roo (<15MB RAM), transaksi batch atomik, serta audit SAST Brakeman.
Part 5Deployment VPS & Monitoring ProduksiMulti-kontainer Docker Compose, tuning konkurensi Puma, Nginx SSL reverse proxy, serta skrip monitoring auto-recovery.

Seluruh kode sumber, konfigurasi Docker, dan pengujian otomatis RSpec tersedia secara open-source di GitHub: mfarim/ruby-rails-elearning-react.

Lanjut Membaca

Previous article thumbnail

← Artikel Sebelumnya

Ruby on Rails 8 & React E-Learning Part 4: High-Throughput Data Ingestion with Roo & DevSecOps Security Hardening

Artikel Selanjutnya →

Fleet Management System Part 1: Introduction & System Architecture

Next article thumbnail