Web Server

Web Server #

Ekosistem web Ruby dibangun di atas satu abstraksi yang menyatukan semuanya: Rack. Rack adalah antarmuka standar antara web server (Puma, Unicorn, WEBrick) dan aplikasi web Ruby (Rails, Sinatra, atau bahkan aplikasi Rack murni). Karena semua server dan framework Ruby berbicara “bahasa Rack”, kamu bisa mengganti server tanpa mengubah satu baris kode aplikasi. Artikel ini membahas Rack sebagai fondasi, perbandingan mendalam setiap web server, cara mengkonfigurasi Puma untuk produksi, arsitektur reverse proxy dengan Nginx, zero-downtime deployment, dan keamanan yang tidak boleh diabaikan.

Rack — Fondasi Semua Web Ruby #

Rack mendefinisikan kontrak sederhana: sebuah aplikasi Rack adalah objek Ruby apapun yang merespons method call dengan argumen env (Hash berisi informasi request) dan mengembalikan Array tiga elemen [status, headers, body].

# Aplikasi Rack paling sederhana yang pernah ada
app = lambda do |env|
  [
    200,                                          # HTTP status code
    { "Content-Type" => "text/plain" },           # Response headers
    ["Halo dari aplikasi Rack!"]                  # Response body (Enumerable)
  ]
end

# Jalankan dengan server apapun yang mendukung Rack
# Simpan sebagai config.ru dan jalankan: rackup
# config.ru — file konfigurasi standar untuk semua aplikasi Rack
# Rack membaca file ini saat server dijalankan

# Aplikasi Rack sebagai kelas
class AplikasiSederhana
  def call(env)
    request  = Rack::Request.new(env)
    response = Rack::Response.new

    case request.path
    when "/"
      response.write "Selamat datang!"
      response.status = 200
    when "/about"
      response.write "Tentang kami"
      response.status = 200
    else
      response.write "404 - Halaman tidak ditemukan"
      response.status = 404
    end

    response.finish
  end
end

run AplikasiSederhana.new
# Jalankan dengan rackup (menggunakan WEBrick secara default)
rackup config.ru

# Tentukan server dan port
rackup config.ru --server puma --port 3000

# Mode produksi
rackup config.ru -E production -p 80

Rack Middleware #

Middleware adalah lapisan yang “membungkus” aplikasi — memproses request sebelum sampai ke aplikasi dan/atau memodifikasi response sebelum dikirim ke klien. Ini adalah pola yang sangat powerful dan digunakan secara ekstensif oleh Rails:

# Middleware custom
class LoggingMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    mulai = Time.now
    status, headers, body = @app.call(env)   # teruskan ke aplikasi
    durasi = ((Time.now - mulai) * 1000).round(2)

    puts "[#{Time.now.strftime('%H:%M:%S')}] #{env['REQUEST_METHOD']} " \
         "#{env['PATH_INFO']}#{status} (#{durasi}ms)"

    [status, headers, body]
  end
end

class AuthMiddleware
  def initialize(app, token_wajib)
    @app          = app
    @token_wajib  = token_wajib
  end

  def call(env)
    token = env["HTTP_AUTHORIZATION"]&.gsub("Bearer ", "")

    if token == @token_wajib
      @app.call(env)
    else
      [
        401,
        { "Content-Type" => "application/json" },
        ['{"error":"Unauthorized"}']
      ]
    end
  end
end

# Susun middleware stack di config.ru
use LoggingMiddleware
use AuthMiddleware, ENV["API_TOKEN"]
run AplikasiSederhana.new
flowchart TD
    A[HTTP Request] --> B[LoggingMiddleware]
    B --> C[AuthMiddleware]
    C --> D[RateLimitMiddleware]
    D --> E[Aplikasi Rack]
    E --> D2[RateLimitMiddleware]
    D2 --> C2[AuthMiddleware]
    C2 --> B2[LoggingMiddleware]
    B2 --> F[HTTP Response]

Perbandingan Web Server Ruby #

WEBrick:
  Model:    Single-threaded (Ruby 1.x), multi-thread (Ruby 2+)
  Terbaik:  Development lokal, belajar, prototipe
  Hindari:  Production apapun

Puma:
  Model:    Multi-thread + multi-process (cluster mode)
  Terbaik:  Production Rails dan Sinatra, thread-safe apps
  Standard: Default Rails 5+ dan Heroku

Unicorn:
  Model:    Pre-fork multi-process, single-thread per worker
  Terbaik:  CPU-bound apps, lingkungan yang tidak thread-safe
  Cocok:    Ketika butuh isolasi penuh antar request

Passenger (mod_rack):
  Model:    Multi-process, terintegrasi dengan Nginx/Apache
  Terbaik:  Deployment tradisional VPS, mudah dikonfigurasi
  Bonus:    Panel monitoring bawaan, rolling restart otomatis

Falcon:
  Model:    Async/fiber-based, HTTP/2 native
  Terbaik:  Aplikasi dengan banyak I/O concurrent
  Baru:     Ekosistem masih berkembang

WEBrick — Server Bawaan untuk Development #

WEBrick adalah bagian dari standard library Ruby dan tidak perlu instalasi tambahan. Cocok untuk belajar dan development ringan:

require 'webrick'

# Server HTTP dasar dengan WEBrick
server = WEBrick::HTTPServer.new(
  Port:            8080,
  DocumentRoot:    Dir.pwd,
  Logger:          WEBrick::Log.new("/dev/null"),   # matikan log
  AccessLog:       []
)

# Tambahkan servlet (handler) untuk path tertentu
server.mount_proc "/api/halo" do |req, res|
  res.content_type = "application/json"
  res.body = JSON.generate({
    pesan: "Halo!",
    waktu: Time.now.iso8601,
    method: req.request_method,
    path: req.path
  })
end

server.mount_proc "/api/user" do |req, res|
  case req.request_method
  when "GET"
    res.body = JSON.generate({ id: 1, nama: "Rina" })
  when "POST"
    data = JSON.parse(req.body)
    res.status = 201
    res.body = JSON.generate({ berhasil: true, data: data })
  else
    res.status = 405
    res.body = "Method Not Allowed"
  end
end

# Tangani Ctrl+C
trap("INT") { server.shutdown }

puts "WEBrick berjalan di http://localhost:8080"
server.start

Puma — Server Produksi Standar Rails #

Puma adalah web server yang direkomendasikan untuk Rails di production. Ia menggunakan model hybrid: multiple worker process (fork), masing-masing dengan multiple thread.

# Gemfile
gem "puma", "~> 6.4"

Konfigurasi Puma untuk Produksi #

# config/puma.rb

# === Thread Configuration ===
# Setiap worker memiliki thread_count thread
# Thread berguna untuk I/O-bound tasks (query DB, HTTP request)
threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS", 5))
threads threads_count, threads_count

# === Worker Configuration (Cluster Mode) ===
# Worker = forked processes, satu per CPU core biasanya
# Worker berguna untuk CPU-bound tasks dan isolasi memori
workers Integer(ENV.fetch("WEB_CONCURRENCY", 2))

# === Preload App ===
# Load aplikasi sebelum fork worker — lebih efisien (Copy-on-Write)
# WAJIB jika menggunakan workers > 1
preload_app!

# === Server Socket ===
port ENV.fetch("PORT", 3000)
environment ENV.fetch("RACK_ENV", "development")

# === Callbacks ===
on_worker_boot do
  # Re-establish koneksi database setelah fork
  # Penting! Koneksi tidak bisa dibagi antar proses
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

on_worker_shutdown do
  # Cleanup resource sebelum worker dimatikan
  ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
end

before_fork do
  # Jalankan sebelum fork terjadi
  # Tutup koneksi yang tidak perlu sebelum fork
end

# === Timeout ===
# Worker yang tidak merespons dalam waktu ini akan di-kill dan di-restart
worker_timeout 60

# === Binding via Unix Socket (lebih cepat dari TCP) ===
# Gunakan ini jika Nginx berjalan di mesin yang sama
bind "unix:///tmp/puma.sock"
# atau TCP:
# bind "tcp://0.0.0.0:3000"

# === Logging ===
stdout_redirect "log/puma.stdout.log", "log/puma.stderr.log", true

# === PID File ===
pidfile "tmp/pids/puma.pid"
state_path "tmp/pids/puma.state"

# === Plugin ===
plugin :tmp_restart   # restart dengan `touch tmp/restart.txt`
# Jalankan Puma
bundle exec puma -C config/puma.rb

# Jalankan sebagai daemon (background)
bundle exec puma -C config/puma.rb --daemon

# Restart graceful (tanpa downtime)
bundle exec pumactl restart

# Status worker
bundle exec pumactl stats

# Hot reload (phased restart — satu worker per satu)
bundle exec pumactl phased-restart

Cara Kerja Puma Cluster Mode #

Master Process
├── Worker 1 (PID: 1234) — 5 threads
│   ├── Thread 1: Melayani request
│   ├── Thread 2: Menunggu query DB
│   ├── Thread 3: Idle
│   ├── Thread 4: Menunggu HTTP response eksternal
│   └── Thread 5: Idle
├── Worker 2 (PID: 1235) — 5 threads
│   └── (sama seperti Worker 1)
└── Worker 3 (PID: 1236) — 5 threads
    └── (sama seperti Worker 1)

Total kapasitas concurrent: 3 workers × 5 threads = 15 request bersamaan

Unicorn — Pre-fork Multi-process #

Unicorn menggunakan model pre-fork: master process mem-fork sejumlah worker sebelum ada request masuk. Setiap worker adalah proses independen yang menangani satu request pada satu waktu.

# Gemfile
gem "unicorn", "~> 6.1"
# config/unicorn.rb

# Jumlah worker process — satu per CPU core biasanya
worker_processes Integer(ENV.fetch("WEB_CONCURRENCY", 4))

# Direktori kerja aplikasi
working_directory "/var/www/app"

# Socket tempat Unicorn mendengarkan
listen "/tmp/unicorn.sock", backlog: 64
listen 3000, tcp_nopush: true

# Timeout sebelum worker di-kill
timeout 30

# PID file
pid "/var/www/app/tmp/pids/unicorn.pid"

# Log
stderr_path "log/unicorn.stderr.log"
stdout_path "log/unicorn.stdout.log"

# Preload aplikasi untuk Copy-on-Write efficiency
preload_app true

# Callback setelah fork worker baru
after_fork do |server, worker|
  # Re-establish koneksi setelah fork
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end

  # Tutup koneksi Redis yang di-inherit dari master
  if defined?(Sidekiq)
    Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! }
  end
end

before_fork do |server, worker|
  # Matikan koneksi DB di master sebelum fork
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end
end
# Jalankan Unicorn
bundle exec unicorn -c config/unicorn.rb

# Zero-downtime restart dengan USR2
kill -USR2 $(cat tmp/pids/unicorn.pid)

# Graceful shutdown
kill -QUIT $(cat tmp/pids/unicorn.pid)

# Kurangi jumlah worker (tanpa restart)
kill -TTOU $(cat tmp/pids/unicorn.pid)

# Tambah worker (tanpa restart)
kill -TTIN $(cat tmp/pids/unicorn.pid)

Nginx sebagai Reverse Proxy #

Dalam production, Puma atau Unicorn tidak di-expose langsung ke internet. Nginx berdiri di depan sebagai reverse proxy yang menangani koneksi klien, SSL termination, static files, dan meneruskan request ke aplikasi Ruby:

Internet → Nginx (port 443/80) → Puma/Unicorn (Unix socket / port 3000)
# /etc/nginx/sites-available/myapp

upstream puma_app {
  # Lebih cepat dari TCP karena tidak ada network overhead
  server unix:///tmp/puma.sock fail_timeout=0;
}

# Redirect HTTP ke HTTPS
server {
  listen 80;
  server_name example.com www.example.com;
  return 301 https://$server_name$request_uri;
}

server {
  listen 443 ssl http2;
  server_name example.com www.example.com;

  # SSL Certificate (gunakan Let's Encrypt / Certbot)
  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_protocols       TLSv1.2 TLSv1.3;
  ssl_ciphers         HIGH:!aNULL:!MD5;

  root /var/www/app/public;

  # Gzip compression
  gzip on;
  gzip_types text/plain application/json application/javascript text/css;

  # Serve static files langsung dari Nginx — jauh lebih cepat!
  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    try_files $uri =404;
  }

  # Halaman error statis Rails
  error_page 500 502 503 504 /500.html;
  error_page 404 /404.html;

  location / {
    try_files $uri/index.html $uri @puma;
  }

  location @puma {
    proxy_pass http://puma_app;

    # Header penting untuk aplikasi Rails
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header Host              $http_host;

    proxy_redirect off;
    proxy_read_timeout    300;
    proxy_connect_timeout 300;
    proxy_send_timeout    300;

    # Buffer settings
    proxy_buffering    on;
    proxy_buffer_size  4k;
    proxy_buffers      8 4k;
  }

  # Batasi ukuran upload
  client_max_body_size 10M;
  keepalive_timeout    10;

  # 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 Referrer-Policy "strict-origin-when-cross-origin" always;
}
# Test konfigurasi Nginx
nginx -t

# Reload konfigurasi tanpa restart (zero-downtime)
nginx -s reload

# Aktifkan site
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -s reload

Zero-Downtime Deployment #

Deployment tanpa downtime adalah kebutuhan mutlak untuk aplikasi production. Berikut beberapa strategi:

Puma Phased Restart #

# Phased restart — restart worker satu per satu
# Selama proses, selalu ada worker yang siap melayani request
bundle exec pumactl phased-restart

# Atau dengan signal
kill -SIGUSR1 $(cat tmp/pids/puma.pid)

Unicorn Hot Reload (USR2) #

# 1. Kirim USR2 — master baru di-fork, master lama tetap jalan
kill -USR2 $(cat tmp/pids/unicorn.pid)

# 2. Master baru load kode terbaru dan fork worker baru
# 3. Setelah master baru siap, kirim WINCH ke master lama
#    (matikan worker lama secara graceful)
kill -WINCH $(cat tmp/pids/unicorn.pid.oldbin)

# 4. Jika berhasil, kirim QUIT ke master lama
kill -QUIT $(cat tmp/pids/unicorn.pid.oldbin)

# Jika ada masalah, rollback: kirim HUP ke master lama
kill -HUP $(cat tmp/pids/unicorn.pid.oldbin)

Deployment dengan Capistrano #

# Capfile
require "capistrano/setup"
require "capistrano/deploy"
require "capistrano/rbenv"
require "capistrano/bundler"
require "capistrano/rails"
require "capistrano/puma"

install_plugin Capistrano::Puma

# config/deploy.rb
set :application, "myapp"
set :repo_url,    "[email protected]:akun/myapp.git"
set :deploy_to,   "/var/www/myapp"

set :rbenv_ruby, File.read(".ruby-version").strip
set :bundle_flags, "--deployment --quiet"

# Symlink file yang tidak ikut Git
set :linked_files, %w[.env config/master.key]
set :linked_dirs,  %w[log tmp/pids tmp/cache tmp/sockets public/uploads]

namespace :deploy do
  after :finishing, "puma:restart"
end
# Deploy ke production
bundle exec cap production deploy

# Rollback ke versi sebelumnya
bundle exec cap production deploy:rollback

Monitoring Web Server #

# Puma stats endpoint — aktifkan di config/puma.rb
activate_control_app "unix:///tmp/pumactl.sock"

# Atau via plugin
plugin :tmp_restart
# Cek status Puma
bundle exec pumactl stats

# Output contoh:
# {"started_at":"2024-08-15T07:30:00Z",
#  "workers":2,
#  "phase":0,
#  "booted_workers":2,
#  "old_workers":0,
#  "worker_status":[
#    {"started_at":"...","pid":1234,"index":0,"phase":0,
#     "booted":true,"last_checkin":"...","last_status":{
#       "backlog":0,"running":3,"pool_capacity":2,"max_threads":5
#     }
#    }
#  ]}

# Monitor dengan systemd
# /etc/systemd/system/puma.service
# /etc/systemd/system/puma.service
[Unit]
Description=Puma HTTP Server untuk MyApp
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp/current
ExecStart=/home/deploy/.rbenv/bin/rbenv exec bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
Restart=always
RestartSec=5

# Environment
Environment=RAILS_ENV=production
EnvironmentFile=/var/www/myapp/shared/.env

# Limits
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
# Manage dengan systemd
systemctl enable puma
systemctl start puma
systemctl status puma
systemctl restart puma

# Lihat log
journalctl -u puma -f

Membangun Aplikasi Rack Sederhana (Tanpa Framework) #

Rack murni cocok untuk microservice atau API sederhana yang tidak butuh keseluruhan Rails:

# app.rb — API Rack tanpa framework
require 'rack'
require 'json'

class Router
  def initialize
    @routes = {}
  end

  def get(path, &handler)
    @routes[["GET", path]] = handler
  end

  def post(path, &handler)
    @routes[["POST", path]] = handler
  end

  def call(env)
    request  = Rack::Request.new(env)
    key      = [request.request_method, request.path_info]
    handler  = @routes[key]

    if handler
      begin
        hasil = handler.call(request)
        [200, { "Content-Type" => "application/json" }, [JSON.generate(hasil)]]
      rescue => e
        [500, { "Content-Type" => "application/json" },
         [JSON.generate({ error: e.message })]]
      end
    else
      [404, { "Content-Type" => "application/json" },
       [JSON.generate({ error: "Route tidak ditemukan: #{request.path_info}" })]]
    end
  end
end

# Definisikan routes
api = Router.new

api.get "/health" do |req|
  { status: "ok", waktu: Time.now.iso8601 }
end

api.get "/produk" do |req|
  [
    { id: 1, nama: "Laptop",   harga: 15_000_000 },
    { id: 2, nama: "Mouse",    harga:    350_000 },
    { id: 3, nama: "Keyboard", harga:    450_000 }
  ]
end

api.post "/produk" do |req|
  data = JSON.parse(req.body.read)
  { id: rand(100..999), **data.transform_keys(&:to_sym) }
end

# config.ru
use Rack::CommonLogger   # log setiap request
use Rack::Deflater       # gzip compression otomatis
run api

Keamanan Web Server #

Checklist keamanan deployment Ruby:
  ✓ Selalu gunakan HTTPS — Let's Encrypt gratis dan otomatis
  ✓ Security headers di Nginx (X-Frame-Options, CSP, HSTS)
  ✓ Rate limiting di Nginx untuk cegah brute force dan DDoS
  ✓ Batasi ukuran request (client_max_body_size)
  ✓ Jangan expose port Puma/Unicorn langsung ke internet
  ✓ Jalankan aplikasi sebagai user non-root
  ✓ Update gem secara rutin — bundle audit
  ✓ Rails credential terenkripsi, bukan plain text di ENV
  ✓ Aktifkan firewall (ufw/iptables) — hanya port 80 dan 443
  ✓ Log monitoring — cari pola akses mencurigakan
# Rate limiting di Nginx
http {
  # Definisikan zone rate limit: 10 request/detik per IP
  limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

  server {
    location /api/ {
      # Izinkan burst 20 request, sisanya ditolak dengan 429
      limit_req zone=api_limit burst=20 nodelay;
      limit_req_status 429;
      proxy_pass http://puma_app;
    }
  }
}

Memilih Web Server yang Tepat #

flowchart TD
    A[Pilih Web Server] --> B{Environment?}
    B --> C[Development]
    B --> D[Production]
    C --> E["WEBrick atau Puma\n(rails server sudah cukup)"]
    D --> F{Jenis Aplikasi?}
    F --> G["Rails / Sinatra\nThread-safe"]
    F --> H["Legacy / Not thread-safe\natau CPU-intensive"]
    F --> I["Terintegrasi Nginx/Apache\nDeploy tradisional"]
    G --> J["Puma\nDefault dan direkomendasikan"]
    H --> K["Unicorn\nPre-fork, isolasi penuh"]
    I --> L["Passenger\nMudah setup, monitoring bawaan"]
    J --> M["Cluster mode\n2-4 workers × 5 threads"]
    K --> N["4-8 worker processes\ntanpa threading"]

Ringkasan #

  • Rack adalah fondasi semuanya — semua web server dan framework Ruby berbicara Rack; memahami call(env) → [status, headers, body] adalah kunci memahami ekosistem web Ruby.
  • Middleware stack adalah kekuatan Rack — logging, autentikasi, rate limiting, compression bisa ditambahkan sebagai middleware tanpa mengubah kode aplikasi.
  • Puma adalah standar untuk production Rails — cluster mode dengan 2-4 workers dan 5 threads per worker adalah konfigurasi yang baik untuk server 2-4 core.
  • preload_app! wajib di cluster mode — memuat aplikasi sebelum fork menghemat memori dan mempercepat startup worker melalui Copy-on-Write.
  • Re-establish koneksi DB setelah fork — di on_worker_boot (Puma) atau after_fork (Unicorn); koneksi tidak bisa dibagi antar proses.
  • Unix socket lebih cepat dari TCP lokal — untuk komunikasi Nginx ↔ Puma/Unicorn di mesin yang sama, gunakan unix:///tmp/puma.sock.
  • Nginx di depan untuk static files dan SSL — Nginx melayani aset statis jauh lebih efisien dari Ruby; termination SSL di Nginx juga mengurangi beban aplikasi.
  • Phased restart Puma untuk zero-downtimepumactl phased-restart atau kill -SIGUSR1 memulai ulang worker satu per satu tanpa menjatuhkan semua sekaligus.
  • systemd untuk process management — lebih handal dari nohup atau screen; memastikan server restart otomatis setelah reboot atau crash.
  • **Rate limiting di Nginx, bukan ** — lebih efisien karena request ditolak sebelum menyentuh proses Ruby sama sekali.

← Sebelumnya: Web Socket   Berikutnya: Unit Test →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact