Optimization & Security : การเขียน Dockerfile ของผมเอง by AP.xyz

- ผู้เขียนใช้ Nuxt3 + Bun.js บน Alpine Linux เป็น Stack หลัก
 - เริ่มจากการจัดการ   
.dockerignoreเพื่อไม่ให้ไฟล์ไม่จำเป็นหรือข้อมูลสำคัญถูกนำเข้าไปใน Container - ใช้ Multi-stage build เพื่อแยกขั้นตอนการ Build และลดขนาดของ Image
 - ขั้นตอนสุดท้ายใช้ Scratch image แทน Distroless เพื่อควบคุมสิ่งที่ถูก Copy เข้าไปอย่างละเอียด
 - มีการจัดการ User/Group ภายใน Container เพื่อเพิ่มความปลอดภัย
 - คัดลอกเฉพาะ Library และ Executable ที่จำเป็น เช่น 
Bun,libstd++,libgcc_sและld-musl - เหตุผลที่ไม่ใช้ Distroless เพราะไม่รองรับ musl libc ซึ่ง Alpine ใช
 - ผู้เขียนเน้นว่าแนวทางนี้อาจไม่ดีที่สุด แต่เป็นสิ่งที่เขาเรียนรู้และปรับปรุงมาเอง
 
---
สวัสดีครับ ผมได้กลับมาเขียน Blog อีกครั้งแล้วนะครับ หลังจากได้ไปท่องโลกสนธยา โลก DevOps มา ไม่ได้มีเวลากลับมาเขียน Blog เลย คิดถึงผู้อ่านทุกท่านนะครับ และผมหวังอย่างยิ่งว่า บทความนี้ อาจจะเป็นประโยชน์ต่อผู้อ่าน หรือ ผู้ที่ต้องการศึกษา Dockerfile ครับ
Dockerfile ที่ดี ต้องมีอะไรบ้าง หลายคนคงทราบอยู่แล้วนะครับว่า Dockerfile ที่ดี ต้องรู้จักการ Copy ของที่จำเป็นต่อการรันแอปพลิเคชั่น และ ไม่ขาดสิ่งที่จำเป็นไป โดยในการทำ Dockerfile นั้นสิ่งที่ต้องห้ามเลย คือ Copy Credentials / Environment หรือ Sensitive Object ต่างๆนะครับ เอาขึ้นไป ผมขอตีมือทีนึงนะ 5555555555
เอาจริงๆ ผมเองก็มาเริ่ม Nerd กับ Dockerfile ในช่วงหลังๆชีวิตการทำงานตลอด 4 ปีที่ผ่านมานะครับ สมัยก่อน มีอะไรก็ก๊อปๆใส่ไปก่อน เอาขึ้นให้ได้ ช่วงต่อมา ก็เริ่มรู้แล้วว่า อะไรที่ไม่ควร Copy ใส่ และ มาถึง Step Build Multi-stage และ ก็มาถึงอีกสเต็ป คือ การเอา Dockerfile ยัดเขา Distroless ยัน Scratch Dockerfile ครับ
ในบทความนี้ผมจะมาพาทำ Dockerfile แบบเบาสบาย ปลอดภัย และ คลีน ฉบับผมเองนะครับ โดยผมจะเริ่มจากการที่ผมนำโปรเจกต์ Website ส่วนตัวของผม มาเป็นต้บแบบในบทความนี้นะครับ
Stack ที่ผมนำไป Containerized มีดังนี้ครับ
Code base : Nuxt3 (Vue 3) + Bun.js
OS base : Alpine Linux
มาเริ่มกันเลยดีกว่าครับ ผมจะพาทัวร์ โดยเริ่มจากการดูโครงสร้าง Folder ก่อน

Structure ของ Nuxt3
ทุกคนสังเกตเห็น .dockerignore ไหมครับ อันนี้แหละครับที่เป็นตัวช่วยทุกท่านในการ ไม่เอา File ที่เราไม่ต้องการ รวมถึง Sensitive Object ขึ้นไป เวลา Build Dockerfile ครับ

.dockerignore
ในไฟล์นี้ผมจะไม่เอาสิ่งที่เป็น Output เวลาที่ผม Build บน Local ขึ้นไป หรือ ไฟล์ต่างๆที่ไม่จำเป็น หรือ เกินจำเป็น เวลา Build ครับ เช่น node_modules, .output, .nuxt, .git หรือว่าพวก Sensitive Object เช่น .env หรือสิ่งที่ Editor เป็นคน Generate ให้ เช่น .vscode, .idea หรือ อื่นๆครับ และ ถ้า เป็นพวก Script ต่างๆที่ใช้งานเกี่ยวกับ Deployment ผมก็ไม่เอาขึ้นไปเช่นกันครับ เช่น ที่เก็บ File Helm Chart , YAML ของ K8s
มาถึงส่วนที่สำคัญกันบ้างล่ะครับ คือ Dockerfile
FROM oven/bun:alpine as base  # Choose the distro we’ll use as the build base
# Copy only what’s needed for dependency install
COPY package.json bun.lock ./app/
WORKDIR /app  # Set the working directory
RUN bun install --frozen-lockfile  # Install deps for building Nuxt
# ---------- Build stage ----------
FROM base AS prerelease
# Create a dedicated, non-root user
ENV USER_NAME=isaac
ENV UID=1001
ENV GID=1001
# Install minimal packages and create user/group
RUN apk update && \
    apk add --no-cache ca-certificates && \
    addgroup -g ${GID} ${USER_NAME} && \  # Create group
    adduser -D -u ${UID} -G ${USER_NAME} ${USER_NAME} && \  # Create user and add to group
    chown -R ${USER_NAME}:${USER_NAME} /app  # Own /app
USER ${USER_NAME}  # Drop privileges
# Reuse node_modules from the base stage
COPY --from=base /app/node_modules node_modules
# Copy the rest of the app (excluding items filtered by .dockerignore)
COPY . .
ENV NODE_ENV=production  # Build in production mode
RUN bun run build  # Build Nuxt (.output)
# ---------- Final minimal runtime ----------
FROM scratch  # Ultra-minimal image with only what we copy in
# Copy required user/group files so the non-root user works at runtime
COPY --from=prerelease /etc/passwd /etc/passwd
COPY --from=prerelease /etc/group /etc/group
ENV USER_NAME=isaac
USER ${USER_NAME}
WORKDIR /app
# Copy Bun runtime and essential runtime libs
COPY --from=base /usr/local/bin/bun /usr/local/bin/bun
COPY --from=base "/usr/lib/libstdc++.so.6" "/usr/lib/libstdc++.so.6"
COPY --from=base /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
# Dynamic linker (must match architecture):
#   - x86_64: /lib/ld-musl-x86_64.so.1
#   - arm64:  /lib/ld-musl-aarch64.so.1
COPY --from=base /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
# Copy built output (ensure ownership matches the runtime user)
COPY --from=prerelease --chown=${USER_NAME}:${USER_NAME} /app/.output /app
ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=3000
EXPOSE 300
CMD ["bun", "run", "server/index.mjs"]
ที่ท่านเห็นอยู่ข้างบนนี้คือ Dockerfile ของโปรเจกต์ ผมเองนะครับ
แต่ละอัน ผมจะคอมเมนต์ และ อธิบายไว้ในไฟล์นะครับ
สังเกตไหมครับว่า Dockerfile ดังกล่าว ผมใช้ scratch สาเหตุที่ผมใช้มีอยู่อย่างเดียวเลย …. ป้องกัน Shell Execution ครับ
ค่อนข้างจะละเอียดเลยครับ เป็นไฟล์ที่ผมใช้เวลาศึกษาพอสมควร กว่าจะได้ Dockerfile นี้มา อาจจะรู้สึกว่ามันยุ่บยั่บไปหน่อย ไหนจะเรื่อง Lib ต่างๆที่ Copy มาเอง ไหนจะเป็น ld ที่จะต้องมาเลือก Architecture ภายหลัง วุ่นวายดีครับ
แต่ผมจะบอกอย่างนึงนะครับ ที่ผมเลือกใช้ scratch แทน distroless เพราะ มันค่อนข้างเห็นชัดดีครับว่า อะไรที่เราจำเป็นต้อง Copy บ้าง และ distroless อาจจะไม่ตอบโจทย์ผมในเรื่องของการใช้งาน Alpine linux ครับ เพราะมันเป็น glibc แต่ Alpine Linux เป็น musl
ไม่ว่าอย่างไรก็ดีครับ การเขียน Dockerfile ในแบบฉบับของผมเอง อาจไม่ใช่วิธีทางที่ดีหรือดูน่าชื่นชมสำหรับพี่ๆน้องๆทุกท่านนะครับ แต่ผมอยากมาแชร์วิธีการเขียนในแนวทางของผม ที่ผมได้ศึกษามา และ ได้มองเห็นจุดอ่อน และ ได้แก้จุดอ่อนในแบบฉบับของผมเพียงเท่านั้นเองครับ ผมยินดีรับความคิดเห็นที่สร้างสรรค์จากพี่ๆ ในวงการ Developer / DevOps / Security ทุกท่านอยู่นะครับ หวังว่าพี่ๆจะใจดีกับผมนะครับ
ขอพระเจ้าอวยพรทุกท่านครับ สวัสดีครับ

