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:
- 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.
- 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.
- 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).
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:
- 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. - Pengecekan Kesiapan Layanan (
service_healthy): Kontainer Rails menunda proses booting hingga PostgreSQL benar-benar siap menerima koneksi viapg_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:
| Part | Topik Pembahasan | Intisari Rekayasa Arsitektur |
|---|---|---|
| Part 1 | Arsitektur & Migrasi Monolit | Konsep Convention over Configuration, multithreading Puma, serta decoupling backend dan frontend. |
| Part 2 | Penerapan Clean Architecture & Use Cases | Menuntaskan dilema model gemuk (fat models), Repository Pattern, Use Cases berprinsip SRP, serta Result Monad (PR #1). |
| Part 3 | Engine Ujian CBT Presisi Tinggi | Sisa waktu otoritatif server, ambang batas 5x pelanggaran curang, dan telemetri pengawasan Action Cable. |
| Part 4 | Pipeline Data & Penguatan DevSecOps | Streaming spreadsheet massal hemat memori dengan Roo (<15MB RAM), transaksi batch atomik, serta audit SAST Brakeman. |
| Part 5 | Deployment VPS & Monitoring Produksi | Multi-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.

