บทที่ 9 — Docker: ห่อเว็บให้ยกไปรันที่ไหนก็ได้
ปัญหาที่ Docker มาแก้
เว็บที่รันได้ในเครื่องคุณ ไม่ได้แปลว่าจะรันได้บน server เพราะ server อาจมี Node.js คนละเวอร์ชัน ไม่มี library ที่คุณติดตั้งไว้ หรือตั้งค่าระบบไว้คนละแบบ
Docker แก้ด้วยการห่อ ทุกอย่างที่ต้องใช้รัน ไว้ในกล่องเดียว — โค้ด + Node.js + library + การตั้งค่า ยกกล่องนี้ไปวางที่ไหนก็ทำงานเหมือนกันเป๊ะ
สองคำที่ต้องแยกให้ออก
Image กับ Container ต่างกันยังไง
Image = แม่พิมพ์
- ไฟล์ก้อนหนึ่ง ไม่ทำงานเอง
- สร้างครั้งเดียว ใช้ซ้ำได้ตลอด
- ก๊อปไปเก็บ/ส่งต่อได้
- ของโปรเจกต์นี้หนัก 85 MB
- เหมือนไฟล์ติดตั้งโปรแกรม
Container = ของที่กำลังรัน
- เกิดจากการเอา image มารัน
- จาก image เดียวเปิดกี่ container ก็ได้
- ปิด/เปิด/ลบได้ ไม่กระทบ image
- ของโปรเจกต์นี้ชื่อ apexhuas-app-1
- เหมือนโปรแกรมที่เปิดอยู่
การ deploy เวอร์ชันใหม่ = สร้าง image ใหม่ → หยุด container เก่า → เปิด container ใหม่จาก image นั้น
Dockerfile: สูตรสร้าง image
Dockerfile คือไฟล์สูตรที่บอกว่าจะห่อยังไง ของโปรเจกต์นี้ใช้เทคนิค multi-stage คือแบ่งเป็น 3 ช่วง แล้วเก็บเฉพาะของที่จำเป็นจริงลงกล่องสุดท้าย
Dockerfile 3 ช่วงของโปรเจกต์นี้
1. deps
ก๊อปแค่ package.json กับ package-lock.json เข้ามาแล้วติดตั้ง — แยกขั้นนี้ออกมาเพราะถ้าโค้ดเปลี่ยนแต่ library ไม่เปลี่ยน Docker จะข้ามขั้นนี้ ทำให้ build ครั้งต่อไปเร็วมาก
FROM node:24-alpine AS deps
COPY package.json package-lock.json ./
RUN npm ci
2. builder
เอา library จากขั้นก่อนมา + ก๊อปโค้ดทั้งหมดเข้ามา แล้วสั่ง build ผลลัพธ์คือเว็บเวอร์ชันพร้อมใช้
FROM node:24-alpine AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
3. runner
เริ่มจากกล่องเปล่าใหม่ แล้วก๊อปมาเฉพาะผลลัพธ์ที่ต้องใช้รันจริง — ไม่เอา source code ดิบ ไม่เอาเครื่องมือ build ทำให้เหลือแค่ 85 MB และปลอดภัยกว่า
FROM node:24-alpine AS runner
COPY --from=builder /app/.next/standalone ./
USER nextjs
CMD ["node", "server.js"]
ถ้าไม่แบ่ง stage กล่องจะติดทั้ง source code และเครื่องมือ build ไปด้วย ใหญ่กว่านี้หลายเท่า
จุดที่คนมักพลาด
รายละเอียดใน Dockerfile ที่มีเหตุผลอยู่เบื้องหลัง (คลิกดู)
คลิกชั้นใดก็ได้เพื่อดูรายละเอียด
ใช้ image ฐาน alpine
Alpine คือ Linux รุ่นจิ๋ว ทำให้กล่องเล็กลงมาก เทียบกับ image ปกติที่ใหญ่กว่า 5-10 เท่า
ก๊อป package.json ก่อนโค้ด
Docker แคชผลแต่ละขั้น ถ้าก๊อปโค้ดทั้งหมดก่อนติดตั้ง library ทุกครั้งที่แก้โค้ดนิดเดียวมันจะติดตั้ง library ใหม่หมด ช้ามาก
USER nextjs
รันเว็บด้วยผู้ใช้ธรรมดา ไม่ใช่ root ถ้ามีช่องโหว่ ผู้บุกรุกจะทำอะไรได้จำกัดกว่ามาก
output: standalone
สั่ง Next.js ให้รวมเฉพาะไฟล์ที่จำเป็นต่อการรันไว้ที่เดียว เลยไม่ต้องขน node_modules ทั้งก้อนขึ้น server
.dockerignore
บอกว่าห้ามก๊อปอะไรเข้ากล่อง เช่น node_modules, .git, .env, เอกสาร — ทำให้เล็กลงและไม่มีความลับหลุดติดไปในกล่อง
คำสั่ง Docker ที่ได้ใช้จริง
docker build -t apexhuas:uat . # สร้าง image จาก Dockerfile ในโฟลเดอร์นี้
docker image ls # ดู image ที่มีในเครื่อง
docker ps # ดู container ที่กำลังรัน
docker logs <ชื่อ container> # ดู log ว่าเว็บพ่นอะไรออกมา
docker compose up -d # เปิดทุก service ตามไฟล์ compose.yaml
docker compose pull && docker compose up -d # ดึงเวอร์ชันใหม่มาแล้วรีสตาร์ทcompose.yaml คือไฟล์ที่บอกว่า "ระบบนี้ประกอบด้วย container อะไรบ้าง" ของเราบน server มี 2 ตัวคือเว็บกับ Caddy (ตัวรับ request) — รายละเอียดบทหน้า
registry: โกดังเก็บ image
สร้าง image เสร็จแล้วจะส่งไป server ยังไง? คำตอบคือฝากไว้ที่ registry แล้วให้ server ไปดึงเอา
โปรเจกต์นี้ใช้ GHCR (GitHub Container Registry) ซึ่งมาพร้อม GitHub อยู่แล้ว
image เดินทางยังไง (คลิกดูแต่ละจุด)
GitHub Actions
GHCR
Server
คลิกกล่องด้านบนเพื่อดูคำอธิบาย
build image
หลัง merge เข้า uat ระบบสร้าง image จาก Dockerfile บนเครื่องของ GitHub
push ขึ้น GHCR
อัปโหลด image พร้อมติดป้ายสองแบบ: :uat (ป้ายเลื่อนตามเวอร์ชันล่าสุด) และ :uat-<รหัส commit> (ป้ายถาวร ไว้ย้อนกลับ)
ghcr.io/natt-woramet/apexhuas
โกดังส่วนตัวของโปรเจกต์ เก็บทุกเวอร์ชันที่เคย build ทำให้ย้อนกลับไปเวอร์ชันก่อนได้ถ้าเวอร์ชันใหม่มีปัญหา
docker compose pull
server ดึง image เวอร์ชันใหม่ลงมา — ใช้เวลาไม่กี่วินาทีเพราะดึงเฉพาะส่วนที่ต่างจากของเดิม
รีสตาร์ท container
หยุดตัวเก่า เปิดตัวใหม่จาก image ที่เพิ่งดึงมา เว็บดาวน์ประมาณ 1-2 วินาที
เช็คความเข้าใจ
-
เพราะใช้ multi-stage build — กล่องสุดท้ายเริ่มจากศูนย์แล้วก๊อปมาเฉพาะผลลัพธ์ที่ต้องใช้รัน (Next.js standalone) ไม่เอา source code ดิบ ไม่เอา library สำหรับ build และไม่เอาเครื่องมือพัฒนา
-
เพราะ :uat ชี้ไปเวอร์ชันล่าสุดเสมอ ถ้าเวอร์ชันใหม่พังแล้วอยากย้อนกลับ คุณต้องมีชื่อที่ชี้ไปเวอร์ชันเก่าแบบเจาะจง ป้ายที่มีรหัส commit ทำหน้าที่นั้น
-
แปลว่าโค้ดพึ่งพาบางอย่างที่มีเฉพาะในเครื่องคุณ เช่น ไฟล์ที่ไม่ได้ commit ขึ้น repo หรือ package-lock.json ไม่ตรงกับ package.json — เป็นสัญญาณดีที่ CI จับได้ก่อนขึ้น server
บทหน้า: server จริง โดเมน และ HTTPS — ตามรอย request หนึ่งอันตั้งแต่คนพิมพ์ URL จนได้หน้าเว็บ