# ─── Stage 1: ติดตั้ง dependencies (npm ci รวม postinstall คัดลอก pdf.worker) ─── FROM node:24-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ # postinstall รัน scripts/patch-pdf-worker.mjs + copy-swagger-ui.mjs + fetch-office-js.mjs ตอน npm ci — คัดลอกเข้ามาก่อน ไม่งั้นหาไฟล์ไม่เจอ # (fetch-office-js ดาวน์โหลด Office.js runtime จาก appsforoffice.microsoft.com — build stage ต้องออกอินเทอร์เน็ตถึง CDN นี้ได้) COPY scripts/patch-pdf-worker.mjs scripts/copy-swagger-ui.mjs scripts/fetch-office-js.mjs scripts/ # postinstall เขียน public/note/pdf.worker.min.mjs (script สร้าง dir ปลายทางเอง) RUN npm ci # ─── Stage 2: build Next.js (output: standalone) ─── FROM node:24-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # ประกันว่า pdf.worker.min.mjs ตรงกับเวอร์ชัน pdfjs-dist ใน node_modules เสมอ # (postinstall รันใน stage deps ซึ่งยังไม่มี public/ ของ repo) — ต้องใช้ตัว patch ด้วย # ไม่ใช่ copyFileSync เปล่า ๆ ไม่งั้น worker ที่ patch fake-space ไทยถูกเขียนทับด้วยตัวดิบ # + คัดลอก asset Swagger UI (public/note/swagger-ui/) ให้ตรงเวอร์ชันเช่นกัน # + ดาวน์โหลด Office.js runtime ล่าสุด (public/wordaddin/office-js/) ของ taskpane wordaddin RUN node scripts/patch-pdf-worker.mjs && node scripts/copy-swagger-ui.mjs && node scripts/fetch-office-js.mjs ENV NEXT_TELEMETRY_DISABLED=1 # db/index.ts throw ถ้าไม่มี DATABASE_URL ตอน next build เก็บ page data — # ใส่ค่า dummy (postgres.js เชื่อมแบบ lazy ไม่มี query ตอน build) ตัวจริงฉีดตอน docker run ENV DATABASE_URL=postgresql://placeholder:placeholder@127.0.0.1:5432/placeholder ENV MINIO_ENDPOINT=127.0.0.1 ENV MINIO_PORT=19000 ENV MINIO_USE_SSL=false ENV MINIO_ACCESS_KEY=minioadmin ENV MINIO_SECRET_KEY=minioadmin ENV DOCVER_API_KEY=2d9b76d49a4f5311f6a0dacf759ecec53f34b6d102f24c696890a4052c99cf54 ENV DOCVER_TOKEN_SECRET=487023dd1c1c4d6a07475e96b1cc300fec1e68ac9c2dd7d5ac9b1bf75da1053e ENV DOCVER_TOKEN_TTL_SECONDS=604800 ENV MINIO_BUCKET=docver ENV DOCVER_LINK_TTL_SECONDS=900 RUN npm run build # ─── Stage 3: runtime ตัวเบา (แค่ standalone + public/) ─── FROM node:24-alpine AS runner WORKDIR /app ENV NODE_ENV=production \ NEXT_TELEMETRY_DISABLED=1 \ PORT=3000 \ HOSTNAME=0.0.0.0 RUN addgroup -S nodejs && adduser -S nextjs -G nodejs # Next 14.2 standalone **ไม่รวม public/ และ .next/static เอง** — ต้องคัดลอกเองทั้งคู่ # (ก่อนหน้านี้เข้าใจผิดว่า Next รวม public/ ให้: จริง ๆ มันหลุดเข้ามาแค่บางส่วนผ่าน output # file tracing ที่ตาม fs.readFile ของ server code (public/note จาก lib/server-pdf.ts, # ฟอนต์ Sarabun จาก export route) — public/wordaddin/office-js ไม่มีใครอ่านผ่าน fs # จึงไม่ถูกดึงมา แล้ว taskpane พังใน production 2026-10-16) COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/public ./public # migration: ไฟล์ SQL (drizzle-kit generate) + runner — รันตอน container start COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts # postgres.js โดน Next บีบ bundle เข้า server chunks — standalone node_modules ไม่มี package นี้ # แต่ migrate.mjs import มันตรง ๆ → คัดลอกจาก stage deps (v3 ไม่มี dependency ย่อย โฟลเดอร์เดียวพอ) COPY --from=deps --chown=nextjs:nodejs /app/node_modules/postgres ./node_modules/postgres # สร้างโฟลเดอร์ uploads ให้ user nextjs เป็นเจ้าของ — named volume ที่ compose mount ทับ # (เก็บไฟล์อัปโหลดข้าม redeploy) จะได้ ownership จาก image ตอน init: # ไม่มี dir นี้ใน image = volume ถูกสร้างโดย root แล้ว action เขียนไฟล์ไม่ได้ (EACCES) RUN mkdir -p public/note/uploads && chown nextjs:nodejs public/note/uploads USER nextjs EXPOSE 3000 # DATABASE_URL ฉีดตอนรันผ่าน env ของ container (อย่าฝังใน image) # migrate สร้าง database ถ้ายังไม่มี + apply ไฟล์ที่ยังไม่ได้ apply (idempotent) # seed-samples sync ตาราง files กับ public/note/samples/ ที่ bake อยู่ใน image (idempotent) CMD ["sh", "-c", "node scripts/migrate.mjs && node scripts/seed-samples.mjs && exec node server.js"]