# รัน Easy CMS หลาย server: Docker Compose, Postgres, S3 และ load balancer

> ตั้งค่าทีละขั้นด้วย Docker Compose: Easy CMS สอง instance จาก image อย่างเป็นทางการ, Postgres, storage แบบ S3 และ Caddy เป็น load balancer โดยรัน migration ครั้งเดียว

URL: https://easy-cms-website.vercel.app/th/blog/run-easy-cms-on-several-servers-th  
Published: 2026-10-25  
Updated: 2026-10-28

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 เดียวกัน:

```ts
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:

```dockerfile
FROM ghcr.io/maritonx/easy-cms:1
COPY easy-cms.config.ts ./
COPY easy-cms ./easy-cms
```

## 3. Compose

`compose.yaml` รัน Postgres, service `migrate` ที่รันครั้งเดียว, สอง instance และ load balancer:

```yaml
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`:

```txt
: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`:

```bash
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` ด้วย แล้วรัน:

```bash
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 ต่อ:

```bash
docker compose stop app1
```

ภายในไม่กี่วินาที Caddy จะรู้ว่า `app1` ไม่ตอบ `/readyz` และส่งทุก request ไปที่ `app2` เปิดกลับด้วย `docker compose start app1`

## deploy เวอร์ชันใหม่

build ใหม่ migrate ครั้งเดียว แล้วเปลี่ยน instance ทีละตัว จะมีตัวหนึ่งตอบอยู่เสมอ:

```bash
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](/th/docs/databases#connections)
- เปิด log แบบ JSON (`EASY_CMS_LOG_FORMAT=json`) ถ้าเก็บ log และ backup ทั้งฐานข้อมูลและ bucket

หน้า[สถาปัตยกรรมสำหรับ production](/th/docs/deploy/production) มีทุกอย่างที่รองรับ และ[คู่มือ Docker](/th/docs/deploy/docker) ครอบคลุมกรณี server เดียว
