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 คือขั้นที่ใช้เวลาหลายสิบวินาที
Build cache ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “Build cache ทำงานอย่างไร”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
ทำไมลำดับ instruction จึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไมลำดับ instruction จึงสำคัญ”โปรเจกต์ที่ใช้วัดทุกอย่างด้านล่าง (package.json ที่มี dependency เดียว, lockfile ของมัน และไฟล์ซอร์สหนึ่งไฟล์):
{ "name": "cachedemo", "version": "1.0.0", "private": true, "dependencies": { "ms": "2.1.3" }}{ "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" } }}console.log("v1")ก่อนอื่น Dockerfile ที่เรียงลำดับไม่ดี:
# @buildFROM node:24-alpineWORKDIR /app# BAD: copies all source code firstCOPY . .# runs after COPY, so it re-runs on every code changeRUN npm ci --omit=devCMD ["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 ก่อน ติดตั้ง แล้วค่อยคัดลอกซอร์ส:
# @buildFROM node:24-alpineWORKDIR /app# only changes when dependencies changeCOPY package.json package-lock.json ./# cached as long as the manifests are unchangedRUN npm ci --omit=dev# source changes only invalidate this last COPYCOPY 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” ในคอร์สนี้)
ดู cache ทำงานจริง
หัวข้อที่มีชื่อว่า “ดู cache ทำงานจริง”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 จาก เปลี่ยนน้อยที่สุด ไป เปลี่ยนบ่อยที่สุด:
FROM(base image — เปลี่ยนน้อย)- ติดตั้งแพ็กเกจระบบ (
RUN apt-get install ...) - คัดลอก manifest ของ dependency แล้วติดตั้ง (
COPY package.json ./+RUN npm ci) - โค้ดแอป (
COPY . .) - ขั้นตอน build ที่ขึ้นกับซอร์ส (
RUN npm run build)
Cache mount: เก็บ cache ของ package manager ไว้ข้าม build
หัวข้อที่มีชื่อว่า “Cache mount: เก็บ cache ของ package manager ไว้ข้าม build”layer cache เป็นแบบทั้งหมดหรือไม่มีเลยต่อขั้น: เปลี่ยน package.json แล้ว npm ci ดาวน์โหลดทุกอย่างใหม่ cache mount เก็บไดเรกทอรีไว้นอก image ระหว่าง build ทำให้ cache การดาวน์โหลดอยู่รอดแม้ขั้นตอนนั้นรันใหม่:
# @build# syntax=docker/dockerfile:1FROM node:24-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm npm ci --omit=devCMD ["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)
ลงมือทำ: สังเกต cache
หัวข้อที่มีชื่อว่า “ลงมือทำ: สังเกต cache”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 .Reading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”Dockerfile ที่ AI เขียนพร้อม comment ช่วยอธิบายทุกบรรทัด:
🐛 หา bug ในโค้ดนี้
FROM alpine:3.24WORKDIR /appCOPY src/ ./src/ # copy the application sourceCMD ["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 foundERROR: 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 ในบรรทัดของตัวเอง:
# @buildFROM alpine:3.24WORKDIR /app# copy the application sourceCOPY src/ ./src/CMD ["ls", "src"]กฎเดียวกันใช้กับ RUN: # comment ท้ายบรรทัดกลายเป็นส่วนของคำสั่ง shell (ไม่เป็นอันตรายเพราะ shell ถือว่าเป็น comment แต่ใช้กับ COPY, ENV หรือ EXPOSE ไม่ได้)
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”build คือ ห่วงโซ่ของฟังก์ชันที่จำผลไว้ (memoised): แต่ละขั้นมี key เป็น (ผลของขั้นแม่, instruction, เนื้อหาไฟล์ input) เปลี่ยน input ใดก็ตาม ขั้นนั้นและทุกอย่างด้านล่างต้องคำนวณใหม่; ด้านบนใช้ซ้ำ การเรียง Dockerfile คือการตัดสินใจว่า input ใดมีสิทธิ์ทำให้งานส่วนไหนเป็นโมฆะ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
COPY แยกตามกลุ่ม (manifest ก่อน แล้วซอร์ส) | cache แม่นยำ ใช้ซ้ำสูงสุด (ขั้น npm ci ค้างที่ CACHED) | ต้องเขียน Dockerfile ให้ระวังลำดับ instruction มากขึ้น |
COPY . . ครั้งเดียว | เขียนง่าย Dockerfile สั้น | cache แตกง่าย — ทุกการแก้โค้ดทำให้ npm ci รันใหม่ |
--no-cache | รับประกัน build สะอาดตั้งแต่ต้น | ทิ้งการใช้ซ้ำทั้งหมด ช้า |
| Cache mount | การดาวน์โหลดแพ็กเกจอยู่รอดข้าม build | ต้องใช้ BuildKit; cache อยู่บน builder ไม่ใช่ใน CI เว้นแต่ export |
Common mistakes
หัวข้อที่มีชื่อว่า “Common mistakes”COPY . .ทั้งโปรเจกต์ก่อนRUN npm ciทำให้ cache แตกทุกครั้งที่โค้ดเปลี่ยนทั้งที่ dependency ไม่เปลี่ยน- ไม่มี
.dockerignoreทำให้ checksum ของ build context เปลี่ยนบ่อยกว่าที่จำเป็น (ไฟล์.gitหรือ log ที่เปลี่ยนทำให้COPY . .แตก) - วาง instruction ที่เปลี่ยนบ่อย (เช่นคัดลอกซอร์ส) ไว้ด้านบน ทำให้ cache แตกกับ layer ด้านล่างที่ควรนิ่ง
- ใส่
# commentท้ายอาร์กิวเมนต์ของCOPY - เชื่อว่า
--checkผ่านแปลว่า Dockerfile build ได้
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ทุก instruction ของ Dockerfile สร้าง layer” ทุก instruction เป็นขั้นตอนที่ cache ได้ แต่มีเพียง
RUN,COPYและADD(และWORKDIRที่สร้างไดเรกทอรี) ที่เพิ่ม filesystem layer - “timestamp ของไฟล์ตัดสินว่า
COPYถูก cache หรือไม่” เนื้อหาตัดสิน:touchทำให้COPYยังCACHED; เว้นวรรคเพิ่มหนึ่งตัวทำให้ build ใหม่ - “
CACHEDแปลว่าดาวน์โหลดจากที่ไหนสักแห่ง” หมายถึงใช้ผลของขั้นในเครื่องซ้ำ (เว้นแต่ตั้ง remote cache) - “cache ถูก invalidate เฉพาะขั้นที่เปลี่ยน” ทุกอย่างหลังมันก็รันใหม่ด้วย