- บทนำ
- แผนการ Workshop (ประมาณ 120 นาที)
- 1. Multi-stage Build ทำงานอย่างไร
- 2. เครื่องมือที่ต้องเตรียม
- 3. โครงสร้างโครงการ
- 4. กำหนดแพ็กเกจใน package.json
- 5. กำหนดค่า TypeScript
- 6. สร้าง Express REST API
- 7. เขียน Dockerfile แบบ Multi-stage Build
- 8. สร้าง .dockerignore
- 9. สร้าง Docker Image และ Run Container
- 10. ใช้งานผ่าน Docker Compose
- 11. เพิ่ม Automated API Test
- 12. ตรวจสอบว่าการทำ Multi-stage มีผลอย่างไร
- 13. เทียบกับ Single-stage Dockerfile
- 14. เพิ่ม CI Pipeline ด้วย GitHub Actions (ทางเลือก)
- 15. ประเด็นสำหรับ Production
- 16. ปัญหาที่พบบ่อย
- 17. แบบฝึกหัดท้าย Workshop
- สรุป
- เอกสารอ้างอิง
#บทนำ
เมื่อพัฒนา REST API ด้วย Express.js ปัญหาที่พบได้บ่อยเมื่อนำแอปพลิเคชันขึ้น Docker คือ Image มีไฟล์ที่ไม่จำเป็นต่อการทำงานจริง เช่น ซอร์ส TypeScript, เครื่องมือคอมไพล์, development dependencies และไฟล์ทดสอบ สิ่งเหล่านี้อาจทำให้ Image ใหญ่ขึ้น ส่งต่อช้าลง และเพิ่มพื้นที่ที่ผู้โจมตีอาจใช้ประโยชน์ได้ (attack surface)
Multi-stage Build คือเทคนิคเขียน Dockerfile ให้มี FROM หลายช่วง โดยแต่ละช่วงทำงานคนละหน้าที่ แล้วคัดลอกเฉพาะสิ่งที่จำเป็นมายัง Runtime Image สุดท้าย ตัวอย่างนี้ใช้ Express API เขียนด้วย TypeScript เพื่อให้เห็นขั้นตอน Build จริง ไม่ได้เป็นเพียงการติดตั้งแพ็กเกจซ้ำสองครั้ง
#สิ่งที่จะได้เรียนรู้
- สร้าง Express API ที่มี health endpoint และ REST endpoints
- Compile TypeScript เป็น JavaScript สำหรับ Production
- แยก Dockerfile เป็น
builder,production-deps,runtime - ตั้งค่า
.dockerignore,npm ci, non-root user และHEALTHCHECK - Run ผ่าน Docker Compose และทดสอบด้วย
curl - ตรวจสอบ Image, health status และแนวทางพัฒนาสู่ Production
#แผนการ Workshop (ประมาณ 120 นาที)
| ช่วงเวลา | กิจกรรม | ผลลัพธ์ที่คาดหวัง |
|---|---|---|
| 0–20 นาที | รู้จัก Docker Image, Dockerfile และ Multi-stage Build | อธิบายหน้าที่ของแต่ละ Stage ได้ |
| 20–50 นาที | สร้าง Express REST API ด้วย TypeScript | ทดสอบ API บนเครื่องได้ |
| 50–80 นาที | สร้าง 3-stage Dockerfile และ .dockerignore |
Build Production Image ได้ |
| 80–100 นาที | Run ผ่าน Docker Compose และทดสอบ Healthcheck | เรียก API จาก Host ได้ |
| 100–120 นาที | เปรียบเทียบ Image และเพิ่ม Automated Test | อธิบายข้อดี/ข้อจำกัดของ Multi-stage ได้ |
สำหรับ Windows PowerShell: ตัวอย่าง
curlที่ใช้-Hและ-dเป็นไวยากรณ์แบบ Unix shell หากทดสอบใน PowerShell ให้ใช้curl.exeแทนcurlหรือใช้Invoke-RestMethod; การรันคำสั่งหลายบรรทัดแบบ\ใช้กับ Bash/WSL/Git Bash ไม่ใช่ PowerShell โดยตรง
#1. Multi-stage Build ทำงานอย่างไร
flowchart TD
A[Source Code + package-lock.json] --> B[Stage 1: builder]
A --> C[Stage 2: production-deps]
B --> D[Compile TypeScript เป็น dist]
C --> E[Install runtime dependencies]
D --> F[Stage 3: runtime]
E --> F
F --> G[Express API Container]
| Stage | หน้าที่ | เก็บไว้ใน Image สุดท้ายหรือไม่ |
|---|---|---|
builder |
ติดตั้ง dependency ทั้งหมดและรัน tsc |
คัดลอกเฉพาะ dist/ |
production-deps |
ติดตั้ง node_modules เฉพาะ Production |
คัดลอกเฉพาะ node_modules/ |
runtime |
บรรจุไฟล์ที่ต้องใช้รัน API | ใช่ เป็น Final Image |
Docker รองรับชื่อ Stage ผ่าน FROM ... AS ... และคัดลอก Artifact ด้วย COPY --from=... ทำให้ไม่ต้องนำ build tools ติดไปกับ Image สุดท้าย
#2. เครื่องมือที่ต้องเตรียม
- Node.js 24 และ npm เพื่อใช้พัฒนาและสร้าง
package-lock.json - Docker Desktop หรือ Docker Engine ที่รองรับ Docker Compose v2
- VS Code หรือโปรแกรมแก้ไขโค้ด
- อินเทอร์เน็ตสำหรับดาวน์โหลด npm packages และ Docker base images
หมายเหตุ: Node.js 24 เป็นรุ่น LTS ที่ยังรองรับในช่วงเวลาของบทความ โดยควรเลือกอัปเดตแพตช์และ Base Image ตามนโยบายความปลอดภัยอย่างสม่ำเสมอ
#3. โครงสร้างโครงการ
express-multistage-api/
├── src/
│ ├── app.ts
│ └── server.ts
├── test/
│ └── api.test.mjs
├── .dockerignore
├── .env.example
├── .gitignore
├── compose.yaml
├── Dockerfile
├── package.json
├── package-lock.json # สร้างด้วย npm install ก่อน docker build
└── tsconfig.json
ดาวน์โหลด ZIP ของ Workshop ที่แนบมาพร้อมบทความได้ โดย ไฟล์ ZIP ไม่มี package-lock.json ในตอนแรก ให้รัน npm install หนึ่งครั้งก่อน Build เพื่อสร้างไฟล์ดังกล่าว จากนั้นควร commit ไฟล์ lock ลง Git เพื่อให้ติดตั้ง dependency ได้ทำซ้ำอย่างสม่ำเสมอ
#4. กำหนดแพ็กเกจใน package.json
{
"name": "express-multistage-api",
"version": "1.0.0",
"private": true,
"description": "Express API + TypeScript + Docker multi-stage build workshop",
"main": "dist/server.js",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "npm run build && node --test test/*.test.mjs"
},
"engines": {
"node": ">=24"
},
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/node": "^24.0.0",
"tsx": "^4.20.0",
"typescript": "^5.9.0"
}
}
อธิบายคำสั่งสำคัญ:
npm run devเปิด Development Server ด้วยtsxและ Watch Modenpm run buildคอมไพล์.tsเป็น JavaScript ในdist/npm startรัน JavaScript ที่คอมไพล์แล้วnpm testสร้างไฟล์และทดสอบ REST API ด้วยnode:test
หลังสร้างไฟล์ package.json ให้ติดตั้งแพ็กเกจเพื่อสร้าง Lockfile:
npm install
ในการ Build ภายหลังจะใช้ npm ci ซึ่งอาศัย Lockfile ที่ตรงกับ package.json หากแก้ Dependencies ให้รัน npm install และ commit Lockfile ใหม่ก่อนสร้าง Docker Image
#5. กำหนดค่า TypeScript
สร้างไฟล์ tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}
rootDir ระบุที่เก็บซอร์ส และ outDir ระบุผลลัพธ์ Build ดังนั้น src/server.ts จะถูกคอมไพล์เป็น dist/server.js
#6. สร้าง Express REST API
#6.1 ไฟล์ src/app.ts
import express from "express";
const app = express();
app.disable("x-powered-by");
app.use(express.json({ limit: "10kb" }));
app.get("/health", (_req, res) => {
res.status(200).json({ status: "ok", uptimeSeconds: Math.round(process.uptime()) });
});
app.get("/api/v1/hello", (_req, res) => {
res.json({ message: "Hello from Express Multi-stage Docker!" });
});
app.post("/api/v1/echo", (req, res) => {
res.status(200).json({ received: req.body });
});
app.use((_req, res) => {
res.status(404).json({ error: "Route not found" });
});
export default app;
API ของ Workshop ประกอบด้วย:
| HTTP Method | Endpoint | การทำงาน |
|---|---|---|
| GET | /health |
คืนค่า Status 200 เมื่อ process ตอบสนองได้ |
| GET | /api/v1/hello |
คืนข้อความ JSON |
| POST | /api/v1/echo |
ส่งข้อมูล JSON กลับเพื่อทดลอง API |
#6.2 ไฟล์ src/server.ts
import app from "./app";
const port = Number(process.env.PORT ?? 3000);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("PORT must be an integer between 1 and 65535");
}
const server = app.listen(port, "0.0.0.0", () => {
console.log(`Express API listening on http://0.0.0.0:${port}`);
});
function shutdown(signal: string): void {
console.log(`Received ${signal}; closing HTTP server...`);
const forceExit = setTimeout(() => process.exit(1), 10000);
forceExit.unref();
server.close((error?: Error) => {
clearTimeout(forceExit);
if (error) console.error(error);
process.exit(error ? 1 : 0);
});
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
Server bind ที่ 0.0.0.0 เพื่อรับคำขอจากภายนอก Container ผ่าน Port Mapping พร้อมจัดการ SIGTERM / SIGINT เพื่อปิด HTTP Server อย่างเหมาะสมเมื่อหยุด Container
#6.3 ทดลองรันบนเครื่องโดยไม่ใช้ Docker
npm install
npm run dev
เปิดอีก Terminal แล้วทดสอบ:
curl http://localhost:3000/health
curl http://localhost:3000/api/v1/hello
curl -X POST http://localhost:3000/api/v1/echo \
-H "Content-Type: application/json" \
-d '{"name":"Docker","course":"Express API"}'
ตัวอย่างผลตอบกลับ:
{"received":{"name":"Docker","course":"Express API"}}
#7. เขียน Dockerfile แบบ Multi-stage Build
สร้างไฟล์ Dockerfile ดังนี้:
# syntax=docker/dockerfile:1
# Stage 1: Install build dependencies and compile TypeScript.
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Stage 2: Install only production dependencies.
FROM node:24-alpine AS production-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Stage 3: Copy only the files required for runtime.
FROM node:24-alpine AS runtime
ENV NODE_ENV=production \
PORT=3000
WORKDIR /app
COPY --from=production-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --chown=node:node package.json ./
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + process.env.PORT + '/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "dist/server.js"]
#7.1 Stage builder
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
Stage นี้มีเครื่องมือ typescript สำหรับ Compile แล้วได้ JavaScript ภายใน dist/ เทคนิค Copy Manifest ก่อน Source ช่วยให้ Docker ใช้ Layer Cache ของขั้นตอน Install Dependencies ซ้ำได้เมื่อแก้เฉพาะโค้ด
#7.2 Stage production-deps
FROM node:24-alpine AS production-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
คำสั่ง npm ci --omit=dev ติดตั้ง Dependency สำหรับการรันแอปเท่านั้น เช่น express ไม่รวม TypeScript หรือ tsx ที่กำหนดไว้ใน devDependencies
#7.3 Stage runtime
FROM node:24-alpine AS runtime
ENV NODE_ENV=production \
PORT=3000
WORKDIR /app
COPY --from=production-deps --chown=node:node /app/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/dist ./dist
COPY --chown=node:node package.json ./
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + process.env.PORT + '/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "dist/server.js"]
หลักการสำคัญคือ runtime ไม่ต้องเก็บ src/, tsconfig.json, TypeScript Compiler หรือแพ็กเกจที่ใช้เฉพาะช่วงพัฒนา ขณะที่ USER node ลดการทำงานด้วยสิทธิ์ Root และ HEALTHCHECK ช่วยให้ Docker รายงานสถานะ Healthy/Unhealthy ของ Container ได้
EXPOSEเป็นเพียงการประกาศ Port ที่แอปใช้งาน ยังไม่ได้เผยแพร่ Port ไปยัง Host ต้องระบุ-pหรือports:อีกครั้ง; และHEALTHCHECKไม่ใช่ระบบรีสตาร์ต Container โดยตัวมันเอง
#8. สร้าง .dockerignore
node_modules
npm-debug.log*
*.log
dist
coverage
.git
.github
.env
.env.*
!.env.example
.DS_Store
.vscode
README.md
test
ไฟล์นี้ช่วยป้องกันการส่ง node_modules/, .env, Test Files, Git History และไฟล์ชั่วคราวเข้าไปใน Docker Build Context โดย ไม่ตัด package-lock.json ซึ่งจำเป็นกับ npm ci
#9. สร้าง Docker Image และ Run Container
#9.1 Build แบบปกติ
docker build -t express-multistage-api:1.0 .
#9.2 Run แบบ Docker CLI
docker run -d \
--name express-api \
-p 3000:3000 \
express-multistage-api:1.0
#9.3 ทดสอบจาก Host
curl http://localhost:3000/health
curl http://localhost:3000/api/v1/hello
curl -X POST http://localhost:3000/api/v1/echo \
-H "Content-Type: application/json" \
-d '{"topic":"multi-stage build"}'
ตรวจสอบ Log และหยุด Container:
docker logs -f express-api
docker stop express-api
docker rm express-api
#10. ใช้งานผ่าน Docker Compose
ไฟล์ compose.yaml:
services:
api:
build:
context: .
target: runtime
image: express-multistage-api:latest
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: 3000
init: true
restart: unless-stopped
รันคำสั่ง:
docker compose up --build -d
docker compose ps
docker compose logs -f api
ทดสอบ:
curl http://localhost:3000/health
curl http://localhost:3000/api/v1/hello
หยุด Service:
docker compose down
ค่า init: true ช่วยเรื่องการส่งต่อสัญญาณและเก็บกวาด Child Processes ภายใน Container ส่วน restart: unless-stopped เป็น Policy เมื่อ Container หยุดทำงาน แต่ ไม่ได้รับประกันว่าจะรีสตาร์ตเมื่อ Healthcheck เปลี่ยนเป็น Unhealthy
#11. เพิ่ม Automated API Test
โครงการตัวอย่างมี test/api.test.mjs ซึ่งใช้ Test Runner ที่ติดมากับ Node.js และเรียก API ผ่าน HTTP จริงโดยใช้พอร์ตว่างชั่วคราว:
import test from "node:test";
import assert from "node:assert/strict";
import { createServer } from "node:http";
import appModule from "../dist/app.js";
// TypeScript compile เป็น CommonJS; export default อยู่ที่ .default
const app = appModule.default;
async function withServer(callback) {
const server = createServer(app);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const address = server.address();
assert.ok(address && typeof address !== "string");
await callback(`http://127.0.0.1:${address.port}`);
} finally {
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve()))
);
}
}
test("GET /health returns 200", async () => {
await withServer(async (url) => {
const response = await fetch(`${url}/health`);
assert.equal(response.status, 200);
assert.equal((await response.json()).status, "ok");
});
});
test("GET /api/v1/hello returns a greeting", async () => {
await withServer(async (url) => {
const response = await fetch(`${url}/api/v1/hello`);
assert.equal(response.status, 200);
const body = await response.json();
assert.match(body.message, /Hello from Express/);
});
});
test("POST /api/v1/echo returns submitted JSON", async () => {
await withServer(async (url) => {
const response = await fetch(`${url}/api/v1/echo`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "Docker" })
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { received: { name: "Docker" } });
});
});
รันบน Host:
npm test
ข้อควรระวังเรื่อง Module Interoperability: Workshop นี้ตั้งค่า TypeScript เป็น
"module": "CommonJS"ดังนั้นexport default appที่คอมไพล์แล้วจะอยู่ในmodule.exports.defaultเมื่อไฟล์ทดสอบ.mjsนำเข้า CommonJS Module จะต้องใช้งานappModule.defaultตามตัวอย่าง ไม่เช่นนั้นอาจเกิดTypeErrorเมื่อส่ง object ให้createServer()
หรือรันทดสอบใน Build Stage เพื่อเป็นการตรวจสอบก่อนเผยแพร่ (เหมาะนำไปใส่ใน CI Pipeline):
npm test && docker build -t express-multistage-api:1.0 .
ข้อจำกัด: ตัวอย่าง Dockerfile มุ่งสาธิต Runtime Image จึงยังไม่บังคับรัน Test ภายใน Docker Build อัตโนมัติ การนำขึ้น Production ควรรัน Automated Test ใน CI ก่อน Build/Push Image
#12. ตรวจสอบว่าการทำ Multi-stage มีผลอย่างไร
สร้าง Image สำหรับ Stage builder แล้วเปรียบเทียบกับ Runtime Image:
docker build --target builder -t express-api:builder .
docker build --target runtime -t express-api:runtime .
docker image ls express-api
docker history express-api:runtime
ผลที่ควรสังเกต: Runtime Image จะไม่มี typescript, tsx, TypeScript Source และ Development Dependencies แต่ขนาดที่ต่างกันจริงขึ้นอยู่กับ Node Base Image, Architecture, npm packages และ Layer Sharing ไม่ควรอ้างตัวเลขลดขนาดเป็นเปอร์เซ็นต์ตายตัว
ตรวจสอบ User ภายใน Container:
docker compose up --build -d
docker compose exec api id
ตรวจสอบสถานะ Health:
docker inspect --format='{{json .State.Health}}' "$(docker compose ps -q api)"
#13. เทียบกับ Single-stage Dockerfile
สำหรับผู้เริ่มต้น สามารถทดลองสร้างไฟล์ Dockerfile.single เพื่อดูข้อแตกต่างระหว่าง Docker Image ที่ติดตั้ง Dependency ทุกประเภท กับ Image ที่แยก Stage:
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
ENV NODE_ENV=production PORT=3000
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Build และเปรียบเทียบ:
docker build -f Dockerfile.single -t express-api:single .
docker build -f Dockerfile -t express-api:multi .
docker image ls express-api
เนื่องจาก Dockerfile.single ยังเก็บ node_modules ที่มี Dev Dependencies อยู่ใน Runtime Image จึงมักมีขนาดใหญ่กว่า Multi-stage Image แต่ให้ตรวจสอบขนาดจริงหลัง Build บนเครื่องของตนเอง และจำไว้ว่าการลดขนาด Image ไม่ได้ทดแทนการสแกนช่องโหว่
ทดลองยืนยันว่า Runtime ไม่มี TypeScript Compiler:
docker run --rm --entrypoint node express-api:multi \
-e "try { require.resolve('typescript'); process.exit(1); } catch { console.log('typescript is not installed'); }"
สำหรับแอปที่ใช้ Native Addons หรือ Build Tools เพิ่มเติม ให้ทดสอบว่า Production Dependencies ถูกติดตั้งและใช้งานได้จริงกับ Base Image ของ Stage สุดท้าย
#14. เพิ่ม CI Pipeline ด้วย GitHub Actions (ทางเลือก)
สร้างไฟล์ .github/workflows/docker-ci.yml ใน Repository:
name: Express API CI
on:
push:
pull_request:
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
- name: Install dependencies
run: npm ci
- name: Test
run: npm test
- name: Build production image
run: docker build --target runtime -t express-api:ci .
ตัวอย่างนี้เป็นเพียง Quality Gate ขั้นต้น ไม่ได้ Push Image ไป Registry และ ต้อง Commit package-lock.json ก่อน จึงจะใช้ npm ci ได้ใน CI หากนำไปใช้กับ Production ควรเพิ่ม Dependency/Image Scanning, การตรวจสอบสิทธิ์ และนโยบาย Release ต่อไป
#15. ประเด็นสำหรับ Production
- Repeatable Build: Commit
package-lock.jsonและใช้npm ci; หาก Lockfile ไม่ตรง Manifest ให้แก้ที่ต้นทางก่อน Build - Security: ใช้
USER node, จำกัดสิทธิ์, ไม่ใส่ API Keys ใน Dockerfile, และอัปเดต Dependency สม่ำเสมอ - Image Pinning: Production ควร Pin Node Base Image ด้วย Digest และอัปเดต Digest ตามกระบวนการ Patch Management
- Observability: เพิ่ม Structured Logging, Metrics, Tracing และทำ Health/Readiness Check ที่เหมาะสมกับระบบจริง
- API Security: ตัวอย่าง Echo API ไม่ได้ใส่ Authentication, Authorization, Rate Limiting หรือ Data Validation ระดับ Production
- Deployment: วางระบบหลัง Reverse Proxy/Ingress เพื่อจัดการ HTTPS/TLS, Routing และ Traffic Control
- CI/CD: รัน
npm test, Scan Dependencies/Images และ Publish เฉพาะ Image ที่ผ่าน Quality Gate - Alpine Compatibility: Native Addons บางตัวอาจมีปัญหาจากความต่างระหว่าง musl และ glibc ควรทดสอบ Dependency และพิจารณา Debian-based Image หากจำเป็น
#16. ปัญหาที่พบบ่อย
| ปัญหา | สาเหตุที่เป็นไปได้ | วิธีแก้ |
|---|---|---|
npm ci แจ้งไม่มี Lockfile |
ยังไม่มี package-lock.json |
รัน npm install บน Host ก่อน Build |
npm ci แจ้ง Lockfile ไม่ตรง |
แก้ package.json แต่ไม่ได้อัปเดต Lockfile |
รัน npm install และ Commit อีกครั้ง |
เปิด localhost:3000 ไม่ได้ |
Container ไม่รันหรือ Port Mapping ผิด | ตรวจ docker compose ps, docker compose logs api |
Cannot find module dist/server.js |
Build Stage คอมไพล์ไม่สำเร็จ | ตรวจ npm run build และ COPY --from=builder |
unhealthy |
Health Endpoint ไม่ตอบหรือ PORT ไม่ตรง | ตรวจ Log และ PORT Environment Variable |
port is already allocated |
Port 3000 บน Host ถูกใช้งาน | เปลี่ยน Mapping เช่น 3001:3000 |
#17. แบบฝึกหัดท้าย Workshop
- ปรับ Config: เปลี่ยน Host Port จาก
3000เป็น3001ผ่านcompose.yamlแล้วทดสอบGET /healthโดยไม่แก้PORTภายใน Container - เพิ่ม API: สร้าง
GET /api/v1/versionให้คืนname,versionและenvironmentพร้อม Automated Test ใหม่ - เปรียบเทียบ Image: วัดขนาดของ
express-api:singleและexpress-api:multiแล้วอธิบายว่า Dependency กลุ่มใดถูกตัดออกไป - ทดสอบ Deployment: หยุด Container ด้วย
docker compose stop apiและตรวจสอบการปิด HTTP Server ผ่าน Logs - เพิ่ม Quality Gate: นำไฟล์ GitHub Actions จากหัวข้อก่อนหน้าไปใช้ และตรวจสอบว่า Workflow ล้มเหลวเมื่อ Automated Test ไม่ผ่าน
#สรุป
การใช้ Docker Multi-stage Build กับ Express API ช่วยแยกขั้นตอน Compile, Dependency Installation และ Runtime อย่างเป็นระบบ Final Image จึงมีเฉพาะ JavaScript ที่ Build แล้วกับ Production Dependencies แนวทางนี้เหมาะต่อการสร้าง CI/CD Pipeline และเป็นพื้นฐานสำคัญของการ Deploy Node.js API ในงานจริง
ต่อยอด Workshop: เพิ่ม PostgreSQL, Redis, Jest/Vitest, GitHub Actions, Trivy, Docker Registry และ Kubernetes Deployment พร้อม Readiness/Liveness Probe
#เอกสารอ้างอิง
- Docker Docs: Multi-stage builds
- Docker Docs: Node.js language-specific guide
- Docker Docs: Dockerfile reference
- Node.js Release Working Group: Release schedule
- npm Docs: npm ci
- Express.js: Official documentation
