#บทนำ

เมื่อพัฒนา REST API ด้วย Express.js ปัญหาที่พบได้บ่อยเมื่อนำแอปพลิเคชันขึ้น Docker คือ Image มีไฟล์ที่ไม่จำเป็นต่อการทำงานจริง เช่น ซอร์ส TypeScript, เครื่องมือคอมไพล์, development dependencies และไฟล์ทดสอบ สิ่งเหล่านี้อาจทำให้ Image ใหญ่ขึ้น ส่งต่อช้าลง และเพิ่มพื้นที่ที่ผู้โจมตีอาจใช้ประโยชน์ได้ (attack surface)

Multi-stage Build คือเทคนิคเขียน Dockerfile ให้มี FROM หลายช่วง โดยแต่ละช่วงทำงานคนละหน้าที่ แล้วคัดลอกเฉพาะสิ่งที่จำเป็นมายัง Runtime Image สุดท้าย ตัวอย่างนี้ใช้ Express API เขียนด้วย TypeScript เพื่อให้เห็นขั้นตอน Build จริง ไม่ได้เป็นเพียงการติดตั้งแพ็กเกจซ้ำสองครั้ง

#สิ่งที่จะได้เรียนรู้

  1. สร้าง Express API ที่มี health endpoint และ REST endpoints
  2. Compile TypeScript เป็น JavaScript สำหรับ Production
  3. แยก Dockerfile เป็น builder, production-deps, runtime
  4. ตั้งค่า .dockerignore, npm ci, non-root user และ HEALTHCHECK
  5. Run ผ่าน Docker Compose และทดสอบด้วย curl
  6. ตรวจสอบ 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 Mode
  • npm 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

  1. Repeatable Build: Commit package-lock.json และใช้ npm ci; หาก Lockfile ไม่ตรง Manifest ให้แก้ที่ต้นทางก่อน Build
  2. Security: ใช้ USER node, จำกัดสิทธิ์, ไม่ใส่ API Keys ใน Dockerfile, และอัปเดต Dependency สม่ำเสมอ
  3. Image Pinning: Production ควร Pin Node Base Image ด้วย Digest และอัปเดต Digest ตามกระบวนการ Patch Management
  4. Observability: เพิ่ม Structured Logging, Metrics, Tracing และทำ Health/Readiness Check ที่เหมาะสมกับระบบจริง
  5. API Security: ตัวอย่าง Echo API ไม่ได้ใส่ Authentication, Authorization, Rate Limiting หรือ Data Validation ระดับ Production
  6. Deployment: วางระบบหลัง Reverse Proxy/Ingress เพื่อจัดการ HTTPS/TLS, Routing และ Traffic Control
  7. CI/CD: รัน npm test, Scan Dependencies/Images และ Publish เฉพาะ Image ที่ผ่าน Quality Gate
  8. 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

  1. ปรับ Config: เปลี่ยน Host Port จาก 3000 เป็น 3001 ผ่าน compose.yaml แล้วทดสอบ GET /health โดยไม่แก้ PORT ภายใน Container
  2. เพิ่ม API: สร้าง GET /api/v1/version ให้คืน name, version และ environment พร้อม Automated Test ใหม่
  3. เปรียบเทียบ Image: วัดขนาดของ express-api:single และ express-api:multi แล้วอธิบายว่า Dependency กลุ่มใดถูกตัดออกไป
  4. ทดสอบ Deployment: หยุด Container ด้วย docker compose stop api และตรวจสอบการปิด HTTP Server ผ่าน Logs
  5. เพิ่ม 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

#เอกสารอ้างอิง

info=dockerfile-multi-stage-build-express-api-workshop-production