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

Layers และ Build Cache

วัดบน Docker Desktop 4.81.0 / Engine 29.6.1, Buildx v0.35.0-desktop.2, BuildKit v0.31.1 (macOS, linux/arm64), 2026-09-30 ด้วย --progress=plain โปรเจกต์ตัวอย่างมี dependency เล็กเดียว (ms) เวลาแบบสัมบูรณ์จึงเล็กทั้งหมด สิ่งที่ควรอ่านคือ รูปแบบ ของบรรทัด CACHED และในโปรเจกต์จริงขั้น npm ci คือขั้นที่ใช้เวลาหลายสิบวินาที

Docker เก็บขั้นตอนการ build ไว้บนดิสก์และให้ cache key แก่แต่ละขั้นตามสิ่งเหล่านี้:

  • ข้อความของ instruction เอง
  • cache key ของขั้นตอนแม่
  • (สำหรับ COPY/ADD) checksum ของ เนื้อหา ไฟล์ที่คัดลอก

เมื่อ rebuild image BuildKit ตรวจแต่ละขั้นกับ cache ถ้า key ตรงกัน จะใช้ผลที่เก็บไว้ซ้ำและพิมพ์ CACHED ถ้า key ไม่ ตรง (instruction เปลี่ยน หรือไฟล์ที่คัดลอกเปลี่ยน) ขั้นนั้น และทุกขั้นหลังมัน จะถูก build ใหม่

นี่คือกฎสำคัญ:

การ invalidate cache ไหลลงล่าง เมื่อขั้นตอนหนึ่งถูก invalidate ขั้นตอนถัดไปทั้งหมดก็ถูก build ใหม่ด้วย

(“ขั้นตอน” กับ “layer” เกือบเป็นสิ่งเดียวกันที่นี่: instruction ที่เป็น metadata อย่าง ENV หรือ CMD ก็ถูก cache เช่นกัน แต่ไม่เพิ่ม filesystem layer ตามที่บทเรียนพื้นฐาน Dockerfileพิสูจน์ไว้) เอกสารอ้างอิงคือ Docker build cache

โปรเจกต์ที่ใช้วัดทุกอย่างด้านล่าง (package.json ที่มี dependency เดียว, lockfile ของมัน และไฟล์ซอร์สหนึ่งไฟล์):

package.json
{
"name": "cachedemo",
"version": "1.0.0",
"private": true,
"dependencies": { "ms": "2.1.3" }
}
package-lock.json
{
"name": "cachedemo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cachedemo",
"version": "1.0.0",
"dependencies": {
"ms": "2.1.3"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
}
}
}
src/app.js
console.log("v1")

ก่อนอื่น Dockerfile ที่เรียงลำดับไม่ดี:

Dockerfile.bad
# @build
FROM node:24-alpine
WORKDIR /app
# BAD: copies all source code first
COPY . .
# runs after COPY, so it re-runs on every code change
RUN npm ci --omit=dev
CMD ["node", "src/app.js"]

ทุกครั้งที่คุณเปลี่ยนไฟล์ซอร์สเพียงไฟล์เดียว COPY . . ถูก invalidate ซึ่งทำให้ RUN npm ci ถูก invalidate ด้วย วัดได้: หลังแก้ src/app.js transcript เป็น

#6 [2/4] WORKDIR /app
#6 CACHED
#7 [3/4] COPY . . # re-run: the context content changed
#8 [4/4] RUN npm ci --omit=dev # re-run, no CACHED
#8 0.741 added 1 package, and audited 2 packages in 561ms

วิธีแก้คือคัดลอก manifest ของ dependency ก่อน ติดตั้ง แล้วค่อยคัดลอกซอร์ส:

Dockerfile.good
# @build
FROM node:24-alpine
WORKDIR /app
# only changes when dependencies change
COPY package.json package-lock.json ./
# cached as long as the manifests are unchanged
RUN npm ci --omit=dev
# source changes only invalidate this last COPY
COPY src/ ./src/
CMD ["node", "src/app.js"]

หลังแก้ src/app.js หนึ่งบรรทัดเหมือนกัน:

#6 [2/5] WORKDIR /app
#6 CACHED
#7 [3/5] COPY package.json package-lock.json ./
#7 CACHED
#8 [4/5] RUN npm ci --omit=dev
#8 CACHED # the expensive step was skipped
#9 [5/5] COPY src/ ./src/ # only this step ran

ตอนนี้ npm ci รันใหม่เฉพาะเมื่อ package.json หรือ package-lock.json เปลี่ยน

cache key ของ COPY มาจาก เนื้อหา ของไฟล์ ไม่ใช่ timestamp วัดสองแบบกับ Dockerfile ที่ดี:

$ touch src/app.js # new mtime, identical bytes
... [5/5] COPY src/ ./src/ CACHED # still a cache hit
$ echo 'console.log("v1") ' > src/app.js # one trailing space added
... [4/5] RUN npm ci --omit=dev CACHED
... [5/5] COPY src/ ./src/ # re-run, no CACHED

ดังนั้น git checkout ใหม่ (ที่เขียน mtime ใหม่) ไม่ทำให้ cache แตกเอง แต่ไบต์ที่เปลี่ยนเพียงไบต์เดียวทำให้แตก cache ยังแตกเมื่อข้อความของ RUN เปลี่ยน และ --no-cache ข้าม cache ทั้งหมด (ใช้กับการ build แบบ “cold” ในคอร์สนี้)

Build แบบ cold (--no-cache) แล้ว build ซ้ำโดยไม่เปลี่ยนอะไร จำนวนบรรทัด CACHED ในการรันครั้งที่สอง: 3 จาก 4 ขั้น (bad) และ 4 จาก 5 ขั้น (good); ขั้นที่เหลือคือ FROM ซึ่งถูก resolve ไม่ใช่ build การ build ที่ไม่มีอะไรเปลี่ยนจึงไม่มีงานให้ทำ:

$ docker build --no-cache -f Dockerfile.good -t cachedemo . # every step runs
$ docker build -f Dockerfile.good -t cachedemo .
#6 [2/5] WORKDIR /app CACHED
#7 [3/5] COPY package.json package-lock.json CACHED
#8 [4/5] RUN npm ci --omit=dev CACHED
#9 [5/5] COPY src/ ./src/ CACHED

ในแอปจริง npm ci หรือ pip install ใช้เวลาหลายสิบวินาทีถึงหลายนาที ซึ่งเป็นจุดที่กฎการเรียงลำดับให้ผล ที่นี่ใช้ 0.4-0.8 วินาทีเพราะ dependency tree มีแพ็กเกจเดียว อย่าประมาณค่าจากตัวเลขของเรา ให้วัดโปรเจกต์ของคุณเองด้วย time docker build

เรียง instruction จาก เปลี่ยนน้อยที่สุด ไป เปลี่ยนบ่อยที่สุด:

  1. FROM (base image — เปลี่ยนน้อย)
  2. ติดตั้งแพ็กเกจระบบ (RUN apt-get install ...)
  3. คัดลอก manifest ของ dependency แล้วติดตั้ง (COPY package.json ./ + RUN npm ci)
  4. โค้ดแอป (COPY . .)
  5. ขั้นตอน build ที่ขึ้นกับซอร์ส (RUN npm run build)

layer cache เป็นแบบทั้งหมดหรือไม่มีเลยต่อขั้น: เปลี่ยน package.json แล้ว npm ci ดาวน์โหลดทุกอย่างใหม่ cache mount เก็บไดเรกทอรีไว้นอก image ระหว่าง build ทำให้ cache การดาวน์โหลดอยู่รอดแม้ขั้นตอนนั้นรันใหม่:

Dockerfile.mount
# @build
# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
CMD ["node", "-e", "console.log(require('ms')(60000))"]

image build สำเร็จและพิมพ์ 1m (แพ็กเกจ ms แปลง 60000 ms) docker history แสดงขั้นตอนนี้เป็น RUN /bin/sh -c npm ci --omit=dev # buildkit ธรรมดา — ไดเรกทอรี cache ไม่ได้ เป็นส่วนของ image มันถูกเก็บใน cache ของ builder; ดูสิ่งที่ builder เก็บด้วย docker buildx du (output นั้นครอบคลุมทั้ง host จึงไม่ได้ทำซ้ำที่นี่) เราไม่ได้วัดความเร็วที่เพิ่มขึ้นกับโปรเจกต์แพ็กเกจเดียวนี้ ประโยชน์ตามเอกสารอยู่ที่ dependency tree ขนาดใหญ่ (cache mounts)

snippet ด้านล่าง build image เล็กสองครั้ง รันแล้วดู build ที่สองใช้ layer CACHED

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
# Step 1: write a manifest and install
RUN echo '{"name":"demo","version":"1.0.0"}' > package.json
RUN npm install --omit=dev 2>/dev/null || true
# Step 2: write "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 the last RUN (v1 -> v2) and rebuild: only that step reruns ---
# docker build -t cachetest .

Dockerfile ที่ AI เขียนพร้อม comment ช่วยอธิบายทุกบรรทัด:

🐛 หา bug ในโค้ดนี้

FROM alpine:3.24
WORKDIR /app
COPY src/ ./src/ # copy the application source
CMD ["ls", "src"]
เฉลย

comment ใน Dockerfile ถูกรู้จักเฉพาะที่ ต้นบรรทัด บรรทัด COPY ทุกอย่างหลัง destination เป็นอาร์กิวเมนต์เพิ่ม ดังนั้นนี่คือ COPY src/ ./src/ # copy the application source ที่มีต้นทาง src/, ./src/, #, copy, the, application และปลายทาง source การ build ล้มด้วย error เกี่ยวกับไฟล์ชื่อ the:

#7 ERROR: failed to calculate checksum of ref …: "/the": not found
ERROR: failed to build: failed to solve: failed to compute cache key: … "/the": not found

กับดัก: docker buildx build --check — linter ในตัว — รายงาน Check complete, no warnings found สำหรับไฟล์นี้ การวินิจฉัยจริงมาจาก hadolint (ซึ่ง harness ของคอร์สรันด้วยเหตุผลนี้): DL3021 error: COPY with more than 2 arguments requires the last argument to end with / ให้วาง comment ในบรรทัดของตัวเอง:

Dockerfile.comment
# @build
FROM alpine:3.24
WORKDIR /app
# copy the application source
COPY src/ ./src/
CMD ["ls", "src"]

กฎเดียวกันใช้กับ RUN: # comment ท้ายบรรทัดกลายเป็นส่วนของคำสั่ง shell (ไม่เป็นอันตรายเพราะ shell ถือว่าเป็น comment แต่ใช้กับ COPY, ENV หรือ EXPOSE ไม่ได้)

build คือ ห่วงโซ่ของฟังก์ชันที่จำผลไว้ (memoised): แต่ละขั้นมี key เป็น (ผลของขั้นแม่, instruction, เนื้อหาไฟล์ input) เปลี่ยน input ใดก็ตาม ขั้นนั้นและทุกอย่างด้านล่างต้องคำนวณใหม่; ด้านบนใช้ซ้ำ การเรียง Dockerfile คือการตัดสินใจว่า input ใดมีสิทธิ์ทำให้งานส่วนไหนเป็นโมฆะ

ทางเลือกข้อดีข้อเสีย
COPY แยกตามกลุ่ม (manifest ก่อน แล้วซอร์ส)cache แม่นยำ ใช้ซ้ำสูงสุด (ขั้น npm ci ค้างที่ CACHED)ต้องเขียน Dockerfile ให้ระวังลำดับ instruction มากขึ้น
COPY . . ครั้งเดียวเขียนง่าย Dockerfile สั้นcache แตกง่าย — ทุกการแก้โค้ดทำให้ npm ci รันใหม่
--no-cacheรับประกัน build สะอาดตั้งแต่ต้นทิ้งการใช้ซ้ำทั้งหมด ช้า
Cache mountการดาวน์โหลดแพ็กเกจอยู่รอดข้าม buildต้องใช้ BuildKit; cache อยู่บน builder ไม่ใช่ใน CI เว้นแต่ export
  • COPY . . ทั้งโปรเจกต์ก่อน RUN npm ci ทำให้ cache แตกทุกครั้งที่โค้ดเปลี่ยนทั้งที่ dependency ไม่เปลี่ยน
  • ไม่มี .dockerignore ทำให้ checksum ของ build context เปลี่ยนบ่อยกว่าที่จำเป็น (ไฟล์ .git หรือ log ที่เปลี่ยนทำให้ COPY . . แตก)
  • วาง instruction ที่เปลี่ยนบ่อย (เช่นคัดลอกซอร์ส) ไว้ด้านบน ทำให้ cache แตกกับ layer ด้านล่างที่ควรนิ่ง
  • ใส่ # comment ท้ายอาร์กิวเมนต์ของ COPY
  • เชื่อว่า --check ผ่านแปลว่า Dockerfile build ได้
  • “ทุก instruction ของ Dockerfile สร้าง layer” ทุก instruction เป็นขั้นตอนที่ cache ได้ แต่มีเพียง RUN, COPY และ ADD (และ WORKDIR ที่สร้างไดเรกทอรี) ที่เพิ่ม filesystem layer
  • “timestamp ของไฟล์ตัดสินว่า COPY ถูก cache หรือไม่” เนื้อหาตัดสิน: touch ทำให้ COPY ยัง CACHED; เว้นวรรคเพิ่มหนึ่งตัวทำให้ build ใหม่
  • “CACHED แปลว่าดาวน์โหลดจากที่ไหนสักแห่ง” หมายถึงใช้ผลของขั้นในเครื่องซ้ำ (เว้นแต่ตั้ง remote cache)
  • “cache ถูก invalidate เฉพาะขั้นที่เปลี่ยน” ทุกอย่างหลังมันก็รันใหม่ด้วย
เกิดอะไรขึ้นกับขั้นตอนทั้งหมดใต้ขั้นที่ถูก invalidate?
ทำไมใน Dockerfile ของ Node.js เรา COPY package.json ก่อน COPY . .?
instruction ใดต่อไปนี้สร้าง filesystem layer ใหม่ใน image?
docker build ครั้งที่สองกับซอร์สที่ไม่เปลี่ยนพิมพ์ CACHED ให้ส่วนใหญ่ หมายความว่าอะไร?
คุณรัน touch src/app.js (เนื้อหาไม่เปลี่ยน) แล้ว rebuild เกิดอะไรกับ COPY src/ ./src/?
COPY src/ ./src/ # copy the source ผ่าน docker buildx build --check แต่การ build ล้มด้วย "/the": not found เพราะอะไร?