รัน Easy CMS หลาย server ด้วย Docker และ Postgres
Easy CMS สอง instance หลัง load balancer บน Postgres และ bucket เดียวกันด้วย Docker Compose ปิดตัวไหนเว็บก็ยังตอบ
server เดียวพอสำหรับเว็บส่วนใหญ่ แต่ server ตัวที่สองทำให้ deploy ได้โดยไม่มี downtime และเว็บยังทำงานเมื่อ server ตัวหนึ่งล่ม คู่มือนี้ตั้ง Easy CMS สอง instance ด้วย Docker Compose: image อย่างเป็นทางการ, Postgres, bucket สำหรับไฟล์ upload และ Caddy ข้างหน้า
คู่มือนี้ใช้ standalone server (easy-cms serve) แอป Next.js หรือ Nuxt ทำแบบเดียวกันได้ โดยรัน image ของแอปคุณหลายตัวแทน
สิ่งที่ต้องมี
Docker ที่มี Compose
bucket แบบ S3 และ key: S3, Cloudflare R2 หรือ MinIO เพราะเมื่อมีสอง server ไฟล์ upload อยู่บนดิสก์ของ server ไม่ได้
โปรเจกต์ที่มี
easy-cms.config.tsและ migration ในeasy-cms/migrations(รันeasy-cms migrate:create initครั้งเดียวในช่วงพัฒนา)
1. Config
ทุกค่ามาจาก environment ทั้งสอง instance จึงใช้ image เดียวกัน:
import { defineConfig } from '@easy-cms/core'
import { postgres } from '@easy-cms/db-postgres'
import { s3Storage } from '@easy-cms/storage-s3'
export default defineConfig({
secret: process.env.EASY_CMS_SECRET as string,
serverURL: process.env.SERVER_URL,
db: postgres({ url: process.env.DATABASE_URL as string, max: 5 }),
upload: {
storage: s3Storage({
bucket: process.env.S3_BUCKET as string,
region: process.env.S3_REGION ?? 'auto',
endpoint: process.env.S3_ENDPOINT,
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
}),
},
collections: [
{ slug: 'posts', drafts: true, schedule: true, fields: [{ name: 'title', type: 'text' }] },
],
})2. Image
image อย่างเป็นทางการมี Easy CMS และ adapter อยู่แล้ว image ของคุณเพิ่มแค่ config และ migration:
FROM ghcr.io/maritonx/easy-cms:1
COPY easy-cms.config.ts ./
COPY easy-cms ./easy-cms3. Compose
compose.yaml รัน Postgres, service migrate ที่รันครั้งเดียว, สอง instance และ load balancer:
x-app: &app
build: .
environment:
EASY_CMS_SECRET: ${EASY_CMS_SECRET:?set EASY_CMS_SECRET in .env}
SERVER_URL: ${SERVER_URL:-http://localhost:8080}
DATABASE_URL: postgres://cms:${POSTGRES_PASSWORD:?}@db:5432/cms
S3_BUCKET: ${S3_BUCKET:?}
S3_REGION: ${S3_REGION:-auto}
S3_ENDPOINT: ${S3_ENDPOINT:-}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:?}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:?}
# Migrations run once, in the migrate service.
EASY_CMS_MIGRATE: "false"
depends_on:
migrate:
condition: service_completed_successfully
restart: unless-stopped
services:
db:
image: postgres:17
environment:
POSTGRES_USER: cms
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
POSTGRES_DB: cms
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cms -d cms"]
interval: 5s
retries: 10
restart: unless-stopped
migrate:
<<: *app
entrypoint: ["easy-cms", "migrate"]
depends_on:
db:
condition: service_healthy
restart: "no"
app1: *app
app2: *app
lb:
image: caddy:2
ports:
- "8080:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
depends_on: [app1, app2]
restart: unless-stopped
volumes:
pgdata:instance ทั้งสองรอ migrate เสร็จก่อน ฐานข้อมูลจึงเป็นเวอร์ชันล่าสุดก่อนตัวไหนจะรับ request และ migration รันครั้งเดียว ไม่ใช่รันในทั้งสองตัว
4. Load balancer
Caddy ส่ง request ให้ทั้งสอง instance สลับกัน และหยุดส่งให้ตัวที่ /readyz ไม่ตอบ 200:
:80 {
reverse_proxy app1:4000 app2:4000 {
lb_policy round_robin
health_uri /readyz
health_interval 5s
}
}ไม่ต้องใช้ sticky session เพราะ session, การจำกัดการ login และคิวงานอยู่ใน Postgres instance ไหนก็ตอบ request ไหนก็ได้
5. เริ่มระบบ
ใส่ค่าลับใน .env ข้าง compose.yaml:
printf 'EASY_CMS_SECRET=%s\nPOSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 16)" > .envเพิ่ม S3_BUCKET, S3_ACCESS_KEY_ID และ S3_SECRET_ACCESS_KEY ถ้าใช้ R2 หรือ MinIO ให้เพิ่ม S3_ENDPOINT ด้วย แล้วรัน:
docker compose up -d --build
docker compose exec app1 easy-cms admin:create --email you@example.comเปิด http://localhost:8080/admin แล้ว login ไฟล์ upload ไปอยู่ใน bucket ไฟล์ที่ upload ตอนที่ instance หนึ่งตอบอยู่ จึงเปิดผ่านอีกตัวได้
6. ลองดู
ปิด instance หนึ่งแล้วใช้ admin ต่อ:
docker compose stop app1ภายในไม่กี่วินาที Caddy จะรู้ว่า app1 ไม่ตอบ /readyz และส่งทุก request ไปที่ app2 เปิดกลับด้วย docker compose start app1
deploy เวอร์ชันใหม่
build ใหม่ migrate ครั้งเดียว แล้วเปลี่ยน instance ทีละตัว จะมีตัวหนึ่งตอบอยู่เสมอ:
docker compose build
docker compose run --rm migrate
docker compose up -d --no-deps app1
docker compose up -d --no-deps app2ก่อนขึ้นระบบจริง
ใส่ HTTPS ข้างหน้า: ใส่ domain ใน Caddyfile แทน
:80แล้ว Caddy จะขอ certificate ให้เอง และตั้งSERVER_URLเป็นที่อยู่จริงให้จำนวน instance ×
maxน้อยกว่าmax_connectionsของ Postgres พอสมควร และใส่ PgBouncer เมื่อมีหลาย instance ดูConnectionเปิด log แบบ JSON (
EASY_CMS_LOG_FORMAT=json) ถ้าเก็บ log และ backup ทั้งฐานข้อมูลและ bucket
หน้าสถาปัตยกรรมสำหรับ production มีทุกอย่างที่รองรับ และคู่มือ Docker ครอบคลุมกรณี server เดียว