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
สรุปง่ายๆ:
- Next.js (SSR): Server สร้าง HTML → browser โหลด JS → JS “ปลุกชีพ” HTML ให้ interactive → นี่คือ hydrate
- Rails: Server สร้าง HTML → browser แสดงเลย → จบ ไม่ต้อง hydrate เพราะไม่มี React ที่ต้อง “ปลุก”
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
| ต้องการ | React | Hotwire |
|---|---|---|
| กดลิงก์ไม่ reload | React Router | Turbo Drive (อัตโนมัติ) |
| อัปเดตบางส่วนของหน้า | useState + fetch | Turbo Frames |
| Real-time update | Socket.io + state management | Turbo Streams + Action Cable |
| Dropdown, modal, toggle | React component | Stimulus controller |
| JS bundle size | 200KB-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 ก็แยก:
- Frontend → Vercel, Netlify, S3 (static files)
- Backend → Railway, Fly.io, EC2 (server)
โลกของ Rails: ทุกอย่างอยู่ในที่เดียว
[Browser] [Rails Server]
HTML + CSS ← HTML → Controller → Model → View
(แค่แสดงผล) - รับ request
- ดึงข้อมูลจาก database
- render HTML สำเร็จรูป
- ส่ง HTML กลับมาเลย
Rails เป็นทั้ง backend และ frontend ในตัวเดียว
เมื่อ user เปิด /posts:
- Browser ส่ง request ไปที่ Rails server
- Rails รัน Ruby บน server เพื่อดึงข้อมูลจาก database
- Rails render HTML สำเร็จรูปบน server (ไม่ใช่ส่ง JSON)
- 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 -->
ผลลัพธ์เหมือนกัน — ต่างกันแค่:
- JSX: JS เป็นตัวหลัก HTML เป็นส่วนหนึ่งของ JS → รันใน browser
- ERB: HTML เป็นตัวหลัก Ruby เป็นส่วนที่ฝังเข้าไป → รันบน 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 + Node | Rails | |
|---|---|---|
| ใครสร้าง HTML? | Browser (JS render) | Server (Ruby render) |
| Server ส่งอะไร? | JSON data | HTML สำเร็จรูป |
| ต้องมี JS ไหม? | ต้องมี ไม่มีหน้าขาว | ไม่ต้องก็ได้ HTML มาพร้อมแสดง |
| SEO | ต้องทำ SSR เพิ่ม (Next.js) | ได้เลย HTML มาจาก server |
| First paint | ช้า (โหลด JS → fetch data → render) | เร็ว (HTML มาพร้อมแสดง) |
| Interactivity | ทุกอย่างเป็น JS | HTML + โรย JS เล็กน้อย (Stimulus) |
| State management | Redux, Zustand, Pinia… | ไม่มี (state อยู่ใน database) |
| Deploy | แยก 2 ที่ (frontend + backend) | ที่เดียว |
| Build step | webpack / 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 |
|---|---|
| Shopify | Kubernetes + Docker (scale ใหญ่มาก) |
| GitHub | Kubernetes + Docker |
| Basecamp / HEY | Kamal + Docker บน bare metal servers |
| Cookpad | Kubernetes + Docker |
| Startup ทั่วไป | Render / Fly.io / Kamal + VPS |
คำตอบจริงๆ คือ: ไม่จำเป็นต้อง Docker — แต่ Rails community เลือก Docker เป็น default แล้ว เพราะ:
rails newสร้าง Dockerfile มาให้ ไม่ต้องเขียนเอง- ลง server ไหนก็ได้ ไม่ต้อง install Ruby, gem, Node บน server
- 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?
- AWS/GCP = ร้อยบริการ ตั้ง VPC, Security Group, IAM, RDS, ECS… ซับซ้อนมากสำหรับ app ตัวเดียว
- VPS = เครื่อง Linux 1 ตัว SSH เข้าไป ทำเลย จบ
- ราคา VPS $4-6/เดือน vs AWS EC2 เล็กสุด $15+/เดือน + RDS $15+/เดือน
ไม่ใช่ของเล่น — นี่คือ 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:
- OS: Ubuntu 24.04 LTS
- RAM: 2GB (production ควร 2GB ขึ้นไป — 1GB มักพังตอน build Docker image)
- CPU: 1-2 vCPU
- Disk: 50GB SSD
- Region: Singapore (ใกล้ไทยสุด)
สมัครเสร็จจะได้ 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 จริงใช้สำหรับ:
- Solid Cache — Rails cache backend
- Solid Queue — background job processing
- Action Cable — WebSocket pub/sub
# 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 ทำให้ครบ:
- HTTPS อัตโนมัติ (ขอ cert จาก Let’s Encrypt, auto-renew)
- HTTP → HTTPS redirect
- gzip compression
- Security headers
- Access log พร้อม rotation
# .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:
DEPLOY_HOST=159.89.123.45DEPLOY_USER=deployDEPLOY_KEY= copy ค่าจากcat ~/.ssh/deploy_key(private key ทั้งก้อน)
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) ฟรี:
- ใส่ URL:
https://payroll.yourdomain.com/up - ตั้ง check ทุก 5 นาที
- ตั้ง alert ผ่าน email / LINE / Slack
ถ้า 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