Dockerfile สำหรับ Node.js production ควรมีอะไรบ้าง
หลายโปรเจกต์เริ่มจาก Dockerfile แบบสั้นมาก
- ใช้ base image
- copy source ทั้งหมด
npm installnpm 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:20node:20-slimnode: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.jsonpackage-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 แล้ว
บทความที่ควรอ่านต่อ
- Docker Healthcheck สำคัญยังไง และเขียนแบบไหนไม่หลอกตัวเอง
- Cloud Run vs GCE vs GKE ควรเลือกอะไรสำหรับเว็บแอปและ backend
- GitHub Actions Release Checklist สำหรับ production deploy
สรุป
Dockerfile สำหรับ Node.js production ที่ดีไม่ได้ซับซ้อนเพราะมีบรรทัดเยอะ แต่ซับซ้อนเพราะมันต้องสะท้อนขอบเขตของ runtime ให้ถูก ว่าอะไรควรอยู่ใน image อะไรควรอยู่ข้างนอก และอะไรควรถูกเตรียมให้พร้อมก่อนถึงเวลา deploy
ถ้าจับหลักได้ถูกตั้งแต่ base image, build stages, dependency install, user permissions และ health semantics คุณจะได้ image ที่เบากว่า ปลอดภัยกว่า และดูแลง่ายกว่าในระยะยาว
สรุปสั้นที่สุดคือ
production Dockerfile ที่ดีควรทำให้ runtime แคบลง ชัดขึ้น และคาดเดาได้มากขึ้น