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

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

สวัสดีครับ ผมได้กลับมาเขียน 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 ก่อน

blog9-1.webp

Structure ของ Nuxt3

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

blog9-2.webp

.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 #เลือก Distro ที่เราต้องการจะนำมาเป็น Base สำหรับการ BuildCOPY package.json bun.lock ./app/ #คัดลอกไฟล์ package.json และ bun.lock ซึ่งเป็นไฟล์ระบุ Package ที่ใช้ภายในระบบWORKDIR /app #กำหนดพื้นที่หรือขอบเขตในการ ExecuteRUN bun install --frozen-lockfile #ติดตั้งไฟล์ที่จำเป็นสำหรับการ Build NuxtFROM base AS prerelease #เปลี่ยน Step การทำงานENV USER_NAME=isaac #กำหนด UsernameENV UID=1001 #กำหนด User IDENV GID=1001 #กำหนด Group ID#เริ่มติดตั้ง Package ที่จำเป็นต่อการใช้งานRUN apk update && \ apk add --no-cache ca-certificates && \ addgroup -g ${GID} ${USER_NAME} && \ #เพิ่ม Group ที่ User ID จะอยู่ adduser -D -u ${UID} -G ${USER_NAME} ${USER_NAME} && \ # เพิ่ม User เข้า Group chown -R ${USER_NAME}:${USER_NAME} /app #ให้สิทธิ์ในโฟลเดอร์ /app ทั้งหมดแก่ User ดังกล่าวUSER ${USER_NAME} #สลับ UserCOPY --from=base /app/node_modules node_modules #Copy File node_modules จาก StepCOPY . . #Copy file ทั้งหมดที่จำเป็นสำหรับการ Build (ยกเว้น node_modules)ENV NODE_ENV=production #กำหนด ENV เป็น NODE_ENV=production เพื่อ build แบบ ProductionRUN bun run build #คำสั่ง BuildFROM scratch #เปลี่ยน Step มาเป็น Step สุดท้าย คือการ สลับเป็น ScratchCOPY --from=prerelease /etc/passwd /etc/passwd #คัดลอกไฟล์ที่จำเป็นจากสเต็บ Pre-release อันนี้เป็นไฟล์ /etc/passwd คือไฟล์ที่เกี่ยวข้องกับการจัดการ UserCOPY --from=prerelease /etc/group /etc/group #คัดลอกไฟล์ที่จำเป็นจากสเต็บ Pre-release อันนี้เป็นไฟล์ /etc/group คือไฟล์ที่เกี่ยวข้องกับการจัดการ User เช่นเดียวกันENV USER_NAME=isaac #กำหนด UsernameUSER ${USER_NAME} #สลับ UserWORKDIR /app #กำหนดพื้นที่หรือขอบเขตในการ ExecuteCOPY --from=base /usr/local/bin/bun /usr/local/bin/bun #คัดลอกไฟล์ที่จำเป็นจาก Step Base อันนี้เป็น Executable ที่จำเป็นต่อการรัน App ของเรา คือ bunCOPY --from=base "/usr/lib/libstdc++.so.6" "/usr/lib/libstdc++.so.6" #คัดลอก Library ที่จำเป็นสำหรับภาพรวมCOPY --from=base /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 #คัดลอก Library ที่จำเป็นสำหรับภาพรวม# คัดลอก Library ที่จำเป็นสำหรับภาพรวม# (Library นี้ ขาดแล้ว รันไม่ได้ คือ Library ที่เกี่ยวกับ ld เป็น dynamic linker# ถ้าก๊อปปี้ผิด ก็รันไม่ได้้เช่นกัน เช่น ถ้าเครื่องปลายทางใช้ AMD64 / Intel ควรเป็น ld-musl-x86_64.so.1# หรือถ้าเป็น Apple Silicon / ARM64 ควรเป็น ld-musl-aarch64.so.1COPY --from=base /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1COPY --from=prerelease --chown=${USER_NAME}:${USER_NAME} /app/.output /app #คัดลอกโฟลเดอร์ Output สำหรับ Application โดย ต้องกำหนด User สำหรับการ Copy ให้ตรงกับ User ของเราที่กำหนดไว้ในขั้น Pre-releaseENV NUXT_HOST=0.0.0.0 #กำหนด EnvironmentENV NUXT_PORT=3000 #กำหนด EnvironmentEXPOSE 3000 #กำหนด Port ที่จะนำออก สำหรับ ContainerCMD ["bun", "run","server/index.mjs"] #กำหนด คำสั่ง ในการรัน Application

ที่ท่านเห็นอยู่ข้างบนนี้คือ Dockerfile ของโปรเจกต์ ผมเองนะครับ

แต่ละอัน ผมจะคอมเมนต์ และ อธิบายไว้ในไฟล์นะครับ

สังเกตไหมครับว่า Dockerfile ดังกล่าว ผมใช้ scratch สาเหตุที่ผมใช้มีอยู่อย่างเดียวเลย …. ป้องกัน Shell Execution ครับ

ค่อนข้างจะละเอียดเลยครับ เป็นไฟล์ที่ผมใช้เวลาศึกษาพอสมควร กว่าจะได้ Dockerfile นี้มา อาจจะรู้สึกว่ามันยุ่บยั่บไปหน่อย ไหนจะเรื่อง Lib ต่างๆที่ Copy มาเอง ไหนจะเป็น ld ที่จะต้องมาเลือก Architecture ภายหลัง วุ่นวายดีครับ

แต่ผมจะบอกอย่างนึงนะครับ ที่ผมเลือกใช้ scratch แทน distroless เพราะ มันค่อนข้างเห็นชัดดีครับว่า อะไรที่เราจำเป็นต้อง Copy บ้าง และ distroless อาจจะไม่ตอบโจทย์ผมในเรื่องของการใช้งาน Alpine linux ครับ เพราะมันเป็น glibc แต่ Alpine Linux เป็น musl

ไม่ว่าอย่างไรก็ดีครับ การเขียน Dockerfile ในแบบฉบับของผมเอง อาจไม่ใช่วิธีทางที่ดีหรือดูน่าชื่นชมสำหรับพี่ๆน้องๆทุกท่านนะครับ แต่ผมอยากมาแชร์วิธีการเขียนในแนวทางของผม ที่ผมได้ศึกษามา และ ได้มองเห็นจุดอ่อน และ ได้แก้จุดอ่อนในแบบฉบับของผมเพียงเท่านั้นเองครับ ผมยินดีรับความคิดเห็นที่สร้างสรรค์จากพี่ๆ ในวงการ Developer / DevOps / Security ทุกท่านอยู่นะครับ หวังว่าพี่ๆจะใจดีกับผมนะครับ

ขอพระเจ้าอวยพรทุกท่านครับ สวัสดีครับ


บทความที่เกี่ยวข้อง