# Deploy ด้วย Docker {#deploy-with-docker}

::: info หน้านี้สอนอะไร
สร้าง container image ของเว็บที่มี Easy CMS แล้วรันด้วย Docker Compose แบบใช้ Postgres ใน container ที่สอง หรือแบบ
SQLite บน volume image เดียวกันนี้รันได้บน server หรือแพลตฟอร์มใดก็ได้ที่รัน container

**ควรอ่านก่อน:** [เลือกที่ deploy](./), [Migration และการ deploy](../deployment)
:::

container รันอยู่ตลอด server จึงรัน[งานตามเวลา](../drafts#scheduled-publishing)เองทุกนาที และเก็บ SQLite กับไฟล์
อัปโหลดไว้บน volume ได้ การ build ใน image ยังทำให้ native driver ของ SQLite ตรงกับระบบที่รันจริงด้วย

## 1. Dockerfile {#1-the-dockerfile}

image จะ migrate ฐานข้อมูลตอน start แล้วจึงเสิร์ฟเว็บ เลือกตามแบบที่ใช้:

::: code-group

```dockerfile [Next.js]
# Build: ติดตั้งทุกอย่างแล้ว build Next.js ไม่ต้องใช้ฐานข้อมูลตอน build
FROM node:24-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx next build

# Run: แอปที่ build แล้วพร้อม node_modules (CLI easy-cms ใช้รัน migration)
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
COPY --from=build /app ./
EXPOSE 3000
HEALTHCHECK --interval=30s --start-period=30s \
  CMD node -e "fetch('http://localhost:3000/api/cms/readyz').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
CMD ["sh", "-c", "npx easy-cms migrate && npx next start"]
```

```dockerfile [Nuxt]
FROM node:24-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx nuxi build

FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOST=0.0.0.0
COPY --from=build /app ./
EXPOSE 3000
HEALTHCHECK --interval=30s --start-period=30s \
  CMD node -e "fetch('http://localhost:3000/api/cms/readyz').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
CMD ["sh", "-c", "npx easy-cms migrate && node .output/server/index.mjs"]
```

```dockerfile [Standalone]
# image อย่างเป็นทางการ: easy-cms serve พร้อม adapter ที่ใช้บ่อย ดู "image อย่างเป็นทางการ" ข้างล่าง
FROM ghcr.io/maritonx/easy-cms:1
COPY easy-cms.config.ts ./
COPY easy-cms ./easy-cms
```

:::

- script `build` ต้องไม่ migrate เพราะ image ถูก build โดยไม่มีฐานข้อมูล ให้รัน `next build` (หรือ `nuxi build`) ตรงๆ
  แบบข้างบน
- health check เรียก `/readyz`: ฐานข้อมูลตอบและไม่มี migration ค้าง ดู[Health check](../deployment#health-checks)
- image แบบ standalone ต้องการแค่ config และ migration ของคุณ ตัว Easy CMS มาจาก[image อย่างเป็นทางการ](#the-official-image)
  และฟังที่ port 4000
- ถ้าใช้ pnpm ให้เริ่ม stage build ด้วย `RUN corepack enable` คัดลอก `pnpm-lock.yaml` และรัน
  `pnpm install --frozen-lockfile`

กันไฟล์บนเครื่องไม่ให้เข้าไปใน image ด้วย `.dockerignore`:

```txt [.dockerignore]
node_modules
.next
.output
.env
.pglite
uploads
data
*.db
.git
```

### image อย่างเป็นทางการ {#the-official-image}

`ghcr.io/maritonx/easy-cms` รัน [`easy-cms serve`](../standalone) ของเวอร์ชันตาม tag: `1.7.0`, `1.7`, `1` (1.x
ล่าสุด) และ `latest` สำหรับ `linux/amd64` และ `linux/arm64` ข้างในมี `easy-cms`, `@easy-cms/core`, adapter ของ
SQLite และ Postgres, S3 storage, อีเมล SMTP และ `sharp` สำหรับย่อรูป build จากแพ็กเกจบน npm

| | |
|---|---|
| ไฟล์ของคุณ | `/app`: คัดลอก `easy-cms.config.ts` และโฟลเดอร์ `easy-cms` (migration) ไปไว้ที่นี่ |
| ข้อมูล | `/app/data` เป็น volume: ชี้ SQLite (`file:./data/cms.db`) และไฟล์ upload บนดิสก์ (`upload: { dir: './data/uploads' }`) มาที่นี่ |
| Port | `4000` (`PORT`) |
| User | `node` ไม่ใช่ root |
| ตอนเริ่ม | `easy-cms migrate` แล้ว `easy-cms serve --trust-proxy` ตั้ง `EASY_CMS_MIGRATE=false` เพื่อข้าม migration |
| Health check | `/readyz`: ฐานข้อมูลตอบและไม่มี migration ค้าง |

แพ็กเกจอื่นที่ config import เช่น plugin ให้ติดตั้งด้วย `npm install` ใน Dockerfile ของคุณ:

```dockerfile
FROM ghcr.io/maritonx/easy-cms:1
RUN npm install @easy-cms/plugin-seo@1 @easy-cms/plugin-redirects@1
COPY easy-cms.config.ts ./
COPY easy-cms ./easy-cms
```

ใช้เวอร์ชันของแพ็กเกจเหล่านั้นให้ตรงกับ release ของ image ถ้ามีหลาย container บนฐานข้อมูลเดียวกัน ให้ migrate
ครั้งเดียวก่อนเริ่ม (ขั้น release หรือ init container ที่รัน `easy-cms migrate`) แล้วเริ่ม container ด้วย `EASY_CMS_MIGRATE=false`

## 2. Compose {#2-compose}

::: code-group

```yaml [With Postgres: compose.yaml]
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      EASY_CMS_SECRET: ${EASY_CMS_SECRET:?set EASY_CMS_SECRET in .env}
      EASY_CMS_SETUP_CODE: ${EASY_CMS_SETUP_CODE:-}
      DATABASE_URL: postgres://cms:cms@db:5432/cms
    volumes:
      - uploads:/app/uploads
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:17
    shm_size: 1g
    environment:
      POSTGRES_USER: cms
      POSTGRES_PASSWORD: cms
      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

volumes:
  uploads:
  pgdata:
```

```yaml [With SQLite: compose.yaml]
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      EASY_CMS_SECRET: ${EASY_CMS_SECRET:?set EASY_CMS_SECRET in .env}
      EASY_CMS_SETUP_CODE: ${EASY_CMS_SETUP_CODE:-}
      DATABASE_URL: file:/app/data/cms.db
    volumes:
      - data:/app/data
      - uploads:/app/uploads
    restart: unless-stopped

volumes:
  data:
  uploads:
```

:::

`shm_size` เผื่อพื้นที่ให้ Postgres ทำ parallel query: ค่าเริ่มต้นของ Docker คือ 64 MB ซึ่งไม่พอเมื่อตารางใหญ่ และ query
เหล่านั้นจะล้มด้วย "could not resize shared memory segment"

config อ่านฐานข้อมูลจาก `DATABASE_URL` เช่น `postgres({ url: process.env.DATABASE_URL })` หรือ
`sqlite({ url: process.env.DATABASE_URL })` ถ้าใช้ SQLite ให้รัน container เดียว เพราะไฟล์ SQLite มี server ได้ตัวเดียว
ถ้าใช้ image แบบ standalone ให้ map `"4000:4000"` และ mount volume `data` ที่ `/app/data`

ใส่ secret ในไฟล์ `.env` ข้าง `compose.yaml` (ไม่ใส่ใน image) แล้ว start:

```bash
printf 'EASY_CMS_SECRET=%s\nEASY_CMS_SETUP_CODE=%s\n' "$(openssl rand -hex 32)" "choose-a-phrase" > .env
docker compose up -d --build
docker compose logs -f app
```

log จะแสดง migration ที่รัน แล้วตามด้วย server เปิด `http://localhost:3000/admin` แล้วสร้าง admin คนแรกด้วยรหัส
setup เปลี่ยนรหัสผ่าน Postgres ก่อนใช้ที่อื่นนอกจากเครื่องของคุณ

## 3. การอัปเดต {#3-updating}

ดึงโค้ดใหม่แล้ว build ใหม่ container ใหม่จะรัน migration ที่ค้างก่อน start และ volume เก็บฐานข้อมูลกับไฟล์อัปโหลดไว้

```bash
git pull
docker compose up -d --build
```

## 4. HTTPS และ proxy {#4-https-and-the-proxy}

วาง reverse proxy ไว้ข้างหน้าสำหรับ HTTPS เช่น Caddy, Traefik, nginx หรือของแพลตฟอร์ม proxy จะตั้ง
`X-Forwarded-For` ให้ จึงต้องเชื่อมัน: `--trust-proxy` (standalone ซึ่งอยู่ใน image ข้างบนแล้ว) หรือ
`trustProxy: true` (route handler ของ Next.js, module ของ Nuxt) จากนั้นตั้ง `serverURL` เป็นที่อยู่สาธารณะ
proxy ควร redirect HTTP ไป HTTPS ด้วย ตัวอย่างการตั้งค่า Caddy อยู่ใน [VPS](./vps#https-with-caddy)

## สำรองข้อมูล {#backups}

ทุกอย่างอยู่ใน volume: สำรอง volume `pgdata` หรือ `data` และ `uploads` หรือให้ Easy CMS สำรองฐานข้อมูลเอง
(`backups` ใน config เก็บไว้บน volume อื่นหรือใน S3) ดู [สำรองข้อมูล](../backups)

## ขั้นต่อไป {#next-steps}

- [VPS](./vps): รันเว็บเดียวกันโดยไม่ใช้ container
- [โฮสต์อื่นๆ](./other-hosts): แพลตฟอร์มที่ build และรัน image นี้ให้
