1. Home
  2. Learn
  3. Docker
  4. Dockerfile สำหรับ Node.js production ควรมีอะไรบ้าง
Docker

Dockerfile สำหรับ Node.js production ควรมีอะไรบ้าง

อธิบายว่า Dockerfile สำหรับ Node.js production ควรมีอะไรบ้าง ตั้งแต่ base image, dependency install, build step, user permissions, healthcheck, environment และการลดขนาด image โดยไม่ทำให้ runtime เปราะเกินไป

Dockerfile สำหรับ Node.js production ควรมีอะไรบ้าง

หลายโปรเจกต์เริ่มจาก Dockerfile แบบสั้นมาก

  • ใช้ base image
  • copy source ทั้งหมด
  • npm install
  • npm start

ช่วงแรกมันทำงานได้ และสำหรับ local dev มันอาจเพียงพอจริง แต่พอเริ่มใช้ใน production คำถามจะเริ่มละเอียดขึ้นทันที

image ใหญ่เกินไปไหม
build ช้าเกินไปหรือไม่
dev dependencies หลุดไปอยู่ใน runtime หรือเปล่า
container รันด้วย root ไหม
startup ช้าเพราะ build ใน runtime หรือไม่
cache layers ใช้คุ้มไหม
logs, healthcheck, env และ signal handling พร้อมหรือยัง
ถ้า deploy ซ้ำบ่อย image นี้จะสร้างภาระให้ CI/CD และ infra แค่ไหน

ปัญหาไม่ได้อยู่ที่ Dockerfile เขียนยาวหรือสั้น แต่อยู่ที่มันสะท้อนความเข้าใจเรื่อง runtime boundary ดีพอหรือไม่

บทความนี้อธิบายว่า Dockerfile สำหรับ Node.js production ควรมีอะไรบ้าง อะไรคือ baseline ที่ควรเริ่มให้ถูก และอะไรที่ไม่ควรทำถ้าไม่อยากให้ image production เปราะหรือหนักเกินจำเป็น

TL;DR

สรุปให้สั้นที่สุด

Dockerfile สำหรับ production ควรแยก build ออกจาก runtime, ลดสิ่งที่ไม่จำเป็นใน image, รันด้วย user ที่ไม่ใช่ root, และทำให้ container สะท้อนสภาพพร้อมใช้งานจริงได้

สิ่งที่ควรมีอย่างน้อยคือ

  • base image ที่เหมาะ
  • layer caching ที่ดี
  • dependency install แบบ production-minded
  • multi-stage build เมื่อมี build artifacts
  • non-root user
  • NODE_ENV=production
  • .dockerignore
  • healthcheck เมื่อเหมาะสม
  • command ที่เหมาะกับ runtime จริง
  • การ copy เฉพาะสิ่งที่จำเป็น

Dockerfile ที่ดีต้องตอบโจทย์อะไร

ก่อนดู syntax ควรถามก่อนว่า Dockerfile production ที่ดีควรช่วยอะไร

อย่างแรกคือสร้าง image ที่รันได้เสถียร
อย่างที่สองคือ build ซ้ำได้และคาดเดาได้
อย่างที่สามคือเล็กพอและเร็วพอสำหรับ CI/CD จริง
อย่างที่สี่คือไม่พกของที่ไม่ควรอยู่ใน runtime
อย่างที่ห้าคือไม่เพิ่ม risk โดยไม่จำเป็น เช่นรันเป็น root หรือเปิดเครื่องมือ debug มากเกินไป

พูดอีกแบบ Dockerfile ที่ดีไม่ได้แค่ “ทำให้แอปรันได้” แต่ทำให้แอปรันได้ในสภาพแวดล้อม production อย่างมีวินัยมากขึ้น

เริ่มจากการแยก dev mindset ออกจาก production mindset

Dockerfile สำหรับ local development กับ production ไม่ควรถูกมองเป็นของชิ้นเดียวกันเสมอไป

ของฝั่ง dev มักต้องการ

  • hot reload
  • bind mount
  • tooling ครบ
  • debug convenience
  • dependencies ครบทุกอย่าง

แต่ของ production มักต้องการ

  • runtime แคบที่สุดเท่าที่จำเป็น
  • startup ที่คาดเดาได้
  • image เล็กลง
  • attack surface ต่ำลง
  • deployment เร็วขึ้น
  • build behavior ชัดเจน

ถ้าเอา Dockerfile แบบ dev ไปใช้ production ตรง ๆ มักจะได้ image ที่ใหญ่ ช้า และพกของไม่จำเป็นเต็มไปหมด

เลือก base image ยังไง

จุดเริ่มที่สำคัญที่สุดจุดหนึ่งคือ base image

ตัวเลือกที่เจอบ่อย เช่น

  • node:20
  • node:20-slim
  • node:20-alpine

หลายทีมรีบเลือก alpine เพราะเห็นว่าเล็ก แต่ความจริงคำตอบไม่ได้ง่ายแบบ “เล็กสุด = ดีสุด” เสมอไป

node:<version>

มักครบและใช้ง่าย แต่ image ใหญ่กว่า
เหมาะกับกรณีที่คุณต้องการ compatibility สูงและยังไม่กังวลเรื่องขนาดมาก

node:<version>-slim

มักเป็นจุดสมดุลที่ดีสำหรับหลายระบบ production เพราะเล็กกว่าตัวเต็ม แต่ยังไม่สร้าง friction เท่า Alpine ในบาง dependency

node:<version>-alpine

เล็กมาก แต่บางครั้งจะมี complexity เรื่อง native modules, libc differences หรือ build dependencies เพิ่มขึ้น
ถ้า stack ของคุณเรียบง่ายและทดสอบมาแล้วก็ใช้ได้ แต่ไม่ใช่ตัวเลือกที่ “ดีกว่าเสมอไป” แค่เพราะเล็กกว่า

สำหรับหลายทีม slim มักเป็น baseline ที่ practical กว่า

ใช้ version pinning ให้ชัด

อย่าปล่อยให้ Dockerfile ลอยไปกับ latest behavior โดยไม่ตั้งใจ

ตัวอย่างที่ปลอดภัยกว่าคือ

FROM node:20-slim

ดีกว่า

FROM node:latest

เพราะ production ควรลดความเปลี่ยนแปลงที่ไม่ตั้งใจจาก upstream image ให้มากที่สุด

Layer caching สำคัญมาก

Docker build ที่ช้าเกินไปจำนวนมากเกิดจากการ copy source ทั้งหมดเร็วเกินไป ทำให้ layer cache พังทุกครั้ง

แนวทางที่ดีคือ copy เฉพาะไฟล์ dependency manifests ก่อน เช่น

  • package.json
  • package-lock.json

แล้วค่อยติดตั้ง dependencies
จากนั้นค่อย copy source code ส่วนที่เหลือ

เหตุผลคือ ถ้า source code เปลี่ยนแต่ dependency ไม่เปลี่ยน Docker จะ reuse layer install ได้ ทำให้ build เร็วขึ้นมากในชีวิตจริง

.dockerignore สำคัญไม่แพ้ Dockerfile

หลายโปรเจกต์มี Dockerfile พอใช้ได้ แต่ .dockerignore แย่มาก ทำให้ build context ใหญ่โดยไม่จำเป็น

สิ่งที่มักไม่ควรถูกส่งเข้า build context เช่น

  • node_modules
  • .git
  • logs
  • local build outputs
  • test artifacts
  • .env
  • editor files

ตัวอย่าง .dockerignore ที่ควรมี baseline เช่น

node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
coverage
dist
.next
.vscode
.DS_Store

การจัด .dockerignore ดีช่วยทั้งความเร็วและลดความเสี่ยงเผลอส่งไฟล์ไม่ควรเข้า image

ทำไม multi-stage build ถึงสำคัญ

ถ้าแอปของคุณมีขั้น build เช่น

  • TypeScript compile
  • Next.js build
  • bundling
  • asset generation
  • Prisma generate บางกรณี
  • native compilation

การใช้ multi-stage build จะช่วยมาก เพราะแยก “สิ่งที่ต้องใช้ตอน build” ออกจาก “สิ่งที่ต้องใช้ตอน runtime” ได้ชัด

แนวคิดคือ

  • stage แรกใช้ build artifacts
  • stage สุดท้าย copy เฉพาะสิ่งที่จำเป็นต่อ runtime

แบบนี้ image runtime จะไม่พก toolchains หรือไฟล์ที่ไม่จำเป็นติดไปด้วย

Dependency install ควรคิดยังไง

ใน production เรามักต้องการ install เฉพาะสิ่งที่ runtime ใช้จริง ไม่ใช่ dev dependencies ทั้งหมด

สำหรับ npm สิ่งที่พบได้บ่อยคือ

npm ci --omit=dev

หรือใช้ install strategy ที่ล็อกตาม lockfile อย่างชัดเจน

คำว่า ci สำคัญ เพราะช่วยให้ install คาดเดาได้มากกว่า npm install ในหลายบริบท และเหมาะกับ CI/CD มากกว่า

ถ้าโปรเจกต์ของคุณต้อง build ก่อน แล้ว build ต้องใช้ dev dependencies คุณอาจต้อง

  • install ครบใน build stage
  • build ให้เสร็จ
  • คัดเฉพาะ runtime artifacts + production dependencies ไปยัง final stage

อย่า build ใน runtime โดยไม่จำเป็น

ข้อผิดพลาดที่เจอบ่อยคือ final container ยังต้อง

  • compile TypeScript
  • run bundler
  • install build tools
  • generate files หนัก ๆ ตอน start

สิ่งเหล่านี้ทำให้ startup ช้าและทำให้ runtime behavior ไม่นิ่ง

แนวทางที่ดีกว่าคือ build ให้เสร็จใน image stage ก่อน แล้ว final runtime stage แค่รัน output ที่พร้อมใช้งานจริง

รันด้วย user ที่ไม่ใช่ root

อันนี้เป็น baseline ที่ควรทำถ้าไม่ได้มีเหตุผลชัดเจนจริง ๆ ที่ต้องใช้ root

หลาย base image ของ Node มี user ชื่อ node มาให้แล้ว หรือคุณอาจสร้าง user เองก็ได้

หลักคิดคือ ถ้า process ถูก compromise ผลกระทบจะควรถูกจำกัดให้แคบที่สุด การรันเป็น root โดยไม่จำเป็นจึงไม่ควรเป็น default

ตัวอย่างเช่น

USER node

หรือสร้าง user/group ที่เหมาะกับ app โดยเฉพาะ

NODE_ENV=production ควรตั้งเมื่อไร

สำหรับ Node.js app จำนวนมาก การตั้ง NODE_ENV=production ยังมีความหมาย เช่น

  • library บางตัวเปลี่ยน behavior
  • logging/debug behavior ต่างกัน
  • framework บางตัว optimize runtime ต่างกัน

ควรตั้งให้ชัดใน runtime stage เช่น

ENV NODE_ENV=production

แต่อย่าคิดว่าตั้งแค่นี้แล้ว production พร้อม เพราะมันเป็นแค่หนึ่งในหลายชิ้น

อย่า copy ทุกอย่างแบบไม่คิด

ตัวอย่างที่มักเห็นบ่อยคือ

COPY . .

แม้จะสะดวก แต่ถ้าใช้แบบไม่คิดเลย มันมักพาไฟล์ที่ไม่จำเป็นเข้าไปใน image และทำให้ caching แย่ลง

ใน production ควรรู้ให้ชัดว่า final stage ต้องใช้แค่

  • built output
  • runtime package manifests
  • production dependencies
  • static assets ที่จำเป็น
  • scripts ที่จำเป็นจริง

ไม่ใช่ source tree ทั้งหมดเสมอไป

Command ที่ใช้รันควรตรงกับ runtime จริง

ใน production ควรใช้ command ที่ชัด เช่น

CMD ["node", "dist/server.js"]

ดีกว่าใช้ script ที่ซ่อนหลายชั้นเกินไปโดยไม่จำเป็น

เช่นถ้า npm start ในโปรเจกต์คุณยังทำงานอื่นอีก เช่น migration, build, generate หรือ warmup แบบซับซ้อน ก็ควรทบทวนให้ดีว่าพฤติกรรมเหล่านั้นเหมาะจะเกิดทุกครั้งตอน container start จริงหรือไม่

Healthcheck ควรอยู่ใน Dockerfile ไหม

ถ้าบทบาทของ container นั้นเหมาะกับการมี health semantics ที่ชัด ก็มีประโยชน์มาก

เช่น API service ที่มี /health หรือ /ready ชัดเจน
แต่ถ้าเป็น one-off job หรือ worker บางประเภท อาจต้องคิดให้ดีว่า healthcheck จะวัดอะไร

การมี HEALTHCHECK ใน Dockerfile ช่วยได้เมื่อ

  • endpoint ที่ probe มีความหมายจริง
  • orchestration หรือ runtime ด้านบนใช้สัญญาณนี้ต่อ
  • start-period และ timeout ถูกตั้งอย่างสมเหตุผล

แต่ถ้าใส่ไว้เพียงเพราะ “ดูเหมือนควรมี” โดย endpoint นั้นไม่วัดอะไรจริง ก็อาจหลอกตัวเองได้มากกว่าช่วย

Example Dockerfile สำหรับ Node.js production

ตัวอย่างนี้เป็น baseline แบบ multi-stage ที่ practical สำหรับแอป Node.js/TypeScript จำนวนมาก

# Build stage
FROM node:20-slim AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Runtime stage
FROM node:20-slim AS runner

WORKDIR /app

ENV NODE_ENV=production

COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

COPY --from=builder /app/dist ./dist

USER node

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health || exit 1

CMD ["node", "dist/server.js"]

โค้ดชุดนี้กำลังช่วยอะไร

มีหลายจุดที่สำคัญ

อย่างแรก มันแยก build stage ออกจาก runtime stage ทำให้ final image ไม่ต้องพก source และ build environment ทุกอย่าง

อย่างที่สอง มันติดตั้ง production dependencies เฉพาะใน runner stage แทนที่จะหอบ dev dependencies มาทั้งหมด

อย่างที่สาม มันตั้ง NODE_ENV=production ให้ชัด และใช้ USER node เพื่อลดความเสี่ยงจากการรันเป็น root

อย่างที่สี่ มันมี healthcheck ที่ชี้ไปยัง endpoint ที่ตั้งใจออกแบบไว้ ไม่ได้ยิง root route แบบลอย ๆ

แต่ Dockerfile ตัวอย่างนี้ยังไม่ใช่คำตอบสุดท้ายของทุกระบบ

ของจริงคุณอาจต้องปรับเพิ่มตามลักษณะโปรเจกต์ เช่น

  • ถ้าใช้ Next.js final output อาจไม่ใช่ dist/server.js
  • ถ้าใช้ Prisma อาจต้อง copy generated client หรือ schema บางส่วน
  • ถ้าใช้ native modules อาจต้องดู base image ให้เหมาะ
  • ถ้ามี static assets ต้อง copy เพิ่ม
  • ถ้ามี entrypoint script จริง อาจต้องจัด signal handling และ startup logic ให้เหมาะ
  • ถ้ามี secrets หรือ env config ต้องส่งผ่าน runtime ไม่ใช่ bake ลง image

ดังนั้นให้ใช้ baseline นี้เป็นโครงคิด ไม่ใช่ copy โดยไม่เข้าใจ

เรื่อง security ที่ควรคิดเพิ่ม

Dockerfile production ไม่ได้มีแค่เรื่องขนาด image แต่มีมิติ security ด้วย เช่น

  • ใช้ user ที่ไม่ใช่ root
  • ลดเครื่องมือที่ไม่จำเป็นใน runtime
  • ไม่ copy .env เข้า image
  • ไม่ bake secrets ลง image layers
  • ใช้ base image ที่เหมาะและอัปเดตตามนโยบาย
  • จำกัดสิ่งที่ final stage พกไปจริง ๆ

ยิ่ง image แคบเท่าไร operational surface ก็ยิ่งเล็กลงเท่านั้น

เรื่อง performance และ CI/CD

Dockerfile ที่ออกแบบดีช่วยลดเวลาหลายส่วนพร้อมกัน

  • build time จาก layer caching
  • push/pull time จาก image ที่เล็กลง
  • startup time จากการไม่ build ตอน runtime
  • deploy time จาก image ที่ predictable
  • rollback time จาก artifacts ที่ชัด

ดังนั้นมันไม่ใช่แค่เรื่อง “เขียน Dockerfile ให้สวย” แต่กระทบ deploy loop ของทีมโดยตรง

ข้อผิดพลาดที่เจอบ่อย

1) ใช้ node:latest

เสี่ยงต่อ behavior เปลี่ยนโดยไม่ตั้งใจ

2) copy source ทั้งหมดเร็วเกินไป

ทำให้ cache dependency install ใช้ไม่ได้

3) ใช้ npm install แบบลอย ๆ ใน CI/CD

ทำให้ build reproducibility ลดลงเมื่อเทียบกับ lockfile-aware install

4) พก dev dependencies ไป production

ทำให้ image ใหญ่และ surface กว้างเกินจำเป็น

5) รันเป็น root ทั้งที่ไม่จำเป็น

เพิ่มความเสี่ยงโดยไม่คุ้ม

6) build ตอน container start

ทำให้ startup ช้าและ behavior ไม่นิ่ง

7) ไม่มี .dockerignore

ทำให้ build context ใหญ่และเสี่ยงเผลอพาไฟล์ไม่ควรเข้า image

8) ใส่ healthcheck แบบไม่มีความหมาย

สถานะดูดี แต่ไม่ได้ช่วย operations จริง

Dockerfile กับ Cloud Runtime เกี่ยวกันยังไง

เมื่อคุณเอา image ไป deploy บน Cloud Run, GCE, GKE หรือ platform อื่น พฤติกรรมของ image จะเริ่มสำคัญมากขึ้น เช่น

  • startup speed
  • memory footprint
  • health/readiness behavior
  • signal handling
  • logs ออก stdout/stderr หรือไม่
  • process model เหมาะกับ runtime หรือไม่

นี่คือเหตุผลว่าทำไม Dockerfile ไม่ควรถูกมองแยกจาก deployment target เพราะสิ่งที่เขียนใน image มีผลตรงต่อ runtime behavior จริง

รีวิวเชิง production-minded

Correctness

Dockerfile ที่ดีช่วยให้ runtime behavior คาดเดาได้มากขึ้น โดยเฉพาะเมื่อแยก build ออกจาก run ชัดเจน

Security

การใช้ non-root user, ลดของใน image และไม่ bake secrets ลงไป เป็น baseline ที่ควรมีใน production แทบทุกระบบ

Efficiency

multi-stage build, layer caching และ .dockerignore ที่ดี ช่วยทั้ง build speed, deploy speed และขนาด image อย่างชัดเจน

Error handling

healthcheck ที่มีความหมายและ command runtime ที่ชัด ช่วยให้ deployment, rollback และ incident debugging ง่ายขึ้นมาก

Checklist สั้น ๆ ก่อนใช้ Dockerfile ใน production

  • ใช้ base image ที่เหมาะและ pin version ชัด
  • มี .dockerignore
  • แยก dependency install ให้ใช้ cache ได้
  • ใช้ multi-stage build เมื่อมี build artifacts
  • final image พกเฉพาะ runtime essentials
  • ใช้ NODE_ENV=production
  • รันด้วย user ที่ไม่ใช่ root
  • ไม่ bake secrets ลง image
  • command runtime ชัดและไม่ทำงานแอบแฝงเกินจำเป็น
  • มี healthcheck เมื่อ service นั้นเหมาะสม
  • ทดสอบ image จริงใน environment ใกล้ production แล้ว

บทความที่ควรอ่านต่อ

สรุป

Dockerfile สำหรับ Node.js production ที่ดีไม่ได้ซับซ้อนเพราะมีบรรทัดเยอะ แต่ซับซ้อนเพราะมันต้องสะท้อนขอบเขตของ runtime ให้ถูก ว่าอะไรควรอยู่ใน image อะไรควรอยู่ข้างนอก และอะไรควรถูกเตรียมให้พร้อมก่อนถึงเวลา deploy

ถ้าจับหลักได้ถูกตั้งแต่ base image, build stages, dependency install, user permissions และ health semantics คุณจะได้ image ที่เบากว่า ปลอดภัยกว่า และดูแลง่ายกว่าในระยะยาว

สรุปสั้นที่สุดคือ

production Dockerfile ที่ดีควรทำให้ runtime แคบลง ชัดขึ้น และคาดเดาได้มากขึ้น

💬 Chat (ตอบเร็ว)