T Thingnoy
Home Ruby on Rails
Part 2

Ruby on Rails — สร้างได้เป็นอะไร? ทำงานยังไง?

อ่าน ~202 นาที
ruby rails mvc active-record
Ruby on Rails — สร้างได้เป็นอะไร? ทำงานยังไง?
Table of Contents

Ruby on Rails — สร้างได้เป็นอะไร? ทำงานยังไง?

“HTML Over The Wire — ส่ง HTML สำเร็จรูปผ่านสาย แทนที่จะส่ง JSON แล้วให้ JS สร้าง HTML เอง”

ถ้าอ่าน Part 1 มาแล้ว จะรู้ว่า Rails เกิดมาทำไม — แก้ปัญหาอะไร ปรัชญาเบื้องหลังเป็นยังไง ตอนนี้มาดูว่ามันทำงานยังไง สร้างแล้วได้อะไร และโลกของ Rails ต่างจากโลกของ React/Vue ที่คุ้นเคยตรงไหน


แล้ว Rails มันสร้างได้เป็นอะไร?

ถ้ามาจาก React / Vue / Svelte อ่านมาถึงตรงนี้อาจจะงง — “แล้วมันสร้างแล้วได้อะไร? HTML? JS? deploy ยังไง? มันเป็นทั้ง frontend และ backend หรอ?”

งงเพราะ mental model ต่างกันโดยสิ้นเชิง มาดูกัน

ก่อนอื่น — เว็บมันมีกี่แบบ?

ต้องเข้าใจก่อนว่าวิธีสร้างเว็บมีหลายแบบ และแต่ละแบบ “ใครเป็นคนสร้าง HTML ที่ user เห็น”:

แบบที่ 1: Static Site (HTML ดิบ / Hugo / Astro)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Build time:  Markdown → HTML files สำเร็จรูป
Runtime:     Browser โหลด HTML เลย ไม่มี server ประมวลผล
ตัวอย่าง:    Blog ส่วนตัว, documentation site

แบบที่ 2: SPA — Single Page Application (React / Vue / Svelte)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Server ส่ง:  HTML เปล่าๆ + JS bundle ก้อนใหญ่
Browser:     JS โหลดเสร็จ → fetch data จาก API → สร้าง HTML เอง
ตัวอย่าง:    Figma, Notion, Gmail

แบบที่ 3: SSR — Server-Side Rendering (Next.js / Nuxt)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Server:      รัน React/Vue บน server → สร้าง HTML → ส่งให้ browser
Browser:     แสดง HTML ก่อน → โหลด JS → "hydrate" ให้ interactive
ตัวอย่าง:    Vercel dashboard, หลาย e-commerce site

แบบที่ 4: Server-Rendered HTML (Rails / Laravel / Django / PHP)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Server:      รัน Ruby/PHP/Python → ดึง data → ใส่ลง template → ส่ง HTML
Browser:     แสดง HTML เลย จบ ไม่ต้อง hydrate ไม่ต้องรอ JS
ตัวอย่าง:    GitHub, Shopify, Basecamp

Hydrate คืออะไร? ทำไมเรียกว่า Hydrate?

คำว่า hydrate แปลตรงตัวว่า “เติมน้ำ” — เหมือนอาหารฟรีซดราย (freeze-dried food):

อาหารฟรีซดราย:
  อาหารจริง → ดึงน้ำออก → เหลือแค่โครงแห้งๆ → เติมน้ำร้อน → กลับมาเป็นอาหารอีกครั้ง

SSR Hydration:
  React component → render บน server → เหลือแค่ HTML แห้งๆ → เติม JS → กลับมา interactive อีกครั้ง

HTML ที่ server render มาคือ “โครงแห้ง” — ดูเหมือนหน้าเว็บจริง แต่กดอะไรไม่ได้ ต้อง “เติม JS” (hydrate) เข้าไปก่อนมันถึงจะมีชีวิต

ที่ต้อง hydrate เพราะ React/Vue ถูกออกแบบมาให้ รันใน browser ตั้งแต่แรก — พอเอามารันบน server (SSR) มันสร้างได้แค่ HTML ล้วนๆ ส่วน event handler, state, interactivity ทั้งหมดยังอยู่ใน JS ที่ browser ต้องโหลดมา “แปะทับ” ทีหลัง

ลองนึกภาพ Next.js (React SSR):

Step 1 — Server รัน React component บน Node.js
         สร้าง HTML สำเร็จรูปได้:
         <button>Like (42)</button>

Step 2 — ส่ง HTML ให้ browser → user เห็นปุ่มเลย (เร็ว!)

Step 3 — แต่ปุ่ม Like ยังกดไม่ได้!
         เพราะ onClick handler ยังไม่มี มันอยู่ใน JS ที่ยังโหลดไม่เสร็จ

Step 4 — Browser โหลด JS bundle เสร็จ (อาจใช้เวลา 1-3 วินาที)
         React "hydrate" = เอา JS ไปแปะทับ HTML ที่มีอยู่
         ตอนนี้ onClick ทำงานแล้ว ปุ่มกดได้แล้ว

ช่วง Step 2-4 คือ "Uncanny Valley" — user เห็นหน้าเว็บแล้ว
แต่กดอะไรไม่ได้ ดูเหมือนพัง

Rails ไม่มีปัญหานี้เลย เพราะ:

Step 1 — Server รัน Ruby → สร้าง HTML:
         <button data-action="click->like#toggle">Like (42)</button>

Step 2 — ส่ง HTML ให้ browser → user เห็นปุ่ม
         กดได้เลย! เพราะ Turbo จัดการ form/link โดยไม่ต้องรอ JS
         (หรือถ้าเป็น link/form ธรรมดา — browser handle ได้เองอยู่แล้ว)

ไม่มี hydration step ไม่มี uncanny valley ไม่มี JS bundle 500KB ที่ต้องโหลดก่อนหน้าเว็บจะ interactive

สรุปง่ายๆ:

Rails คือแบบที่ 4 — Server-Rendered HTML

ไม่ใช่ SPA — ไม่มี JS bundle ก้อนใหญ่ที่สร้าง HTML ใน browser ไม่ใช่ SSR แบบ Next.js — ไม่ได้รัน React บน server แล้ว hydrate คือ server สร้าง HTML จาก template แล้วส่งมาเลย — แบบเดียวกับ PHP/Laravel/Django

แต่ต่างจาก PHP ดิบตรงที่ Rails มี structure, convention, และ Hotwire ที่ทำให้ UX รู้สึกเหมือน SPA

เทียบง่ายๆ: user กดปุ่ม “Like” เกิดอะไรขึ้น

SPA (React):
1. JS จับ click event
2. JS เรียก fetch("/api/posts/1/like", { method: "POST" })
3. Server return JSON: { likes: 43 }
4. React อัปเดต state → re-render component → DOM เปลี่ยน

Rails + Turbo:
1. Turbo จับ click event (อัตโนมัติ ไม่ต้องเขียน JS)
2. Turbo ส่ง POST /posts/1/like
3. Server return HTML: <span id="like-count">43</span>
4. Turbo swap HTML เข้าไปแทนที่ของเก่า → DOM เปลี่ยน

ผลลัพธ์ที่ user เห็นเหมือนกัน — แต่ Rails ไม่ต้องเขียน JS สักบรรทัด

Hotwire คืออะไร? ทำไมเรียกว่า Hotwire?

Hotwire = HTML Over The Wire

ชื่อบอกหมดเลยว่ามันทำอะไร:

แบบเดิม (SPA):    ส่ง JSON over the wire → browser เอา JSON ไปสร้าง HTML
แบบ Hotwire:       ส่ง HTML over the wire → browser เอา HTML ไปแสดงเลย

“Wire” คือสายเชื่อมระหว่าง server กับ browser (network) — Hotwire ส่ง HTML สำเร็จรูป ผ่านสายนี้โดยตรง แทนที่จะส่ง JSON แล้วให้ JS ฝั่ง browser สร้าง HTML เอง

ส่วนคำว่า “hot” สื่อถึง “hotwiring a car” (ต่อสายตรง ไม่ต้องใช้กุญแจ) — bypass JS framework ตัวใหญ่ แล้วเอา HTML ไปต่อตรงกับ DOM เลย

Hotwire คือชุดเครื่องมือที่ Rails ใช้ทำให้เว็บ “รู้สึกเหมือน SPA” โดยไม่ต้องเขียน JS framework ตัวใหญ่ ประกอบด้วย 3 ตัว:

1. Turbo Drive — ไม่ต้อง reload ทั้งหน้า

ไม่มี Turbo (เว็บแบบเก่า):
  กดลิงก์ → browser reload ทั้งหน้า → จอขาวแว้บ → render ใหม่ทั้งหมด

มี Turbo Drive (Rails default):
  กดลิงก์ → Turbo ดักจับ click → fetch HTML เบื้องหลัง → swap <body> → ไม่ reload

ไม่ต้องเขียน code อะไรเลย Rails 7+ เปิดให้โดย default ทุก <a> tag และ <form> ถูก Turbo ดักจับอัตโนมัติ

เทียบกับ React Router:

React Router:  <Link to="/posts">Posts</Link>  → JS handle routing, fetch data, re-render
Turbo Drive:   <a href="/posts">Posts</a>       → Turbo handle fetch, swap HTML, ไม่ต้องเขียนอะไรพิเศษ

2. Turbo Frames — อัปเดตแค่ส่วนที่ต้องการ

ปัญหา: ถ้ามีหน้าเว็บที่มี sidebar + content แต่กดลิงก์แล้วอยากเปลี่ยนแค่ content ไม่อยาก reload sidebar?

ใน React ก็ทำ component แยก ใน Rails ใช้ Turbo Frame:

<!-- layout -->
<nav>Sidebar ไม่เปลี่ยน</nav>

<%= turbo_frame_tag "content" do %>
  <!-- เฉพาะส่วนนี้ที่จะถูก swap เมื่อกดลิงก์ข้างใน -->
  <h1>Posts</h1>
  <%= render @posts %>
<% end %>

เหมือน <div> ที่ฉลาด — ลิงก์ที่อยู่ข้างในจะ fetch แล้ว swap เฉพาะ frame นี้ ส่วนอื่นของหน้าไม่กระทบ

เทียบกับ React:

React:       useState + useEffect + loading state + error handling + re-render
Turbo Frame: ครอบ <turbo-frame> แล้วจบ server ส่ง HTML มา swap ให้

3. Turbo Streams — real-time update หลายจุดพร้อมกัน

ถ้าอยาก update หลายส่วนของหน้าพร้อมกัน หรือทำ real-time (เช่น chat):

<!-- server ส่ง Turbo Stream response กลับมา -->
<%= turbo_stream.append "comments" do %>
  <%= render @comment %>    <!-- เพิ่ม comment ใหม่ต่อท้าย list -->
<% end %>

<%= turbo_stream.update "comment-count" do %>
  <%= @post.comments.count %> comments   <!-- อัปเดตตัวเลข -->
<% end %>

คำสั่งเดียวอัปเดตได้หลายจุด: append, prepend, replace, update, remove

ทำ real-time ได้ด้วย — ต่อ WebSocket ผ่าน Action Cable แล้ว broadcast Turbo Streams ไปหา user ทุกคน:

# เมื่อสร้าง comment ใหม่ → broadcast ไปหาทุกคนที่ดู post นี้
class Comment < ApplicationRecord
  after_create_commit -> {
    broadcast_append_to post, target: "comments"
  }
end

แค่นี้ — user คนอื่นที่เปิดหน้าเดียวกันจะเห็น comment ใหม่โผล่มาเลย โดยไม่ต้องเขียน JS สักบรรทัด

4. Stimulus — สำหรับ JS ที่จำเป็นจริงๆ

Turbo จัดการ navigation, form, real-time ให้หมดแล้ว แต่บางอย่างต้องใช้ JS จริงๆ เช่น toggle dropdown, copy to clipboard, character counter

Stimulus ไม่ใช่ framework ที่ manage ทุกอย่าง — มันแค่ “โรย” behavior ลงบน HTML ที่ server render มา:

<!-- HTML จาก server (ทำงานได้แม้ไม่มี JS) -->
<div data-controller="clipboard">
  <input type="text" value="https://mysite.com/abc" data-clipboard-target="source">
  <button data-action="click->clipboard#copy">Copy</button>
</div>
// app/javascript/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source"]

  copy() {
    navigator.clipboard.writeText(this.sourceTarget.value)
  }
}

เทียบ mental model กับ React:

React:    ทุกอย่างคือ JS — UI, state, logic, routing, ทุกอย่าง
          JS เป็นพลเมืองชั้นหนึ่ง HTML เป็นผลพลอยได้ (JSX)

Hotwire:  HTML เป็นพลเมืองชั้นหนึ่ง — server render HTML มาเสร็จ
          JS เป็นแค่เครื่องเทศโรยหน้า (Stimulus)
          navigation + form + real-time ใช้ HTML swap (Turbo)

สรุป Hotwire = ทำ 80% ของสิ่งที่ SPA ทำ โดยไม่เขียน JS

ต้องการReactHotwire
กดลิงก์ไม่ reloadReact RouterTurbo Drive (อัตโนมัติ)
อัปเดตบางส่วนของหน้าuseState + fetchTurbo Frames
Real-time updateSocket.io + state managementTurbo Streams + Action Cable
Dropdown, modal, toggleReact componentStimulus controller
JS bundle size200KB-1MB+~25KB

ส่วนอีก 20% ที่ Hotwire ทำไม่ดีเท่า (เช่น complex drag & drop, interactive chart, real-time collaborative editing) — ก็ฝัง React/Vue เฉพาะจุดนั้นได้


ทีนี้มาดูรายละเอียดว่า 2 โลกนี้ต่างกันยังไง:

โลกที่คุ้นเคย: Frontend + Backend แยกกัน

ถ้าสร้าง app ด้วย React + Node ตอนนี้ architecture จะเป็นแบบนี้:

[Browser]                    [Server]
React app (JS)  ← JSON →    Node.js API
- render UI                  - business logic
- state management           - database
- routing (client-side)      - authentication
- ทำ fetch() ไป API          - return JSON

Frontend (React): เป็น JS ล้วน รันใน browser ทำหน้าที่ render HTML เอง Backend (Node): เป็น API server ส่ง JSON กลับมา ไม่ยุ่งกับ HTML เลย

deploy ก็แยก:

โลกของ Rails: ทุกอย่างอยู่ในที่เดียว

[Browser]                    [Rails Server]
HTML + CSS  ← HTML →        Controller → Model → View
(แค่แสดงผล)                  - รับ request
                              - ดึงข้อมูลจาก database
                              - render HTML สำเร็จรูป
                              - ส่ง HTML กลับมาเลย

Rails เป็นทั้ง backend และ frontend ในตัวเดียว

เมื่อ user เปิด /posts:

  1. Browser ส่ง request ไปที่ Rails server
  2. Rails รัน Ruby บน server เพื่อดึงข้อมูลจาก database
  3. Rails render HTML สำเร็จรูปบน server (ไม่ใช่ส่ง JSON)
  4. Browser ได้ HTML มา แสดงผลเลย ไม่ต้องมี JS มา render
# React: server ส่ง JSON → JS ใน browser สร้าง HTML
GET /api/posts → { "posts": [{ "title": "Hello" }] }
→ React อ่าน JSON แล้วสร้าง <h1>Hello</h1> ใน browser

# Rails: server ส่ง HTML สำเร็จรูปมาเลย
GET /posts → <html><body><h1>Hello</h1></body></html>
→ Browser แสดงผลเลย ไม่ต้องมี JS

ERB คืออะไร? Server สร้าง HTML ยังไง?

ถ้า Rails ไม่ใช้ React/Vue สร้าง HTML แล้วใช้อะไร? คำตอบคือ ERB

ERB = Embedded Ruby — template ที่ฝัง Ruby code ลงใน HTML

ถ้าเคยเขียน PHP, EJS, Blade, Jinja — มันคือสิ่งเดียวกัน ต่างแค่ภาษา:

PHP:        <?php echo $title; ?>           → ฝัง PHP ใน HTML
EJS:        <%= title %>                    → ฝัง JS ใน HTML (Node.js)
Blade:      {{ $title }}                    → ฝัง PHP ใน HTML (Laravel)
Jinja:      {{ title }}                     → ฝัง Python ใน HTML (Django)
JSX:        <h1>{title}</h1>                → ฝัง JS ใน JS ที่สร้าง HTML (React)
ERB:        <%= @title %>                   → ฝัง Ruby ใน HTML (Rails)

ERB syntax มีแค่ 2 อย่าง

<% %>    รัน Ruby code (ไม่แสดงผล)
<%= %>   รัน Ruby code แล้วแสดงผลลงใน HTML

ตัวอย่าง:

<!-- app/views/posts/index.html.erb -->

<h1>Blog</h1>

<%# นี่คือ comment — ไม่แสดงอะไร %>

<% if @posts.any? %>
  <% @posts.each do |post| %>
    <article>
      <h2><%= post.title %></h2>
      <p><%= post.body %></p>
      <small>by <%= post.author.name %></small>
    </article>
  <% end %>
<% else %>
  <p>No posts yet.</p>
<% end %>

Server รัน Ruby code ข้างในแล้วได้ HTML ออกมา:

<!-- HTML ที่ browser ได้รับ — ไม่มี Ruby เหลือแล้ว -->

<h1>Blog</h1>

<article>
  <h2>Hello Rails</h2>
  <p>My first post!</p>
  <small>by Skypart</small>
</article>
<article>
  <h2>Active Record Basics</h2>
  <p>Active Record is the ORM layer in Rails.</p>
  <small>by Skypart</small>
</article>

Browser ได้ HTML ธรรมดา ไม่มี Ruby ไม่มี template syntax เหลือ — เหมือน HTML ที่เขียนมือทุกประการ

เทียบกับ JSX (React)

// React — JSX: JavaScript สร้าง HTML
function PostList({ posts }) {
  return (
    <div>
      <h1>Blog</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}
// JS สร้าง HTML → รันใน browser
<!-- Rails — ERB: HTML ที่มี Ruby ฝังอยู่ -->
<div>
  <h1>Blog</h1>
  <% @posts.each do |post| %>
    <article>
      <h2><%= post.title %></h2>
      <p><%= post.body %></p>
    </article>
  <% end %>
</div>
<!-- Ruby สร้าง HTML → รันบน server -->

ผลลัพธ์เหมือนกัน — ต่างกันแค่:

ไฟล์ .html.erb อ่านยังไง?

index.html.erb
  │     │    └── ใช้ ERB engine ประมวลผล Ruby code
  │     └────── output เป็น HTML
  └──────────── ชื่อ action (index)

Rails อ่านจากขวาไปซ้าย: ประมวลผล ERB ก่อน → ได้ HTML → ส่งให้ browser

แล้ว JavaScript ล่ะ?

Rails ไม่ต้องการ JavaScript เพื่อ render หน้าเว็บ — HTML มาจาก server พร้อมแสดงผล

แต่ถ้าต้องการ interactivity (dropdown, modal, real-time update) ล่ะ?

Rails 7+ มาพร้อม Hotwire ซึ่งประกอบด้วย:

Turbo — ทำให้รู้สึกเหมือน SPA โดยไม่เขียน JS

# ปกติ: กดลิงก์ → reload ทั้งหน้า (กระพริบ)
# Turbo: กดลิงก์ → fetch HTML มา swap เฉพาะส่วนที่เปลี่ยน (ไม่กระพริบ)

Turbo ดักจับทุก link click และ form submit แล้วทำเป็น AJAX request อัตโนมัติ — แทนที่จะ reload ทั้งหน้า มัน swap เฉพาะ <body> ทำให้ UX รู้สึกเร็วเหมือน SPA โดยที่ server ยังส่ง HTML เหมือนเดิม

Stimulus — JS เล็กๆ สำหรับ interactivity

// ไม่ใช่ React component ที่จัดการทุกอย่าง
// แค่ "โรย" behavior เล็กๆ ลงบน HTML ที่ server render มา

import { Controller } from "@hotwired/stimulus"

// ใช้กับ HTML: <div data-controller="hello">
//                <input data-hello-target="name">
//                <button data-action="click->hello#greet">Greet</button>
//                <span data-hello-target="output"></span>
//              </div>
export default class extends Controller {
  static targets = ["name", "output"]

  greet() {
    this.outputTarget.textContent = `Hello, ${this.nameTarget.value}!`
  }
}

แค่นั้น ไม่มี virtual DOM ไม่มี state management ไม่มี build step ที่ซับซ้อน

เทียบกันตรงๆ

React + NodeRails
ใครสร้าง HTML?Browser (JS render)Server (Ruby render)
Server ส่งอะไร?JSON dataHTML สำเร็จรูป
ต้องมี JS ไหม?ต้องมี ไม่มีหน้าขาวไม่ต้องก็ได้ HTML มาพร้อมแสดง
SEOต้องทำ SSR เพิ่ม (Next.js)ได้เลย HTML มาจาก server
First paintช้า (โหลด JS → fetch data → render)เร็ว (HTML มาพร้อมแสดง)
Interactivityทุกอย่างเป็น JSHTML + โรย JS เล็กน้อย (Stimulus)
State managementRedux, Zustand, Pinia…ไม่มี (state อยู่ใน database)
Deployแยก 2 ที่ (frontend + backend)ที่เดียว
Build stepwebpack / vite (นาที)แทบไม่มี

แล้ว bundle ยังไง?

React / Vue

npm run build    # webpack/vite compile JS ทั้งหมดเป็น bundle
# ได้ dist/ folder → upload ไป CDN

Rails

rails assets:precompile    # compile CSS + อัด JS เล็กน้อยที่มี
# ได้ public/assets/ → serve จาก Rails server เลย

Rails 7+ ใช้ importmap — import JS modules ผ่าน browser โดยตรง ไม่ต้อง bundler เลย:

# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"

ไม่มี node_modules ไม่มี webpack ไม่มี vite — browser โหลด JS ตรงจาก CDN ผ่าน <script type="importmap">

(แต่ถ้าอยากใช้ bundler ก็ได้ Rails รองรับ esbuild, vite, webpack เป็น option)

แล้ว CSS ล่ะ? อยากใช้ Tailwind ทำไง?

Rails 8 support Tailwind ตั้งแต่สร้าง project:

# สร้าง project ใหม่พร้อม Tailwind
rails new blog --css tailwind

แค่นี้จบ — Rails ติดตั้ง Tailwind ให้ ใช้ได้เลย:

<!-- app/views/posts/index.html.erb -->
<div class="max-w-4xl mx-auto px-4 py-8">
  <h1 class="text-3xl font-bold text-gray-900 mb-6">Posts</h1>

  <% @posts.each do |post| %>
    <article class="bg-white shadow rounded-lg p-6 mb-4 hover:shadow-md transition">
      <h2 class="text-xl font-semibold">
        <%= link_to post.title, post, class: "text-blue-600 hover:text-blue-800" %>
      </h2>
      <p class="text-gray-600 mt-2"><%= truncate(post.body, length: 200) %></p>
      <span class="text-sm text-gray-400"><%= time_ago_in_words(post.created_at) %> ago</span>
    </article>
  <% end %>
</div>

ถ้ามี project อยู่แล้ว อยากเพิ่ม Tailwind ทีหลัง

bundle add tailwindcss-rails        # 1. ติดตั้ง gem (โหลด library มา)
rails tailwindcss:install           # 2. setup config ใน project

ทำไมต้อง 2 คำสั่ง?

bundle add tailwindcss-rails
  → เพิ่ม gem เข้า Gemfile แล้วโหลดมาลงเครื่อง
  → เหมือน npm install tailwindcss — แค่โหลด library มาวาง ยังใช้ไม่ได้

rails tailwindcss:install
  → สร้างไฟล์ config ที่จำเป็นใน project:
     - app/assets/stylesheets/application.tailwind.css (ไฟล์หลัก)
     - config/tailwind.config.js
     - แก้ layout ให้ใช้ Tailwind stylesheet
     - ตั้ง build script ใน Procfile.dev
  → เหมือน npx tailwindcss init — setup project ให้พร้อมใช้

rails new --css tailwind ทำทั้ง 2 step ให้อัตโนมัติ — แต่ถ้าเพิ่มทีหลังต้องทำเอง

ใน Rails เป็น pattern ปกติ — gem ส่วนใหญ่ที่ต้อง integrate กับ project จะมี install command แยก:

bundle add devise          # โหลด gem มา
rails generate devise:install   # setup config + ไฟล์ที่ต้องใช้

Tailwind ใน Rails ทำงานยังไง?

ใน React/Vue:
  npm install tailwindcss → tailwind.config.js → postcss.config.js
  → build ผ่าน webpack/vite → ได้ CSS output

ใน Rails:
  rails new --css tailwind → ใช้ได้เลย
  → Tailwind CLI scan ไฟล์ .erb ทุกตัว → generate CSS อัตโนมัติ
  → ไม่ต้องมี Node.js ไม่ต้องมี PostCSS ไม่ต้อง config อะไร

Rails ใช้ Tailwind standalone CLI — binary ตัวเดียวที่ compile Tailwind โดยไม่ต้องมี Node.js ecosystem ทั้งยวง ไม่มี node_modules ไม่มี package.json

rails server จะรัน Tailwind watcher ให้อัตโนมัติ — แก้ class ใน .erb แล้ว CSS อัปเดตทันที

CSS options อื่นๆ ที่ Rails รองรับ

rails new blog                    # default — ไม่มี CSS framework
rails new blog --css tailwind     # Tailwind CSS
rails new blog --css bootstrap    # Bootstrap
rails new blog --css bulma        # Bulma
rails new blog --css postcss      # PostCSS (เขียน CSS เอง)
rails new blog --css sass         # Sass/SCSS

ตัวอย่าง: PostCSS — เขียน CSS เองแบบ modern

rails new blog --css postcss

ได้ไฟล์ CSS หลักที่ app/assets/stylesheets/application.postcss.css เขียน CSS ปกติได้เลย แต่ใช้ feature ของ PostCSS ได้ด้วย เช่น nesting, custom media:

/* app/assets/stylesheets/application.postcss.css */

/* CSS nesting — ไม่ต้องเขียนซ้ำ selector */
.post-card {
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  padding: 1.5rem;
  margin-bottom: 1rem;

  h2 {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 0.5rem;

    a {
      color: #2563eb;
      text-decoration: none;

      &:hover {
        color: #1d4ed8;
        text-decoration: underline;
      }
    }
  }

  .meta {
    font-size: 0.875rem;
    color: #6b7280;
  }

  .body {
    color: #374151;
    margin-top: 0.5rem;
    line-height: 1.6;
  }
}

/* ใช้กับ ERB ปกติ */
<!-- app/views/posts/index.html.erb -->
<div class="container">
  <h1>Posts</h1>

  <% @posts.each do |post| %>
    <article class="post-card">
      <h2><%= link_to post.title, post %></h2>
      <span class="meta"><%= time_ago_in_words(post.created_at) %> ago</span>
      <p class="body"><%= truncate(post.body, length: 200) %></p>
    </article>
  <% end %>
</div>

เทียบ Tailwind vs PostCSS:

Tailwind:  class เยอะใน HTML ไม่ต้องเขียน CSS แยก
           <article class="bg-white shadow rounded-lg p-6 mb-4 hover:shadow-md">

PostCSS:   class น้อยใน HTML เขียน CSS แยกเป็นไฟล์
           <article class="post-card">

เลือกตาม preference:
  - ชอบเขียน CSS เอง ควบคุมทุกอย่าง → PostCSS / Sass
  - ไม่อยากเขียน CSS เลย ใช้ utility class → Tailwind
  - อยากได้ component สำเร็จรูป (ปุ่ม, modal, navbar) → Bootstrap

แล้ว deploy ยังไง?

ก่อนอื่น — production ใช้ Docker จริงหรอ?

คำตอบสั้นๆ: ตอนนี้ใช่ แต่ไม่ได้เป็นแบบนั้นมาตลอด

ยุคที่ 1 (2005-2015): Deploy Ruby ตรงบน server
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- ติดตั้ง Ruby, PostgreSQL, Nginx บน server ตรงๆ
- ใช้ Capistrano (deploy tool) SSH เข้าไป pull code แล้ว restart
- ปัญหา: "it works on my machine" — Ruby version ไม่ตรง, gem ไม่ตรง
- Heroku แก้ปัญหานี้ด้วย: git push heroku main แล้วจบ

ยุคที่ 2 (2015-2023): Docker เริ่มเป็น option
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- บริษัทใหญ่เริ่มใช้ Docker + Kubernetes
- แต่ Rails community ส่วนใหญ่ยังไม่ใช้ Docker — ยุ่งเกินไป
- Heroku ยังเป็น default choice

ยุคที่ 3 (2023-ปัจจุบัน): Docker เป็น default ของ Rails
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Rails 7.1+ generate Dockerfile มาให้ทุก project (ไม่ต้องเขียนเอง)
- Rails 8 มากับ Kamal — deploy tool ที่ใช้ Docker เป็น core
- DHH ประกาศชัด: "Docker is now the deployment target for Rails"
- Heroku แพงขึ้น ไม่มี free tier → community ย้ายไป VPS + Docker

แล้ว production จริง คนใช้อะไรกันบ้าง?

บริษัทวิธี deploy
ShopifyKubernetes + Docker (scale ใหญ่มาก)
GitHubKubernetes + Docker
Basecamp / HEYKamal + Docker บน bare metal servers
CookpadKubernetes + Docker
Startup ทั่วไปRender / Fly.io / Kamal + VPS

คำตอบจริงๆ คือ: ไม่จำเป็นต้อง Docker — แต่ Rails community เลือก Docker เป็น default แล้ว เพราะ:

  1. rails new สร้าง Dockerfile มาให้ ไม่ต้องเขียนเอง
  2. ลง server ไหนก็ได้ ไม่ต้อง install Ruby, gem, Node บน server
  3. dev กับ production ใช้ environment เดียวกัน ไม่มี “works on my machine”

ถ้าไม่อยากยุ่งกับ Docker เลย → ใช้ Render.com / Railway — กด deploy จบ เบื้องหลังมันก็ใช้ Docker อยู่ดี แต่คุณไม่ต้องแตะ

เทียบก่อน: deploy React+Node vs deploy Rails

Deploy React + Node (แยก 2 ส่วน):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Frontend:
  1. npm run build          → ได้ dist/ (HTML + JS + CSS)
  2. Upload dist/ ไป Vercel / Netlify / S3+CloudFront
  3. ตั้ง env var: VITE_API_URL=https://api.myapp.com

Backend:
  4. Deploy Node.js server ไป Railway / Render / EC2
  5. ตั้ง env var: DATABASE_URL, JWT_SECRET, CORS_ORIGIN
  6. ตั้ง CORS ให้ frontend domain เข้าถึงได้
  7. Setup database (migration, seed)

ต้องดูแล: 2 deployments, 2 domains, CORS, API versioning


Deploy Rails (ทุกอย่างในที่เดียว):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  1. Deploy Rails app ไป server
  2. ตั้ง env var: DATABASE_URL, SECRET_KEY_BASE
  3. รัน rails db:migrate
  4. จบ

ต้องดูแล: 1 deployment, 1 domain, ไม่มี CORS

วิธีที่ 1 — PaaS: กด deploy จบ (มือใหม่แนะนำ)

Render.com (ฟรีสำหรับเริ่มต้น):

1. Push code ขึ้น GitHub
2. ไปที่ render.com → New Web Service → เลือก repo
3. Render detect ว่าเป็น Rails อัตโนมัติ
4. ตั้ง environment variables:
   - DATABASE_URL    (Render สร้าง PostgreSQL ให้ กด copy URL มาใส่)
   - SECRET_KEY_BASE (รัน `rails secret` ใน terminal แล้ว copy)
5. กด Deploy

ทุกครั้งที่ push ขึ้น GitHub → Render auto deploy ให้

Fly.io:

# ติดตั้ง flyctl
brew install flyctl

# ใน project directory
fly launch
# Fly จะถามชื่อ app, region, จะสร้าง PostgreSQL ไหม
# ตอบ yes ทั้งหมด → Fly สร้าง Dockerfile, fly.toml, database ให้

fly deploy
# build Docker image → push ไป Fly → รัน migrations → เปิด server

# เปิดเว็บดู
fly open

วิธีที่ 2 — Kamal: deploy ไป VPS ตรงๆ (Rails 8 default)

Rails 8 มากับ Kamal — deploy tool ที่ส่ง Docker container ไป server ที่มี SSH access:

# สิ่งที่ต้องมี:
# - VPS สักตัว (DigitalOcean $6/เดือน, Hetzner $4/เดือน)
# - Domain name ชี้ไปที่ IP ของ VPS
# - Docker Hub account (เก็บ image)
# config/deploy.yml (Rails 8 generate มาให้)
service: blog
image: your-dockerhub-user/blog

servers:
  web:
    hosts:
      - 123.456.789.0    # IP ของ VPS
    options:
      network: "private"

proxy:
  ssl: true
  host: blog.yourdomain.com

registry:
  username: your-dockerhub-user
  password:
    - KAMAL_REGISTRY_PASSWORD

env:
  secret:
    - RAILS_MASTER_KEY
    - DATABASE_URL
kamal setup     # ครั้งแรก: install Docker บน server, ตั้ง SSL, deploy
kamal deploy    # ครั้งถัดไป: build image → push → deploy → zero-downtime

Kamal ทำให้หมด: build Docker image, push ไป registry, SSH เข้า server, pull image, รัน container, ตั้ง SSL certificate, zero-downtime deploy

วิธีที่ 3 — เช่า Linux VPS แล้ว Deploy Production จริง ตั้งแต่ศูนย์

VPS คืออะไร?

VPS = Virtual Private Server — คอมพิวเตอร์เสมือนที่เช่าจาก datacenter

ลองนึกภาพ datacenter ที่มีคอมพิวเตอร์ตัวใหญ่ๆ (physical server) วางเรียงเป็นแถว คอมพิวเตอร์ 1 ตัวถูกแบ่ง (virtualize) ออกเป็นหลายๆ ส่วน — แต่ละส่วนคือ VPS 1 ตัว ที่มี CPU, RAM, Disk, IP address เป็นของตัวเอง

Physical Server (เครื่องจริงใน datacenter)
┌──────────────────────────────────────────┐
│                                          │
│  ┌──────────┐  ┌──────────┐  ┌────────┐ │
│  │  VPS #1  │  │  VPS #2  │  │ VPS #3 │ │
│  │  คุณ     │  │  คนอื่น  │  │ คนอื่น │ │
│  │  2GB RAM │  │  4GB RAM │  │ 1GB    │ │
│  │  Ubuntu  │  │  Debian  │  │ Ubuntu │ │
│  └──────────┘  └──────────┘  └────────┘ │
│                                          │
└──────────────────────────────────────────┘

เทียบกับสิ่งที่คุ้นเคย:

อะไรเปรียบเหมือน
Shared Hostingอยู่ร่วมกับคนอื่น ห้องรวม แชร์ทุกอย่างหอพัก — ห้องน้ำรวม ตึกเดียวกัน
VPSเครื่องเสมือน แยกส่วนชัดเจนคอนโด — ห้องส่วนตัว แต่อยู่ตึกเดียวกัน
Dedicated Serverเครื่องจริงทั้งเครื่องเป็นของคุณบ้านเดี่ยว
Cloud (AWS/GCP)VPS ที่ scale ได้ + บริการเสริมเยอะคอนโดหรู — มี concierge, สระว่ายน้ำ, gym

VPS ให้คุณได้ เครื่อง Linux สักตัวที่เปิดตลอด 24 ชม. มี IP address ต่อ internet ทำอะไรก็ได้ — รัน web app, database, cron job, ฯลฯ

ทำไมไม่ใช้ AWS / GCP?


ไม่ใช่ของเล่น — นี่คือ setup ที่ใช้รัน production ได้จริงๆ ครบทุกอย่าง: security, HTTPS, backup, monitoring, auto-deploy


Phase 1 — เช่า VPS และ Secure Server

Step 1 — เช่า VPS

Providerราคาเริ่มต้นจุดเด่น
Hetzner~$4/เดือนถูกสุด spec ดี มี datacenter ใน EU, US
DigitalOcean$6/เดือนUI ดี มี managed database option
Linode (Akamai)$5/เดือนเสถียร มี Singapore region
Vultr$5/เดือนมี Tokyo/Singapore

เลือก spec:

สมัครเสร็จจะได้ IP address เช่น 159.89.123.45

ตอนสร้าง VPS ให้เลือก SSH Key แทน password — ปลอดภัยกว่ามาก ถ้ายังไม่มี SSH key: ssh-keygen -t ed25519 แล้ว copy public key ไปใส่ตอนสร้าง VPS

Step 2 — SSH เข้า server แล้ว secure

# จากเครื่องตัวเอง
ssh root@159.89.123.45

ตอนนี้อยู่ใน server แล้ว — อันดับแรกต้อง secure ก่อนทำอะไรทั้งหมด:

# อัปเดต packages
apt update && apt upgrade -y

# สร้าง deploy user (ไม่ควรรัน app ด้วย root)
adduser deploy
usermod -aG sudo deploy

# copy SSH key จาก root ไปให้ deploy user
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh

# ทดสอบ SSH ด้วย deploy user (เปิด terminal ใหม่)
# ssh deploy@159.89.123.45

# ปิด root SSH login + ปิด password login (ใช้ SSH key เท่านั้น)
sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

Step 3 — ตั้ง Firewall

# เปิดเฉพาะ port ที่ต้องใช้
ufw allow 22/tcp     # SSH
ufw allow 80/tcp     # HTTP
ufw allow 443/tcp    # HTTPS
ufw --force enable

ufw status
# Status: active
# To                         Action      From
# --                         ------      ----
# 22/tcp                     ALLOW       Anywhere
# 80/tcp                     ALLOW       Anywhere
# 443/tcp                    ALLOW       Anywhere

ตั้งแต่นี้ไป port อื่นทั้งหมดถูก block — PostgreSQL (5432) ไม่ถูก expose ออก internet

Step 4 — ติดตั้ง Docker

# ติดตั้ง Docker
curl -fsSL https://get.docker.com | sh

# ให้ deploy user ใช้ Docker ได้โดยไม่ต้อง sudo
usermod -aG docker deploy

# เช็คว่าได้แล้ว (login ใหม่ด้วย deploy user)
docker --version
docker compose version

Step 5 — ตั้ง auto-update security patches

apt install -y unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
# เลือก Yes — server จะติดตั้ง security update อัตโนมัติ

Phase 2 — เตรียม Project สำหรับ Production

Step 6 — สร้างไฟล์ production ใน project (ทำบนเครื่องตัวเอง)

# docker-compose.production.yml
services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web

  web:
    build: .
    restart: unless-stopped
    expose:
      - "3000"
    env_file: .env.production
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:17
    restart: unless-stopped
    env_file: .env.production
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./backups:/backups
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:
  redis_data:
  caddy_data:
  caddy_config:

restart: unless-stopped ทำให้ container restart อัตโนมัติถ้า crash หรือ server reboot

Redis ใส่ไว้เพราะ production จริงใช้สำหรับ:

# Caddyfile
payroll.yourdomain.com {
    reverse_proxy web:3000

    encode gzip

    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
    }

    log {
        output file /data/access.log {
            roll_size 10mb
            roll_keep 5
        }
    }
}

Caddy ทำให้ครบ:

# .env.production (ห้าม commit ไฟล์นี้ ใส่ใน .gitignore)
RAILS_ENV=production
SECRET_KEY_BASE=ใช้_rails_secret_generate_ใส่ตรงนี้
DATABASE_URL=postgres://postgres:your_strong_password_here@db/payroll_production
REDIS_URL=redis://redis:6379/0
RAILS_LOG_TO_STDOUT=true
POSTGRES_PASSWORD=your_strong_password_here
# สร้าง SECRET_KEY_BASE
rails secret
# copy ค่าที่ได้ไปใส่ใน .env.production

Step 7 — สร้าง deploy script

#!/bin/bash
# deploy.sh — วางที่ root ของ project
set -e

echo "==> Pulling latest code..."
git pull origin main

echo "==> Building and deploying..."
docker compose -f docker-compose.production.yml up -d --build --remove-orphans

echo "==> Running migrations..."
docker compose -f docker-compose.production.yml exec -T web rails db:migrate

echo "==> Cleaning old Docker images..."
docker image prune -f

echo "==> Done! Checking status..."
docker compose -f docker-compose.production.yml ps
chmod +x deploy.sh

Step 8 — สร้าง backup script

#!/bin/bash
# backup.sh — backup database อัตโนมัติ
set -e

BACKUP_DIR="/opt/payroll/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/payroll_$TIMESTAMP.sql.gz"

mkdir -p $BACKUP_DIR

# dump database แล้ว gzip
docker compose -f docker-compose.production.yml exec -T db \
  pg_dump -U postgres payroll_production | gzip > "$BACKUP_FILE"

# ลบ backup ที่เก่ากว่า 30 วัน
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete

echo "Backup saved: $BACKUP_FILE ($(du -h $BACKUP_FILE | cut -f1))"
chmod +x backup.sh

Phase 3 — Deploy

Step 9 — ตั้ง domain

ไปที่ DNS provider → สร้าง A record:

payroll.yourdomain.com → 159.89.123.45

(รอ DNS propagate 5-30 นาที)

Step 10 — Deploy ครั้งแรก

# SSH เข้า server ด้วย deploy user
ssh deploy@159.89.123.45

# clone project
cd /opt
git clone https://github.com/your-user/payroll.git
cd payroll

# สร้าง .env.production บน server (ไม่ได้อยู่ใน git)
nano .env.production
# paste ค่าจาก Step 6

# deploy ครั้งแรก
docker compose -f docker-compose.production.yml up -d --build

# รอ build (~3-5 นาทีครั้งแรก) ดู log:
docker compose -f docker-compose.production.yml logs -f web

# สร้าง database + run migration
docker compose -f docker-compose.production.yml exec web rails db:create
docker compose -f docker-compose.production.yml exec web rails db:migrate

# (optional) seed data
docker compose -f docker-compose.production.yml exec web rails db:seed

เปิด https://payroll.yourdomain.com — เจอ app พร้อม HTTPS!

Step 11 — ตั้ง automatic backup ด้วย cron

# บน server — เปิด crontab
crontab -e

# เพิ่มบรรทัดนี้: backup ทุกวัน ตี 3
0 3 * * * cd /opt/payroll && ./backup.sh >> /var/log/backup.log 2>&1

Phase 4 — Auto-Deploy ด้วย GitHub Actions (ทุก push ขึ้น main = deploy อัตโนมัติ)

Step 12 — ตั้ง SSH key สำหรับ GitHub Actions

# บนเครื่องตัวเอง — สร้าง key คู่ใหม่สำหรับ CI
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key
# ไม่ต้องใส่ passphrase (กด Enter เปล่า)
# copy public key ไปใส่บน server
ssh-copy-id -i ~/.ssh/deploy_key.pub deploy@159.89.123.45

ไปที่ GitHub repo → Settings → Secrets and variables → Actions → New secret:

Step 13 — สร้าง GitHub Actions workflow

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true
      - name: Setup database
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost/test
          RAILS_ENV: test
        run: |
          rails db:create db:migrate
      - name: Run tests
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost/test
          RAILS_ENV: test
        run: rails test

  deploy:
    needs: test      # deploy เฉพาะเมื่อ test ผ่าน
    runs-on: ubuntu-latest
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /opt/payroll
            ./deploy.sh

ตั้งแต่นี้ไป: push ขึ้น main → GitHub Actions รัน test → test ผ่าน → auto deploy ไป server


Phase 5 — Monitoring (รู้ตัวก่อนที่ user จะบอก)

Step 14 — Health check endpoint

# config/routes.rb — เพิ่ม health check
Rails.application.routes.draw do
  get "/up", to: "rails/health#show", as: :rails_health_check  # Rails 7.1+ มีให้แล้ว
  # ...
end

ทดสอบ: curl https://payroll.yourdomain.com/up → ได้ 200 ถ้า app ทำงานปกติ

Step 15 — Uptime monitoring (ฟรี)

สมัคร UptimeRobot (uptimerobot.com) หรือ Better Stack (betterstack.com) ฟรี:

ถ้า server ล่ม → ได้ notification ภายใน 5 นาที

Step 16 — ดู log ง่ายๆ

# ดู log แบบ real-time
docker compose -f docker-compose.production.yml logs -f web

# ดู log ย้อนหลัง 100 บรรทัด
docker compose -f docker-compose.production.yml logs --tail 100 web

# ดู error เฉพาะ
docker compose -f docker-compose.production.yml logs web 2>&1 | grep -i error

Step 17 — ดู resource usage

# ดู CPU / RAM ของแต่ละ container
docker stats

# CONTAINER    CPU %    MEM USAGE / LIMIT
# web          2.3%     256MiB / 2GiB
# db           0.5%     64MiB / 2GiB
# redis        0.1%     8MiB / 2GiB
# caddy        0.1%     16MiB / 2GiB

สรุป: สิ่งที่ได้จาก setup นี้

Production Checklist:
  [x] SSH key only (ไม่มี password login)
  [x] Firewall (เปิดเฉพาะ 22, 80, 443)
  [x] Non-root deploy user
  [x] Auto security updates
  [x] HTTPS + auto-renew (Caddy + Let's Encrypt)
  [x] Security headers
  [x] Database backup อัตโนมัติทุกวัน (เก็บ 30 วัน)
  [x] Auto-restart ถ้า crash
  [x] CI/CD — push main → test → deploy อัตโนมัติ
  [x] Uptime monitoring + alert
  [x] Log access ง่าย
  ค่าใช้จ่าย: ~$4-6/เดือน (VPS) + $0 (ที่เหลือฟรีหมด)

สรุป deploy

วิธีความง่ายค่าใช้จ่ายควบคุมได้เหมาะกับ
Render / Railwayง่ายสุด กด deployฟรี → $7+/เดือนน้อยเรียนรู้, side project
Fly.ioง่าย CLI สั้นฟรี → $5+/เดือนปานกลางside project, small production
Kamal + VPSปานกลาง$4-6/เดือนมากproduction (Rails official)
VPS + Docker (ด้านบน)ละเอียดแต่เข้าใจทุก step$4-6/เดือนมากสุดproduction + อยากรู้ว่าเกิดอะไรขึ้น

เริ่มต้น: Render.com — push GitHub แล้วจบ production จริง ไม่อยากจัดการ server: Kamal production จริง อยากควบคุมทุกอย่าง: VPS + Docker ตาม guide ด้านบน

แล้วถ้าอยากมี React/Vue ด้วยล่ะ?

ได้ มี 2 แนวทาง:

แนวที่ 1 — Rails เป็น API อย่างเดียว + Frontend แยก

rails new blog --api    # สร้าง Rails แบบ API-only (ไม่มี View layer)
class PostsController < ApplicationController
  def index
    @posts = Post.published.recent
    render json: @posts    # ส่ง JSON เหมือน Express.js
  end
end

แล้วเอา React/Vue/Svelte มาเป็น frontend แยก — เหมือนที่ทำกับ Node.js API ปกติ

แนวที่ 2 — Rails render HTML + ฝัง React/Vue เฉพาะส่วนที่ต้องการ

ส่วนใหญ่ใช้ Rails render HTML ปกติ แต่ component ที่ซับซ้อน (เช่น interactive chart, drag & drop) ก็ mount React/Vue component เข้าไปเฉพาะจุด

แนวที่ 3 — ใช้ Inertia.js (ตรงกลางระหว่าง 2 โลก)

Rails controller → ส่ง data เป็น props → React/Vue/Svelte render

Inertia ทำให้เขียน backend เป็น Rails ปกติ แต่ frontend เป็น React/Vue/Svelte — โดยไม่ต้องสร้าง API layer แยก

สรุปให้จบความงง

Rails สร้างแล้วได้ = web app ที่ server render HTML ส่งมาให้ browser โดยตรง

ไม่ต้องมี React ไม่ต้องมี Vue ไม่ต้องมี build step ซับซ้อน server ทำทุกอย่าง — ดึงข้อมูล, จัด HTML, ส่งให้ browser แสดงผล JavaScript ใช้แค่ “โรย” interactivity เล็กๆ น้อยๆ

มันคือวิธีสร้างเว็บแบบดั้งเดิม — แต่ทำให้ดี ทำให้เร็ว ทำให้สนุก


ต่อไป: Part 3: ลงมือสร้าง → — Tutorial ตั้งแต่ศูนย์ + ของดีที่มากับ Rails + Authentication + API mode