การเขียน Dockerfile ให้ “รันได้” นั้นง่าย แต่การเขียนให้ image เล็ก ปลอดภัย และ build ซ้ำได้เหมือนเดิมทุกครั้ง คือคนละเรื่องกัน บทความนี้ถอดบทเรียนจาก repo inetspa/dockerfile-templates ที่รวบรวม Dockerfile เกรด production ไว้ครบ 5 stack ให้หยิบไปใช้ได้ทันที
NOTE
repo นี้เป็น reference / boilerplate ภายใต้ลิขสิทธิ์ MIT — clone ไปปรับใช้กับโปรเจกต์ของคุณได้เลย แต่ละ stack มีทั้ง Dockerfile, docker-compose.yml, โค้ดตัวอย่าง และ AGENTS.md อธิบายเฉพาะ stack นั้น ๆ
5 Stack ที่ครอบคลุม
| # | Stack | Base Image (runtime) | Port | Runtime |
|---|---|---|---|---|
| 01 | Python 3.12 + Flask | python:3.12-slim | 5000 | gunicorn |
| 02 | Python 3.12 + FastAPI | python:3.12-slim | 8000 | uvicorn |
| 03 | Next.js 16 | node:22-alpine | 3000 | node (standalone) |
| 04 | React 19 (Vite) | nginx:1.27-alpine | 8080 | nginx |
| 05 | Go 1.26 | gcr.io/distroless/static | 8080 | static binary |
ต่างภาษา ต่าง runtime แต่ทุกตัวยึด หลักการเดียวกัน ซึ่งคือส่วนที่มีค่าที่สุดของ repo นี้
7 หลักการที่ใช้ร่วมกันทุก Stack
- Multi-stage build เสมอ — แยก stage “build” (มี toolchain) ออกจาก “runtime” (เหลือเฉพาะของจำเป็น) → image เล็กลงและ attack surface น้อยลง
- Layer cache optimization — copy เฉพาะ manifest (
requirements.txt/package.json/go.mod) ก่อนติดตั้ง dependencies แล้วค่อย copy source ทีหลัง → แก้โค้ดไม่ต้องลง dependencies ใหม่ - Non-root user — image สุดท้ายรันด้วย user ธรรมดา (
appuser,nextjs,nginx,nonroot) ไม่ใช่ root - Health check — ทุก image มีวิธีเช็คสุขภาพ ไม่ว่าจะผ่าน
HEALTHCHECKหรือผ่าน compose - Pin versions — ใช้ tag แบบ
major.minor(เช่นpython:3.12-slim) ไม่ใช้:latestที่เปลี่ยนใต้เท้าได้ - Build args + .dockerignore —
BUILDKIT_INLINE_CACHE=1,--no-cache-dir,--no-install-recommendsและ.dockerignoreทุกโฟลเดอร์ - Timezone
Asia/Bangkok— ทุก image ตั้งTZและติดตั้ง zoneinfo ให้ตรงกัน (วิธีต่างกันตาม base image — ดูหัวข้อด้านล่าง)
TIP
ถ้าจะจำหลักการเดียวจาก repo นี้ ให้จำข้อ 2: copy manifest ก่อน copy โค้ด เป็นเทคนิคที่ทำให้ rebuild เร็วขึ้นหลายเท่าโดยแทบไม่มีต้นทุน
ไฮไลต์รายตัว
🐍 01–02 · Python (Flask & FastAPI)
ใช้ builder stage ติดตั้ง dependencies แบบ pip install --user แล้ว copy เฉพาะโฟลเดอร์ /root/.local ไป runtime stage — ได้ packages โดยไม่ต้องลากเครื่องมือ build ติดไปด้วย
# builder
RUN pip install --no-cache-dir --user -r requirements.txt
# runtime
COPY --from=builder /root/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH
Health check ฉลาดตรงที่ใช้ Python stdlib (urllib.request) ไม่ต้องลง curl เพิ่มใน image:
HEALTHCHECK CMD python -c "import urllib.request,sys; \
sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:5000/health').status==200 else 1)"
Flask รันด้วย gunicorn (มี workers/timeout) ส่วน FastAPI รันด้วย uvicorn พร้อม --proxy-headers รองรับการอยู่หลัง reverse proxy
▲ 03 · Next.js 16 — 3 stages + standalone
แยกเป็น 3 stage (deps → builder → runner) และใช้ output: "standalone" ของ Next.js ที่ tree-shake node_modules ให้เหลือเฉพาะที่ใช้จริง runtime stage จึง copy แค่ 3 อย่าง:
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static/ ./.next/static/
COPY --from=builder /app/public/ ./public/
CMD ["node", "server.js"]
IMPORTANT
รายละเอียดเล็ก ๆ ที่คนมักพลาด: base image node:22-alpine มี user node ครอง UID 1000 อยู่แล้ว repo จึงสร้าง user nextjs ด้วย UID 1001 เพื่อเลี่ยงการชนกัน
⚛️ 04 · React (Vite) — build แล้วโยนให้ nginx
Vite build ออกมาเป็น static แล้วเสิร์ฟด้วย nginx:1.27-alpine จุดน่าสนใจคือใช้ port 8080 (unprivileged) แทน 80 เพื่อให้ nginx รันเป็น non-root user ได้:
# nginx user (UID 101) เปิด port < 1024 ไม่ได้ จึงใช้ 8080
EXPOSE 8080
# run: docker run -p 8080:8080 react-demo
🐹 05 · Go — distroless ที่เล็กและปลอดภัยที่สุด
compile เป็น static binary (CGO_ENABLED=0) แล้ววางบน gcr.io/distroless/static-debian12:nonroot ที่ ไม่มี shell, ไม่มี package manager เหลือ attack surface น้อยที่สุด
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o /out/server .
# runtime: distroless nonroot (UID 65532) — ไม่ต้องสร้าง user เอง
มีไฟล์ Dockerfile.alpine เป็นทางเลือกสำหรับคนที่อยากได้ shell + HEALTHCHECK แบบ wget
เกร็ดเด็ด: จัดการ Timezone ตาม Base Image
นี่คือจุดที่ repo ทำได้ละเอียดมาก เพราะแต่ละ base image จัดการ zoneinfo ไม่เหมือนกัน:
| Base Image | วิธีตั้ง Asia/Bangkok |
|---|---|
| debian-slim (Flask/FastAPI) | apt-get install tzdata แล้ว symlink /etc/localtime |
| alpine (Next.js/React) | apk add --no-cache tzdata แล้ว symlink |
| distroless (Go) | ไม่มี zoneinfo ติดมา → COPY เฉพาะไฟล์ Asia/Bangkok จาก builder stage |
WARNING
distroless ไม่มี shell จึงใช้ HEALTHCHECK แบบ shell command ไม่ได้ — repo แก้ด้วยการย้าย healthcheck ไปไว้ใน docker-compose.yml แทน (เรียก binary ด้วย flag -healthcheck)
เริ่มใช้งานอย่างรวดเร็ว
git clone https://github.com/inetspa/dockerfile-templates
cd dockerfile-templates
# ตัวอย่าง: Python + Flask
cd 01-python-flask
docker build -t flask-demo .
docker run --rm -p 5000:5000 flask-demo
# ตัวอย่าง: Go (distroless)
cd ../05-go
docker build -t go-demo .
docker run --rm -p 8080:8080 go-demo
แต่ละโฟลเดอร์มี docker compose up --build ให้ใช้สำหรับ dev ได้เช่นกัน
💡 สรุปสิ่งที่ถอดมาใช้ได้ทันที
- Multi-stage + copy manifest ก่อน — image เล็กลงและ build เร็วขึ้น
- Non-root ทุก image — ลดความเสี่ยงเมื่อถูกเจาะ
- เลือก base ให้เหมาะกับงาน —
slimสำหรับ Python,alpineสำหรับ static serve,distrolessเมื่อต้องการความปลอดภัยสูงสุด - อย่าลืม timezone & health check — รายละเอียดเล็ก ๆ ที่แยก “ใช้ได้” กับ “ใช้ได้จริงบน production”
อยากเข้าใจลึกขึ้น แนะนำให้อ่าน AGENTS.md ในแต่ละโฟลเดอร์ของ repo ครับ — เขียนอธิบายเหตุผลเบื้องหลังทุกบรรทัดไว้อย่างดี
📚 แหล่งอ้างอิง (Source)
บทความนี้เรียบเรียงจาก source code และเอกสารใน repository ต่อไปนี้:
- Repository: inetspa/dockerfile-templates
- เจ้าของ: INET (inetspa)
- License: MIT
- ไฟล์ที่อ้างอิง:
README.md,AGENTS.md(root + รายโฟลเดอร์), และDockerfileของทั้ง 5 stack (01-python-flask,02-python-fastapi,03-nextjs,04-react,05-go)
NOTE
โค้ดตัวอย่างทั้งหมดในบทความนี้คัดมาจาก repo ต้นทางโดยตรง ลิขสิทธิ์เป็นของผู้พัฒนาเดิมภายใต้สัญญาอนุญาต MIT