เราทุกคนก็รัก Docker ใช่ไหมครับ? มันเหมือนเวทมนตร์ที่ช่วยแพ็กแอปพลิเคชันและ dependency ทั้งหลายของเราให้อยู่ในห่อเดียว ทำให้ปัญหาคลาสสิกอย่าง "เครื่องผมรันได้นะ" กลายเป็นเรื่องในอดีตไปเลย แต่รู้ไหม? Dockerfile ที่เราเขียนๆ กันอยู่น่ะ มันช่วยเราจริงๆ หรือว่ามันกำลังแอบสร้าง Image ที่ทั้งใหญ่ เทอะทะ กินทรัพยากร แถมยังอาจจะไม่ปลอดภัยอยู่หรือเปล่า?

จริงอยู่ที่แค่โยนคำสั่งไม่กี่อย่างเข้าไปใน Dockerfile มันก็พอให้แอปฯ เรา รันได้ แล้วล่ะครับ แต่การจะปั้น Dockerfile ให้ออกมาเฉียบคม ทั้งเล็ก ทั้งเร็ว และพร้อมสำหรับโปรดักชันจริงๆ เนี่ย มันต้องใช้ศิลปะกันหน่อย ถ้าคุณเคยนั่งรอการสร้าง Image ที่นานจนเมื่อยนิ้ว หรือเคยเห็นขนาด Image สุดท้ายแล้วอยากจะลมจับ แสดงว่าคุณมาถูกที่แล้วครับ! เพราะวันนี้เราจะมาเจาะลึกเคล็ดลับเด็ดๆ ที่ผ่านการใช้งานจริงมาแล้ว ที่จะช่วยอัปเกรด Dockerfile ของคุณจากแค่ "พอใช้ได้" ให้กลายเป็น "โคตรเทพ!" เพื่อให้ชีวิตการใช้ Container ของเราง่ายขึ้นอีกเยอะเลยครับ

สุดยอดเคล็ดลับ Dockerfile จากแค่ "ดี" สู่ "ยอดเยี่ยม"!

มาลุยกันเลยกับ Tips ที่จะช่วยให้เราสร้าง Docker Image ได้ดีขึ้นกว่าเดิมครับ

1. สร้างบนความเชื่อมั่น: ทำไม Official Images ถึงเป็นเพื่อนซี้ที่ดีที่สุด

เคยกังวลไหมครับว่า base image ที่เราเอามาใช้ใน Docker container เนี่ย มันจะเป็นระเบิดเวลาที่เต็มไปด้วยซอฟต์แวร์เก่าๆ หรือช่องโหว่ด้านความปลอดภัย? เราก็อยากให้แอปพลิเคชันของเรามันยืนอยู่บนฐานที่มั่นคงและปลอดภัยใช่ไหมล่ะครับ? นี่แหละครับคือเหตุผลว่าทำไมการเลือกใช้ Official Docker Images ถึงควรเป็นสิ่งแรกที่เรานึกถึง โดยทั่วไปแล้ว Image เหล่านี้จะถูกดูแลโดยผู้พัฒนาซอฟต์แวร์นั้นๆ เอง หรือโดยคอมมูนิตี้ที่แข็งแกร่ง ซึ่งหมายความว่ามันมักจะอัปเดตอยู่เสมอ ได้รับการปรับแต่งมาเพื่อประสิทธิภาพและขนาดที่ดีที่สุด และยังเป็นไปตามหลักปฏิบัติที่ดีด้านความปลอดภัยด้วย แถมส่วนใหญ่ยังมีคอมมูนิตี้คอยซัพพอร์ตอีกต่างหาก

2. ยิ่งน้อยยิ่งดี: ศิลปะแห่งการจำกัด Image Layers

เคยรู้สึกไหมครับว่า Docker build ของเรามันช้าเหลือเกิน หรือ Image สุดท้ายมันใหญ่เบิ้มแบบไม่ทราบสาเหตุ? บ่อยครั้งเลยครับที่ตัวการก็คือการมี Layer มากเกินไปนั่นเอง ทุกๆ คำสั่ง RUN ใน Dockerfile ของเรามักจะสร้าง Layer ใหม่เพิ่มขึ้นมา และการมี Layer เยอะเกินไปก็หมายถึง Build ที่ช้าลง และ Image ที่ใหญ่ขึ้น เพราะแต่ละ Layer ก็เพิ่มขนาดให้กับ Image โดยรวม

เคล็ดลับก็คือ พยายามลดจำนวน Layer เหล่านั้นลง ครับ

แบบนี้สิดี:

RUN apt-get update && apt-get install -y \
    curl wget \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

แบบนี้ไม่ค่อยเวิร์ค:

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get clean
Tip: ใช้ && เพื่อเชื่อมคำสั่งหลายๆ คำสั่งให้อยู่ใน RUN บล็อกเดียว

โบนัส: ลำดับก็สำคัญนะ! วางคำสั่งที่เปลี่ยนแปลงน้อยไว้ด้านบนๆ ของ Dockerfile

3. เลิกใช้แท็ก latest: ความแน่นอนคือสิ่งสำคัญ

ลองนึกภาพตามนะครับ เมื่อวาน Build ของเรายังทำงานได้สมบูรณ์แบบ แต่วันนี้มันดันพัง ทั้งๆ ที่เรายังไม่ได้แก้โค้ดแม้แต่บรรทัดเดียว เกิดอะไรขึ้น? แท็ก latest บน Docker Image พื้นฐานของเราอาจจะเป็นตัวการลับๆ ก็ได้ครับ การใช้ latest หมายความว่าเรากำลังดึงเวอร์ชันล่าสุดที่ผู้ดูแลเพิ่งจะ push ขึ้นไป ซึ่งอาจจะมีการเปลี่ยนแปลงที่ทำให้เข้ากันไม่ได้ (breaking changes) หรือมีพฤติกรรมที่ไม่คาดคิดเกิดขึ้นโดยไม่มีการแจ้งเตือน เพื่อความสบายใจและความสม่ำเสมอในการ Build นี่คือความเสี่ยงที่เราไม่ควรจะไปเล่นด้วยเลยครับ

ดังนั้น ควรระบุเวอร์ชันของ base image ด้วยแท็กที่เฉพาะเจาะจงเสมอ

หมายเหตุ: การทำแบบนี้หมายความว่าเราจะต้องคอยอัปเดตเวอร์ชันเองเพื่อรับ patch ความปลอดภัยใหม่ๆ แต่มันก็ช่วยให้ Build ของเราคาดเดาได้

แบบนี้สิดี:

FROM node:20-slim

แบบนี้ไม่ค่อยเวิร์ค:

FROM node:latest

4. CMD สำหรับ Arguments, ENTRYPOINT สำหรับ Commands: คู่หูคู่ฮิต

อยากสร้าง Container ที่ยืดหยุ่น ให้ User (และตัวเราเองในอนาคต) สามารถรันด้วยพารามิเตอร์ที่แตกต่างกันได้ง่ายๆ ไหมครับ? การเข้าใจบทบาทของ ENTRYPOINT และ CMD คือกุญแจสำคัญ ENTRYPOINT ควรกำหนดคำสั่งหลักที่ Container ของเราจะรัน ส่วน CMD ควรกำหนด argument เริ่มต้นสำหรับคำสั่งนั้น การตั้งค่าแบบนี้จะช่วยให้ User สามารถ override argument เริ่มต้นเหล่านั้นตอนรัน Container ได้ง่ายๆ ครับ

แบบนี้สิดี:

ENTRYPOINT ["node", "server.js"]
CMD ["--port=3000"]

แบบนี้ไม่ค่อยเวิร์ค:

CMD ["node", "server.js", "--port=3000"]

ด้วยการตั้งค่าแบบ "Good" คุณสามารถรัน docker run myapp เพื่อใช้ค่าเริ่มต้น หรือ docker run myapp --port=8000 เพื่อ override แค่ port ได้ง่ายๆ

5. COPY ดีกว่า ADD: เรียบง่ายและปลอดภัยกว่า

เวลาที่เราต้องการเอาไฟล์เข้าไปใน Image ของเรา ควรจะใช้คำสั่งไหนดี? ถึงแม้ว่า ADD จะมีลูกเล่นพิเศษอย่างการแตกไฟล์ tar โดยอัตโนมัติ หรือการดึงไฟล์จาก URL ระยะไกลได้ แต่ความสามารถพิเศษนี้บางครั้งก็อาจจะนำไปสู่ผลลัพธ์ที่ไม่คาดคิดได้ครับ สำหรับการ copy ไฟล์และไดเรกทอรีจากเครื่องเราแบบตรงไปตรงมา COPY จะชัดเจนและคาดเดาได้ง่ายกว่า ครับ ใช้ COPY ไปเถอะครับ ยกเว้นว่าเราต้องการฟีเจอร์พิเศษของ ADD จริงๆ

แบบนี้สิดี:

COPY ./app /app/

แบบนี้ก็ดี (สำหรับกรณีพิเศษของ ADD):

# แตกไฟล์ tar แล้วเพิ่มเข้าไปใน image
ADD app.tar.gz /app/

แบบนี้ไม่ค่อยเวิร์ค (ถ้าใช้ COPY ก็พอแล้ว):

ADD ./app /app/

6. อัปเดตอย่างชาญฉลาด: รวม apt-get update และ install ไว้ด้วยกัน

ถ้าเรา Build Image บน Debian-based น่าจะได้ใช้ apt-get บ่อยๆ นี่คือเคล็ดลับสำคัญครับ ควรรวม apt-get update กับ apt-get install ไว้ในคำสั่ง RUN เดียวกันเสมอ ทำไมล่ะ? เพราะ apt-get update จะรีเฟรชรายการ package ในเครื่อง ถ้าเรารัน apt-get install ในคำสั่ง RUN แยกต่างหาก ที่ตามมา Docker อาจจะใช้ Layer ที่ cache ไว้สำหรับขั้นตอน update ซึ่งหมายความว่าคำสั่ง install ของเราอาจจะทำงานกับข้อมูล package ที่เก่า ทำให้ได้ package ที่ไม่อัปเดต หรือเกิดปัญหา dependency ได้

แบบนี้สิดี:

RUN apt-get update && apt-get install -y --no-install-recommends \
    package1 \
    package2 \
    && rm -rf /var/lib/apt/lists/*

แบบนี้ไม่ค่อยเวิร์ค:

RUN apt-get update
RUN apt-get install -y --no-install-recommends package1 package2 && rm -rf /var/lib/apt/lists/*
Pro tip: ล้าง apt cache (rm -rf /var/lib/apt/lists/*) ในคำสั่ง RUN เดียวกันเลย เพื่อลดขนาด Image

7. หลักการสิทธิ์น้อยที่สุด: รันในฐานะ Non-Root User

ในโลกของซอฟต์แวร์ การให้สิทธิ์อะไรก็ตามเกินความจำเป็นก็เหมือนกับการเปิดประตูบ้านทิ้งไว้ การรัน Container ของเราในฐานะ root user ถือเป็นความเสี่ยงด้านความปลอดภัยที่สำคัญมาก ครับ ถ้าผู้โจมตีสามารถเจาะเข้ามาในแอปพลิเคชันของคุณได้ การมีสิทธิ์ root ภายใน Container จะทำให้พวกเขามีอำนาจมากเกินไป และอาจจะทำให้พวกเขาสามารถ "ทะลุ" ออกมาส่งผลกระทบต่อระบบ Host ได้เลยทีเดียว ซึ่งเราไม่อยากให้เกิดขึ้นแน่นอน!

จงยึดเป็นกฎเหล็กเลยครับว่า ต้องรันแอปพลิเคชันใน Container ของเราในฐานะ non-root user ซึ่งจะเกี่ยวข้องกับการสร้าง user และ group เฉพาะขึ้นมา ตั้งค่า permission ของไฟล์ หรือไดเรกทอรีให้ถูกต้อง แล้วค่อยสลับไปใช้ user นั้น

แบบนี้สิดี:

FROM node:20-slim

# สร้างไดเรกทอรีสำหรับแอปพลิเคชัน
WORKDIR /usr/src/app

# โดยทั่วไป Node.js image จะมี user 'node' อยู่แล้ว (UID 1000)
# เราจะสร้าง group 'appgroup' และ user 'appuser' ของเราเองเพื่อความชัดเจน
# หรือจะใช้ user 'node' ที่มีอยู่ก็ได้
RUN groupadd -g 10001 appgroup && \
    useradd -u 10000 -m -s /bin/bash -g appgroup appuser # -m สร้าง home dir, -s กำหนด shell

# Copy package.json และ package-lock.json
# ให้สิทธิ์ appuser เป็นเจ้าของไฟล์เหล่านี้ก่อน install
COPY --chown=appuser:appgroup package*.json ./

# เปลี่ยนเป็น appuser ก่อนรัน npm install เพื่อให้ node_modules ถูกสร้างด้วยสิทธิ์ของ appuser
USER appuser

# ติดตั้ง dependencies (ในฐานะ appuser)
RUN npm install

# Copy โค้ดแอปพลิเคชันที่เหลือ ให้ appuser เป็นเจ้าของ
COPY --chown=appuser:appgroup . .

# เปิด port ที่แอปพลิเคชันจะใช้งาน (ตัวอย่าง)
EXPOSE 3000

CMD [ "node", "server.js" ]

หมายเหตุ: ถ้าใช้ user node ที่มีมากับ image อาจจะต้องปรับ chown หรือ WORKDIR permission ตามความเหมาะสม หรือ COPY ไฟล์เข้ามาก่อนแล้ว chown ทั้งไดเรกทอรี WORKDIR ให้ user node ทีเดียว

แบบนี้ไม่ค่อยเวิร์ค:

FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
# ไม่มีการสร้าง user หรือสลับ user, รันทุกอย่างเป็น root
EXPOSE 3000
CMD [ "node", "server.js" ]

8. UID ปลอดภัย: ใช้เลขที่สูงกว่า 10,000

ตอนที่เราสร้าง non-root user นั้น ให้ใส่ใจกับ User ID (UID) ด้วยครับ การใช้ UID ที่ต่ำกว่า 10,000 อาจก่อให้เกิดความเสี่ยงด้านความปลอดภัยในบางระบบได้ ถ้าผู้โจมตีสามารถหลุดออกจาก Container ได้ UID ที่ต่ำอาจจะบังเอิญไปตรงกับ system user ที่มีสิทธิ์สูงบน Host ทำให้ได้ permission ที่ไม่ตั้งใจไป เพื่อความปลอดภัยที่ดีกว่า ควรใช้ UID ที่สูงกว่า 10,000 สำหรับ user ใน Container ของเรา ครับ

แบบนี้สิดี:

RUN groupadd -g 10001 appuser && \
    useradd -u 10001 -g appuser appuser

แบบนี้ไม่ค่อยเวิร์ค:

RUN groupadd -g 100 appuser && \
    useradd -u 100 -g appuser appuser

9. UID/GID แบบคงที่: ความสม่ำเสมอคือกุญแจสำคัญของ File Ownership

ทำไมต้องยุ่งยากกับ User ID (UID) และ Group ID (GID) แบบคงที่ด้วยล่ะ? เวลาที่ Container ของเราไปยุ่งเกี่ยวกับไฟล์ (โดยเฉพาะบน shared volume หรือ host mount) ไฟล์เหล่านั้นจะถูกประทับตราด้วย UID/GID ของ process ใน Container ถ้า Dockerfile ของเราปล่อยให้ระบบกำหนด ID เหล่านี้แบบสุ่ม มันก็อาจจะเปลี่ยนไปในแต่ละ Build ได้ ซึ่งจะทำให้การจัดการ file permission เป็นเรื่องน่าปวดหัว โดยเฉพาะถ้าไฟล์เหล่านั้นจำเป็นต้องถูกเข้าถึงจาก Host ด้วย การใช้ UID/GID แบบคงที่ที่กำหนดไว้ล่วงหน้าจะช่วยให้มั่นใจได้ถึงความสม่ำเสมอ ครับ

แบบนี้สิดี:

ARG UID=10001
ARG GID=10001

RUN groupadd -g $GID appuser && \
    useradd -u $UID -g appuser appuser

แบบนี้ไม่ค่อยเวิร์ค:

RUN adduser --system appuser # UID/GID ถูกกำหนดแบบสุ่ม

10. ลดขนาดด้วย Multi-Stage Builds

Docker Image สุดท้ายของเราแบกสัมภาระตอน Build ไว้เยอะไปหรือเปล่า? เช่น พวก compiler, development header, หรือ library สำหรับเทส ของเหล่านี้มักจะไม่จำเป็นสำหรับการรันแอปพลิเคชันบน Production และมันก็แค่เพิ่มความอ้วนให้กับ Image ของเรา Multi-stage builds เป็นวิธีที่ยอดเยี่ยมในการสร้าง Image ที่เล็กลงและปลอดภัยมากขึ้น แนวคิดก็คือการใช้ stage หนึ่ง (เรียกว่า "builder") เพื่อ compile โค้ดหรือติดตั้ง dependency จากนั้นก็ copy เฉพาะ artifact ที่จำเป็นไปยัง stage สุดท้าย (เรียกว่า "runner")

🔥✅ ดีกว่าเดิม: ตัวอย่าง Multi-Stage Build (Node.js)

# Stage 1: The Builder
FROM node:20-slim AS builder
WORKDIR /usr/src/app

COPY package*.json ./
# ติดตั้ง dependencies ทั้งหมด รวมถึง devDependencies เพื่อใช้ build
RUN npm install

COPY . .
# ตัวอย่าง: ถ้ามีการ build step เช่น TypeScript to JavaScript หรือ build frontend assets
# RUN npm run build

# Stage 2: The Runner (Production Image)
FROM node:20-slim AS runner
WORKDIR /usr/src/app

# สร้าง non-root user สำหรับ production
RUN groupadd -g 10001 appgroup && \
    useradd -u 10000 -m -s /bin/bash -g appgroup appuser

# Copy package.json และ package-lock.json (ถ้ามี)
COPY --chown=appuser:appgroup package*.json ./

# ติดตั้งเฉพาะ production dependencies
# และให้สิทธิ์ appuser เป็นเจ้าของ node_modules
USER appuser # เปลี่ยนเป็น user ก่อน install
RUN npm ci --omit=dev
# หรือ RUN npm install --production

# Copy โค้ดที่ build แล้ว หรือ source code ที่จำเป็นจาก builder stage
# และ copy source code ที่เหลือ (ถ้าไม่ได้ build ทั้งหมด)
# ตรวจสอบ path ให้ถูกต้องตามโครงสร้างโปรเจกต์ของคุณ
COPY --chown=appuser:appgroup --from=builder /usr/src/app/dist ./dist  # ถ้ามี build output ใน dist
COPY --chown=appuser:appgroup . . # หรือ copy เฉพาะไฟล์ที่จำเป็น เช่น server.js, public/

EXPOSE 3000
CMD [ "node", "dist/server.js" ] # หรือ "server.js" ถ้าไม่มี build step

การใช้ multi-stage ช่วยให้เก็บ devDependencies และเครื่องมือ build ต่างๆ ไว้แค่ใน builder stage ทำให้ image สุดท้าย (runner) เล็กและปลอดภัยขึ้นมาก

11. ทิปสำหรับชาว Node.js: ติดตั้งเฉพาะ Production Dependencies และจัดการ Cache

สำหรับ Node.js การทำให้ Image เล็กลงเกี่ยวข้องกับการจัดการ node_modules อย่างมีประสิทธิภาพ

  1. ติดตั้งเฉพาะ Production Dependencies: ใน stage สุดท้าย (runner) ของ multi-stage build หรือใน single-stage build ที่จะใช้บน production, ให้แน่ใจว่าคุณติดตั้งเฉพาะ dependencies ที่จำเป็นสำหรับการรันแอปพลิเคชันเท่านั้น โดยไม่รวม devDependencies
  2. การจัดการ NPM Cache: โดยทั่วไป npm จะ cache package ที่ดาวน์โหลดไว้ การเคลียร์ cache ใน Dockerfile อาจจะไม่ช่วยลดขนาด layer มากนัก เพราะ cache ของ npm มักจะอยู่นอก context ของ layer ที่กำลังถูกสร้าง (เช่น ใน user's home directory ของ build environment) สิ่งสำคัญกว่าคือการไม่ให้ไฟล์ที่ไม่จำเป็นเข้าไปใน image ตั้งแต่แรก

แบบนี้สิดี (ใน stage สุดท้าย หรือ production build):

# (หลังจาก COPY package*.json ./)
# ถ้ามี package-lock.json หรือ npm-shrinkwrap.json (แนะนำให้มี)
RUN npm ci --omit=dev

# หรือถ้าไม่มี lock file (ไม่แนะนำสำหรับ production build ที่ต้องการความแน่นอน)
# RUN npm install --production

การใช้ npm ci --omit=dev จะช่วยให้การติดตั้งเร็วขึ้น, เป็นไปตาม lock file และไม่ติดตั้ง devDependencies ทำให้ node_modules ใน image ของคุณเล็กลง

12. Cache อย่างชาญฉลาด: เรียง Layer ตามความถี่ในการเปลี่ยนแปลง

Docker ฉลาดเรื่องการ cache layer ครับ ถ้าบรรทัดใน Dockerfile ของเราไม่มีการเปลี่ยนแปลง และบรรทัดก่อนหน้าก็ไม่มีการเปลี่ยนแปลงเช่นกัน Docker จะนำ Layer จาก Build ก่อนหน้ามาใช้ซ้ำ ซึ่งจะช่วยเร่งความเร็วในการ Build ของเราได้อย่างมหาศาล! เพื่อให้ได้ประโยชน์สูงสุดจากตรงนี้ ควรเรียงลำดับคำสั่งใน Dockerfile จากส่วนที่เปลี่ยนแปลงน้อยที่สุดไปยังส่วนที่เปลี่ยนแปลงบ่อยที่สุด ครับ

ตัวอย่างการเรียงลำดับที่ดี:

  1. Base Image (เช่น FROM node:20-slim)
  2. System Dependencies (ถ้ามี, ผ่าน apt-get install)
  3. Copy package.json และ package-lock.json
  4. RUN npm install (หรือ npm ci) - Layer นี้จะถูก cache ถ้า package*.json ไม่เปลี่ยน
  5. Copy โค้ดแอปพลิเคชันของคุณ (เปลี่ยนแปลงบ่อยที่สุด)
FROM node:20-slim
WORKDIR /usr/src/app

# ติดตั้ง system dependencies (ถ้ามี, เปลี่ยนแปลงน้อย)
# RUN apt-get update && apt-get install -y --no-install-recommends some-package

# Copy ไฟล์ dependency ก่อน (เปลี่ยนแปลงน้อยกว่าโค้ด)
COPY package*.json ./

# ติดตั้ง dependencies (จะถูก cache ถ้า package*.json ไม่เปลี่ยน)
RUN npm install --production # หรือ npm ci --omit=dev

# Copy โค้ดแอปพลิเคชัน (เปลี่ยนแปลงบ่อยที่สุด)
COPY . .

EXPOSE 3000
CMD [ "node", "server.js" ]

แบบนี้ไม่ค่อยเวิร์ค (ลำดับไม่เหมาะสม):

FROM node:20-slim
WORKDIR /usr/src/app

# Copy โค้ดทั้งหมดก่อน ทำให้ cache ของ npm install ไม่ถูกใช้บ่อย
COPY . .

RUN npm install --production

EXPOSE 3000
CMD [ "node", "server.js" ]

13. ไฟล์ .dockerignore: แผนไดเอทสำหรับ Build Context ของคุณ

เวลาที่เรารัน docker build ตัว Docker CLI จะส่ง "build context" (โดยปกติคือไดเรกทอรีที่มี Dockerfile และโค้ดของเรา) ไปยัง Docker daemon ถ้า context นี้เต็มไปด้วยสิ่งที่ไม่จำเป็นสำหรับการ Build (เช่น ไดเรกทอรี .git, node_modules ในเครื่อง, ไฟล์ชั่วคราว, editor configs) มันจะทำให้ Build ช้าลง และอาจจะทำให้ข้อมูลที่ละเอียดอ่อนรั่วไหลเข้าไปใน Image ได้ถ้าไม่ระวังในการใช้ COPY คำสั่ง

ใช้ไฟล์ .dockerignore เพื่อระบุไฟล์และไดเรกทอรีที่ไม่ต้องการให้รวมอยู่ใน build context ครับ มันทำงานเหมือนกับไฟล์ .gitignore เลย

ตัวอย่าง .dockerignore:

# Version control
.git
.gitignore

# Local dependencies
node_modules # สำคัญมาก! ไม่ควร copy node_modules จากเครื่องเราเข้าไป
npm-debug.log
*.log

# Development tools & OS specific
.idea/
.vscode/
*.swp
*.swo
.DS_Store

14. WORKDIR: กำหนดเวทีของเราให้ชัดเจน

แทนที่จะใส่ RUN cd /some/path && do_something เต็มไปหมดใน Dockerfile ของเรา ให้ใช้คำสั่ง WORKDIR เพื่อกำหนดไดเรกทอรีปัจจุบันสำหรับคำสั่งที่จะตามมา เช่น RUN, CMD, ENTRYPOINT, COPY, และ ADD มันจะทำให้ Dockerfile ของเราสะอาดขึ้น อ่านง่ายขึ้น และมีโอกาสเกิดข้อผิดพลาดจากการจัดการ path ที่ซับซ้อนน้อยลงด้วยครับ

WORKDIR /usr/src/app # แบบนี้ดี
# ... คำสั่งต่อๆ ไปจะรันใน /usr/src/app

# หลีกเลี่ยงแบบนี้:
# RUN cd /app && command # แบบนี้ไม่ค่อยดี

15. ความยืดหยุ่นตอน Build ด้วย ARG

ต้องการส่งค่า config บางอย่างเข้าไปในกระบวนการ Build Docker ของเราไหมครับ เช่น การกำหนดหมายเลขเวอร์ชัน หรือการเปิดใช้งานฟีเจอร์บางอย่างสำหรับ dev build เทียบกับ prod build? Build-time arguments (ARG) คือเพื่อนซี้ของเราครับ เราสามารถกำหนดมันไว้ใน Dockerfile แล้วค่อยกำหนดค่าของมันตอนที่รัน docker build

ARG NODE_ENV=development
ARG APP_PORT=3000

ENV NODE_ENV=${NODE_ENV}
EXPOSE ${APP_PORT}

# CMD ["node", "server.js"]

จากนั้นก็ Build ด้วย docker build --build-arg NODE_ENV=production --build-arg APP_PORT=8080 -t myapp .

16. COPY --chmod: กำหนด Permission ทันทีที่ Copy

ต้องการ copy script เข้าไปใน Image แล้วทำให้มัน execute ได้ใช่ไหมครับ? แทนที่จะใช้ COPY แล้วตามด้วย RUN chmod ... เราสามารถทำได้ในขั้นตอนเดียวเลย! คำสั่ง COPY (และ ADD) มี flag --chmod (ต้องใช้ BuildKit backend ซึ่งเป็น default ใน Docker เวอร์ชันใหม่ๆ) ที่ให้เรากำหนด file permission ได้โดยตรง ช่วยลด Layer ที่ไม่จำเป็นลงไปอีกหนึ่ง และทำให้ Dockerfile ของเราดูเรียบร้อยขึ้นด้วย ครับ

แบบนี้สิดี:

  COPY --chmod=755 ./my-script.sh /usr/local/bin/my-script.sh

แบบนี้ไม่ค่อยเวิร์ค:

COPY ./my-script.sh /usr/local/bin/my-script.sh
RUN chmod +x /usr/local/bin/my-script.sh

17. Debian/Ubuntu Tip (ใช้กับ Node.js ได้ด้วยนะ!): --no-install-recommends สำหรับ apt-get

ถ้า Base Image ของ Node.js ที่เลือกใช้เป็น Debian/Ubuntu based (เช่น node:20-slim) และต้องการติดตั้ง system package เพิ่มเติมด้วย apt-get install, มันมักจะดึง package ที่ "recommended" และ "suggested" ติดมาด้วย ถึงแม้บางครั้งจะมีประโยชน์ แต่ package เหล่านี้ก็อาจจะเพิ่มความอ้วนให้กับ Docker Image ของเราโดยไม่จำเป็น เพื่อให้ Image ยังคงผอมเพรียว ให้ใช้ flag --no-install-recommends กับ apt-get install ครับ มันจะบอก apt ให้ติดตั้งเฉพาะ dependency หลักๆ เท่านั้น

แบบนี้สิดี:

RUN apt-get update && \
    apt-get install -y --no-install-recommends some-package && \
    apt-get clean && rm -rf /var/lib/apt/lists/*

และอย่าลืมล้าง apt cache หลังติดตั้งเสร็จด้วยนะครับ!

18. ได้เวลาจูนอัป: การปรับแต่งประสิทธิภาพทั่วไป

ขึ้นอยู่กับภาษาหรือ runtime ที่แอปพลิเคชันของเราใช้ มักจะมี environment variable ที่เราสามารถตั้งค่าเพื่อปรับแต่งประสิทธิภาพการทำงานภายใน Container ได้ครับ สิ่งเหล่านี้สามารถช่วยจัดการหน่วยความจำ ปรับปรุงความเร็วในการทำงาน หรือใช้ทรัพยากรของ Container ได้ดีขึ้น

นี่คือตัวอย่างบางส่วน:

# การปรับแต่งสำหรับ Node.js
ENV NODE_OPTIONS="--max-old-space-size=2048" \
    UV_THREADPOOL_SIZE=64 \
    NODE_NO_WARNINGS=1

# การปรับแต่งสำหรับ Python
ENV PYTHONUNBUFFERED=1 \ # ทำให้ print() และ log แสดงผลทันที
    PYTHONDONTWRITEBYTECODE=1 # ป้องกันการสร้างไฟล์ .pyc ช่วยประหยัดพื้นที่และ I/O

# การปรับแต่งสำหรับ Golang
ENV GOGC=off \
    GOMAXPROCS=2

ลองค้นหาข้อมูลเพิ่มเติมสำหรับ stack ที่ใช้อยู่นะครับ การปรับแต่งเล็กๆ น้อยๆ เหล่านี้สามารถสร้างความแตกต่างได้มากเลยทีเดียว!

19. ติดป้ายให้งานของคุณ: Metadata เพื่อความสบายใจ

เมื่อเรา Build Image มากขึ้นเรื่อยๆ การติดตามว่า Image ไหนเป็นอะไรอาจจะเริ่มยากขึ้น ใช้คำสั่ง LABEL เพื่อเพิ่ม metadata ให้กับ Image ของเรา ครับ คิดซะว่าเหมือนการติดฉลากบนขวดโหล เราสามารถใส่ข้อมูลอย่างเช่น ผู้ดูแล, คำอธิบาย Image, เวอร์ชัน, ลิงก์ไปยังโปรเจกต์ และอื่นๆ อีกมากมาย สิ่งนี้มีประโยชน์มากสำหรับการจัดการ Image, การทำ automation, และการทำความเข้าใจวัตถุประสงค์ของ Image ได้อย่างรวดเร็ว

LABEL maintainer="Your Name <youremail@example.com>" \
      org.opencontainers.image.title="My Awesome Application" \
      org.opencontainers.image.description="This Docker image runs my awesome application." \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.source="https://github.com/yourrepo/myapp"

(การใช้ label ที่เป็นมาตรฐานอย่างของ org.opencontainers เป็นสิ่งที่ดีครับ!)

20. กับดัก COPY .: ระบุให้ชัดเจนดีกว่า!

มันง่ายมากที่จะใช้ COPY . /app เพื่อเอาโค้ดแอปพลิเคชันทั้งหมดของเราเข้าไปใน Image อย่างไรก็ตาม วิธีนี้อาจจะโลภไปหน่อย มันจะดึงทุกอย่างจาก build context ของเราเข้ามาหมด รวมถึงไฟล์ที่อาจจะลืมใส่ไว้ใน .dockerignore (เช่น config ส่วนตัวในเครื่อง หรือ artifact ชั่วคราวตอน Build) ซึ่งจะทำให้ Image ของเราใหญ่เกินความจำเป็น และอาจจะมีไฟล์ที่ไม่ได้ตั้งใจให้รวมอยู่ด้วยก็ได้ เมื่อไหร่ก็ตามที่เป็นไปได้ พยายามระบุให้ชัดเจนว่าเรากำลัง copy อะไร ครับ

แทนที่จะใช้:

COPY . /app # อาจจะ copy มากเกินไป

ลองพิจารณาระบุให้ชัดเจนขึ้นถ้าทำได้:

COPY src/ /app/src/
COPY public/ /app/public/
COPY package.json /app/

วิธีนี้อาจจะต้องใช้ความคิดมากขึ้นหน่อย แต่จะนำไปสู่ Image ที่สะอาดและเล็กกว่าครับ และการมี .dockerignore ที่ดีมีความสำคัญมากเมื่อใช้ COPY . . ครับ!

ส่งท้าย

ป๊าด! เหนื่อยเเลย... จบไปแล้วสำหรับเคล็ดลับเด็ดๆ ในการปั้น Dockerfile อาจจะดูเยอะไปหน่อย แต่การนำเอาเคล็ดลับเหล่านี้ไปปรับใช้แม้เพียงไม่กี่ข้อ ก็สามารถสร้างความแตกต่างอย่างใหญ่หลวงให้กับขนาด Image, เวลาในการ Build, และความปลอดภัยโดยรวมของเราได้เลยนะครับ

การสร้าง Docker Image ที่มีประสิทธิภาพและปลอดภัยเป็นเส้นทางที่ต้องเรียนรู้และปรับปรุงอยู่เสมอ ไม่ใช่จุดหมายปลายทาง ดังนั้น ลองนำเคล็ดลับเหล่านี้ไปทดลองใช้ แล้วดูว่ามันจะช่วยให้ขั้นตอนการพัฒนาและ deploy ของเราราบรื่นขึ้นได้อย่างไรบ้าง

แล้วคุณล่ะครับ มีทริคเด็ดๆ ในการเขียน Dockerfile หรือ "อ๋อ!" โมเมนต์อะไรที่อยากจะแชร์บ้างไหม? มาแบ่งปันความรู้กันในคอมเมนต์ข้างล่างนี้ได้เลยนะ เรามาเรียนรู้ไปด้วยกัน! Happy Dockering ครับ!