การดูแลระบบ › Migration และการ deploy
เริ่มต้น
เนื้อหา
Plugins
การดูแลระบบ
การ Deploy
สูตรสำเร็จ
Migration และการ deploy
หน้านี้สอนอะไร
การเปลี่ยน schema ถูกใช้ตอนพัฒนาอย่างไร และส่งเป็น migration ขึ้น production อย่างไร
ควรอ่านก่อน: ฐานข้อมูล
ถ้าต้องการลองบน Vercel หรือ Netlify ในไม่กี่คลิก โดยมีฐานข้อมูลและที่เก็บไฟล์สร้างให้ ใช้ deploy ด้วยคลิกเดียว
ช่วงพัฒนา: อัตโนมัติ
ระหว่างพัฒนา (NODE_ENV ไม่ใช่ production) Easy CMS จะปรับฐานข้อมูลให้ตรงกับ
config ตอนเริ่มทำงาน: field ใหม่จะกลายเป็นคอลัมน์ field ที่ถูกลบจะถูก drop
การเปลี่ยนชื่อทำให้ข้อมูลหายในช่วงพัฒนา
การเปลี่ยนชื่อ field ในช่วงพัฒนา (development) จะ drop คอลัมน์เดิมและเพิ่มคอลัมน์ใหม่ ให้ใช้ migration (ด้านล่าง) เมื่อข้อมูลมีความสำคัญ
Production: migration
ใน production จะไม่มีการเปลี่ยนแปลงใดเกิดขึ้นอัตโนมัติ server จะไม่ยอมเริ่มทำงานเมื่อมี migration ค้างอยู่หรือ config เปลี่ยนโดยไม่มี migration และจะบอกว่าต้องรันคำสั่งอะไร
npx easy-cms migrate:create init # after your first model, and after every change
git add easy-cms/migrations # review the SQL, then commit
npx easy-cms migrate # where you deploy, before starting the new versionpnpm exec easy-cms migrate:create init # after your first model, and after every change
git add easy-cms/migrations # review the SQL, then commit
pnpm exec easy-cms migrate # where you deploy, before starting the new versionyarn easy-cms migrate:create init # after your first model, and after every change
git add easy-cms/migrations # review the SQL, then commit
yarn easy-cms migrate # where you deploy, before starting the new versionbunx easy-cms migrate:create init # after your first model, and after every change
git add easy-cms/migrations # review the SQL, then commit
bunx easy-cms migrate # where you deploy, before starting the new versionmigrate:create เปรียบเทียบ config ของคุณกับ migration ล่าสุด เมื่อรันใน terminal จะถามว่า
field ที่เปลี่ยนไปถูกเปลี่ยนชื่อหรือไม่ เพื่อเก็บข้อมูลไว้ แต่ละ migration รันใน transaction เดียว migration ที่
ล้มเหลวจะถูก rollback และไม่ถูกบันทึก
ฐานข้อมูลที่ตั้งค่าด้วยการ push ในช่วงพัฒนาไม่สามารถรับ migration ได้ ให้รัน migration กับฐานข้อมูลใหม่
Environment variable
create-easy-cms ใส่ EASY_CMS_SECRET (และ DATABASE_URL หากใช้) ไว้ใน .env ไฟล์นั้น
มีไว้สำหรับช่วงพัฒนา ส่วน production จะอ่านไฟล์นี้หรือไม่ขึ้นกับวิธีเริ่ม server:
| Server | อ่าน .env ใน production หรือไม่ | สิ่งที่ต้องทำ |
|---|---|---|
Next.js (next start) | อ่าน | ไม่ต้องทำอะไร หรือตั้งค่าตัวแปรบนโฮสต์ |
Nuxt (node .output/server/index.mjs) | ไม่อ่าน | ตั้งค่าตัวแปรบนโฮสต์ หรือเริ่มด้วย node --env-file=.env .output/server/index.mjs |
| แพลตฟอร์ม (Vercel, Netlify, Fly, Docker…) | ใช้การตั้งค่าของแพลตฟอร์ม | เพิ่มตัวแปรใน dashboard, CLI หรือไฟล์ compose ของแพลตฟอร์ม ดู การ Deploy |
เมื่อไม่มี EASY_CMS_SECRET API จะตอบ 500 และ log จะแสดง
secret: is required
Log
โดยปกติ Easy CMS เขียนบรรทัดที่อ่านง่าย [easy-cms] … และ stack ของ error อยู่ใต้บรรทัดนั้น ถ้าส่ง log เข้าระบบรวบรวม log
(Datadog, Loki, CloudWatch หรือ docker logs เข้า collector) ให้เขียนเป็น JSON บรรทัดละหนึ่ง object แทน:
logs: { format: 'json' }หรือตั้ง EASY_CMS_LOG_FORMAT=json บน host ซึ่งมีผลเหนือ config:
{"time":"2026-10-25T14:22:19.280Z","level":"info","name":"easy-cms","message":"GET /api/cms/posts 200","requestId":"lb-7f3a","method":"GET","path":"/api/cms/posts","status":200,"durationMs":7,"userId":3}
{"time":"2026-10-25T14:22:20.011Z","level":"error","name":"easy-cms","message":"REST request failed: …","requestId":"0b6f…","error":{"name":"Error","message":"…","stack":"…"}}- Request id ทุก response ของ API มี header
X-Request-Idและทุกบรรทัดที่ log ระหว่างตอบ request นั้นมีrequestIdเดียวกัน ถ้า load balancer ส่งX-Request-Idมาและเป็นค่าธรรมดา (ตัวอักษร ตัวเลข._-ยาวไม่เกิน 128) จะใช้ค่านั้นต่อ id เดียวจึงตามจาก log ของ proxy มาถึง log ของ Easy CMS ได้ - หนึ่งบรรทัดต่อ request มี method, path (ไม่รวม query string ซึ่งอาจมีข้อมูลส่วนตัว), status, เวลาที่ใช้ และ id ของผู้ใช้
ปิดได้ด้วย
logs: { requests: false }ถ้า load balancer log request อยู่แล้ว - งานเบื้องหลัง การเผยแพร่ตามกำหนดเวลาและ job แต่ละรอบ log พร้อม
runIdของรอบนั้น และrequestIdของ cron ที่เรียกให้รัน
easy-cms serve ใช้ค่าเดียวกัน ถ้าส่ง logger ของคุณเองให้ createEasyCMS ค่า logs จะไม่มีผล ใช้ jsonLogger()
จาก @easy-cms/core ถ้าต้องการรูปแบบนี้
trace และ metric ผ่าน OpenTelemetry: Observability
Health check
load balancer, แพลตฟอร์ม container และ Kubernetes ถามสองคำถาม:
| Path | ตอบ | ใช้เป็น |
|---|---|---|
<api>/healthz | 200 ok เมื่อ process ยังทำงาน ไม่แตะฐานข้อมูลเลย | Liveness: restart process เมื่อไม่ตอบ |
<api>/readyz | 200 เมื่อ Easy CMS เริ่มแล้ว ฐานข้อมูลตอบ และไม่มี migration ค้าง ไม่อย่างนั้นตอบ 503 | Readiness: ส่ง traffic ไปเฉพาะ instance ที่พร้อม |
<api> คือ routes.api (ค่าเริ่มต้น /api/cms) ทุก setup จึงมีทั้งสองตัว: Next.js, Nuxt และ
standalone server ซึ่งตอบ /healthz และ /readyz ที่ root ด้วย ทั้งสองตัวไม่ต้อง login และไม่ถูก cache
{ "status": "ok", "checks": { "database": "ok", "migrations": "ok" } }
{ "status": "error", "checks": { "database": "ok", "migrations": "1 pending" } }response บอกแค่ชื่อข้อที่ไม่ผ่าน ไม่บอกรายละเอียด ข้อความ error อยู่ใน log พร้อม request id ถ้า Easy CMS
เริ่มไม่ขึ้น (config ผิด หรือต่อฐานข้อมูลไม่ได้) /healthz ยังตอบ 200 และ /readyz ตอบ 503 พร้อม "startup": "error"
# Kubernetes
livenessProbe:
httpGet: { path: /api/cms/healthz, port: 3000 }
readinessProbe:
httpGet: { path: /api/cms/readyz, port: 3000 }
periodSeconds: 5ให้ liveness เรียก healthz ไม่ใช่ readyz เพราะเมื่อฐานข้อมูลล่ม ควรเอา instance ออกจาก load balancer ไม่ใช่ restart
ทุกตัว probe ไม่ถูกเขียนเป็นบรรทัดของ request ใน log
HTTP caching
โดยปกติ response ของ API ทุกตัวเป็น no-store ทุกการอ่านจึงไปถึงฐานข้อมูล ถ้ามี CDN หรือ proxy ที่ cache ได้อยู่ข้างหน้า
ให้มันเก็บการอ่านเนื้อหาที่ publish แล้วแบบสาธารณะ:
httpCache: {
sMaxAge: 60, // CDN ส่งซ้ำได้หนึ่งนาทีโดยไม่ต้องถามใหม่
staleWhileRevalidate: 300, // แล้วส่งของเดิมได้อีกห้านาทีระหว่างดึงของใหม่
purge: async (tags) => {
await fetch('https://api.cloudflare.com/client/v4/zones/<zone>/purge_cache', {
method: 'POST',
headers: { authorization: `Bearer ${process.env.CF_TOKEN}`, 'content-type': 'application/json' },
body: JSON.stringify({ tags }),
})
},
},-
อะไรถูก cache:
GETของ list, เอกสารตาม id และ global ที่อ่านโดยไม่มี login, session cookie หรือ key และไม่ได้ขอdraftนอกนั้นยังเป็นno-storeทุกอย่าง ของที่ขึ้นกับว่าใครถามจึงไม่ไปอยู่ใน cache ที่ใช้ร่วมกัน browser ได้max-age=0และถามใหม่ด้วย ETag -
แยกต่อ collection หรือ global:
httpCache: { sMaxAge: 5 }สำหรับเนื้อหาที่เปลี่ยนบ่อย หรือhttpCache: falseเพื่อไม่ให้ cache เลย -
ETag: การอ่านที่ cache ได้ทุกตัวมี ETag ถ้า request ส่ง
If-None-Matchที่ตรงกันมา จะได้304โดยไม่มี body -
Cache tag: response มี
Cache-Tag(Cloudflare, Bunny) และSurrogate-Key(Fastly, Akamai):ecms,ecms:postsและสำหรับเอกสารเดียวecms:posts:42ส่วน global เป็นecms:global:siteฟังก์ชันpurgeได้ tag ของสิ่งที่คนอ่านเห็นเปลี่ยน (บันทึกเอกสารที่ publish แล้ว, เผยแพร่, ยกเลิกเผยแพร่, ลบ แต่ไม่รวม draft) -
Next.js: ติด tag ให้ fetch ของคุณเองด้วย
cacheTagsจาก@easy-cms/nextแล้ว revalidate ในpurge:import { cacheTags } from '@easy-cms/next' import { revalidateTag } from 'next/cache' await fetch(`${api}/posts/${id}`, { next: { tags: cacheTags.document('posts', id) } }) // ใน config httpCache: { purge: (tags) => tags.forEach((tag) => revalidateTag(tag)) } -
Nuxt: ถ้าใช้ cached handler ของ Nitro ที่ใช้ tag เหล่านี้เป็น key ให้ลบรายการที่ตรงกันออกจาก
useStorage('cache')ในpurge -
onRequest: CDN แยก cache ตาม URL (รวม host) ถ้าonRequestของคุณเลือก context จากอย่างอื่น เช่น header ให้ใส่ header นั้นในVaryที่ CDN หรือปิดhttpCacheของ collection เหล่านั้น
Checklist
- ตั้งค่า
EASY_CMS_SECRETใน environment ของ production (อักขระสุ่มอย่างน้อย 32 ตัว:openssl rand -hex 32) ดู Environment variable NODE_ENV=production- deploy โฟลเดอร์
easy-cms/migrationsไปด้วย และรันeasy-cms migrate - เริ่ม server จาก root ของโปรเจกต์: path ฐานข้อมูลแบบ relative, migration และไฟล์ที่อัปโหลด อ้างอิงจาก working directory
- ไฟล์ที่อัปโหลดลงดิสก์ในเครื่องต้องใช้ persistent volume บนแพลตฟอร์ม serverless ให้ใช้ S3 storage, Vercel Blob หรือ Netlify Blobs
- ตั้ง
EASY_CMS_SETUP_CODEไว้จนกว่าจะมี admin คนแรก กันคนอื่นแย่งเว็บ - build บน OS และสถาปัตยกรรมเดียวกับ server เมื่อใช้ SQLite (native driver)
- เมื่ออยู่หลัง proxy ที่เชื่อถือได้ ให้เปิด
trustProxyเพื่อจำกัดอัตราการเข้าสู่ระบบแยกตาม IP - ตั้งค่า
auth.trustedOriginsหากหน้า admin หรือ frontend เรียก API จาก origin อื่น - มีbackupของฐานข้อมูลและไฟล์อัปโหลด และเคยลองกู้คืนแล้ว
หลังเปิดใช้งาน แดชบอร์ดจะบอก admin ว่ามีอะไรต้องดูแล ดูตรวจสุขภาพระบบ
ขั้นต่อไป
- การ Deploy: ทีละขั้นตอนบน Vercel, Netlify, Docker, VPS และโฮสต์อื่นๆ
- สถาปัตยกรรมสำหรับ production: หลาย instance หลัง load balancer
- Backup และการอัปเกรด: สำรองข้อมูลก่อนอัปเกรด
- ความปลอดภัย: เช็กลิสต์ก่อนขึ้นระบบจริง