ข้ามไปยังเนื้อหา

Layers และ 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 ด้วย

ลองดู Dockerfile ที่จัดลำดับไม่ดีตัวนี้:

# BAD: copies all source code first
FROM node:22-alpine
WORKDIR /app
COPY . . # ← copies everything, including app source
RUN 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 separately
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./ # ← only changes when deps change
RUN npm ci --omit=dev # ← cached as long as package.json unchanged
COPY . . # ← source changes only bust this layer

ตอนนี้ npm ci จะถูกรันใหม่ก็ต่อเมื่อ package.json หรือ package-lock.json เปลี่ยนเท่านั้น การแก้โค้ดแอปจะทำให้เฉพาะ COPY . . สุดท้ายและทุกอย่างที่อยู่ใต้บรรทัดนั้นเสีย 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.4s

build ครั้งที่สอง — เปลี่ยนเฉพาะ 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 จากที่ เปลี่ยนน้อยที่สุด ไปยังที่ เปลี่ยนบ่อยที่สุด:

  1. FROM (base image — แทบไม่เปลี่ยน)
  2. การติดตั้ง system package (RUN apt-get install ...)
  3. คัดลอก dependency manifest + ติดตั้ง (COPY package.json ./ + RUN npm ci)
  4. source code ของ application (COPY . .)
  5. ขั้นตอน build ที่ขึ้นกับ source (RUN npm run build)

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 .
ตัวเลือกBenefitCost
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 ออกจาก COPY source เสมอ เพื่อให้ CI/CD pipeline ที่ build ซ้ำหลายพันครั้งต่อวันใช้ cache ของ dependency layer ซ้ำได้ ประหยัดเวลา build รวมของทั้งองค์กรได้มหาศาล

เกิดอะไรขึ้นกับทุก layer ที่อยู่ใต้ layer ที่เสียค่า?
ทำไมเราจึง COPY package.json ก่อน COPY . . ใน Dockerfile ของ Node.js?
instruction ใดต่อไปนี้สร้าง layer ใหม่ใน image?
การ `docker build` ครั้งที่สองบน source ที่ไม่เปลี่ยนจะพิมพ์ `CACHED` สำหรับขั้นตอนส่วนใหญ่ นั่นหมายความว่าอะไร?