Layers และ Build Cache
build cache ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “build cache ทำงานอย่างไร”ทุก instruction ใน Dockerfile สร้าง layer ขึ้นมาหนึ่งชั้น Docker เก็บ layer เหล่านี้ไว้บนดิสก์และกำหนด cache key ให้แต่ละชั้นโดยอิงจาก:
- ตัวข้อความ instruction เอง
- cache key ของ layer แม่
- (สำหรับ
COPY/ADD) checksum ของไฟล์ที่ถูกคัดลอก
เมื่อคุณ rebuild image Docker จะตรวจสอบแต่ละ instruction เทียบกับ cache ถ้า cache key ตรงกัน Docker จะนำ layer ที่ cache ไว้มาใช้ซ้ำ — ทันทีและไม่มีต้นทุน ถ้า cache key ไม่ตรงกัน (instruction เปลี่ยน หรือไฟล์ที่คัดลอกเปลี่ยน) Docker จะทำให้ layer นั้น และทุก layer หลังจากนั้น เสียค่า cache แล้ว rebuild ใหม่ตั้งแต่ต้น
นี่คือกฎสำคัญ:
การทำ cache เสียค่าจะลามต่อลงไปด้านล่าง เมื่อ layer หนึ่งเสียค่า layer ถัด ๆ ไปทั้งหมดก็จะถูก rebuild ด้วย
ทำไมลำดับของ instruction จึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมลำดับของ instruction จึงสำคัญ”ลองดู Dockerfile ที่จัดลำดับไม่ดีตัวนี้:
# BAD: copies all source code firstFROM node:22-alpineWORKDIR /appCOPY . . # ← copies everything, including app sourceRUN npm ci --omit=dev # ← runs after COPY — cache busted on every code changeทุกครั้งที่คุณแก้ไฟล์ source เพียงไฟล์เดียว COPY . . จะเสียค่า ซึ่งทำให้ RUN npm ci เสียค่าตามไปด้วย นั่นหมายความว่า npm จะติดตั้ง dependencies ใหม่ทั้งหมดในทุกการ build — แม้ว่า package.json จะไม่ได้เปลี่ยนก็ตาม
วิธีแก้คือคัดลอก dependency manifest ก่อน, ติดตั้ง, แล้วจึงคัดลอก source:
# GOOD: dependency install is cached separatelyFROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./ # ← only changes when deps changeRUN npm ci --omit=dev # ← cached as long as package.json unchangedCOPY . . # ← source changes only bust this layerตอนนี้ npm ci จะถูกรันใหม่ก็ต่อเมื่อ package.json หรือ package-lock.json เปลี่ยนเท่านั้น การแก้โค้ดแอปจะทำให้เฉพาะ COPY . . สุดท้ายและทุกอย่างที่อยู่ใต้บรรทัดนั้นเสีย cache เท่านั้น
เห็น cache ทำงานจริง
หัวข้อที่มีชื่อว่า “เห็น cache ทำงานจริง”build ครั้งแรก — ทุกอย่างเป็นการดาวน์โหลดใหม่หมด:
[+] Building 14.7s (7/7) FINISHED => [1/5] FROM docker.io/library/node:22-alpine 5.2s => [2/5] WORKDIR /app 0.0s => [3/5] COPY package.json package-lock.json ./ 0.0s => [4/5] RUN npm ci --omit=dev 8.1s => [5/5] COPY . . 0.0s => exporting to image 1.4sbuild ครั้งที่สอง — เปลี่ยนเฉพาะ source ของแอป npm ci ถูก cache ไว้:
[+] Building 1.1s (7/7) FINISHED => [1/5] FROM docker.io/library/node:22-alpine 0.0s => CACHED [2/5] WORKDIR /app 0.0s => CACHED [3/5] COPY package.json package-lock.json ./ 0.0s => CACHED [4/5] RUN npm ci --omit=dev 0.0s => [5/5] COPY . . 0.0s => exporting to image 1.1sการ build ลดจาก 14.7 วินาทีเหลือ 1.1 วินาที เพราะสี่ในห้า layer ถูกเสิร์ฟมาจาก cache
กฎการจัดลำดับโดยทั่วไป
หัวข้อที่มีชื่อว่า “กฎการจัดลำดับโดยทั่วไป”จัดลำดับ instruction จากที่ เปลี่ยนน้อยที่สุด ไปยังที่ เปลี่ยนบ่อยที่สุด:
FROM(base image — แทบไม่เปลี่ยน)- การติดตั้ง system package (
RUN apt-get install ...) - คัดลอก dependency manifest + ติดตั้ง (
COPY package.json ./+RUN npm ci) - source code ของ application (
COPY . .) - ขั้นตอน build ที่ขึ้นกับ source (
RUN npm run build)
ลงมือทำ: สังเกต cache
หัวข้อที่มีชื่อว่า “ลงมือทำ: สังเกต cache”snippet ด้านล่าง build image เล็ก ๆ สองครั้ง ลองรันดูแล้วสังเกตว่า build ครั้งที่สองใช้ layer ที่เป็น CACHED
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
# Step 1: copy only the manifest and install
RUN echo '{"name":"demo","version":"1.0.0"}' > package.json
RUN npm install --omit=dev 2>/dev/null || true
# Step 2: copy "source" (simulated with an echo)
RUN echo "console.log('Hello from app v1');" > app.js
CMD ["node", "app.js"]
# --- first build ---
# docker build -t cachetest .
# docker run --rm cachetest
# --- change "source" and rebuild: only the last RUN reruns ---
# docker build -t cachetest .ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
COPY แยกไฟล์ทีละกลุ่ม (manifest ก่อน source) | cache แม่นยำ ใช้ซ้ำได้มากที่สุด | ต้องเขียน Dockerfile ระมัดระวังเรื่องลำดับ instruction มากขึ้น |
COPY . . รวมทีเดียว | เขียนง่าย Dockerfile สั้น | cache เสียง่าย แทบทุกครั้งที่โค้ดเปลี่ยนต้อง rebuild ใหม่หมด |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”COPY . .ทั้งโปรเจกต์ก่อนRUN npm ciทำให้ cache เสียทุกครั้งที่โค้ดเปลี่ยน แม้ dependency จะเหมือนเดิม- ไม่มี
.dockerignoreทำให้ checksum ของ build context เปลี่ยนบ่อยเกินจำเป็น cache เสียง่ายขึ้น - เรียง instruction ที่เปลี่ยนบ่อย (เช่น คัดลอก source) ไว้บนสุด ทำให้ layer ที่ควรจะนิ่งซึ่งอยู่ถัดลงไปเสีย cache ตามไปด้วย
💡 ตัวอย่างจากของจริง
Docker Hub official image ของ
nodeแยกCOPY package.jsonออกจากCOPYsource เสมอ เพื่อให้ CI/CD pipeline ที่ build ซ้ำหลายพันครั้งต่อวันใช้ cache ของ dependency layer ซ้ำได้ ประหยัดเวลา build รวมของทั้งองค์กรได้มหาศาล