เติมคอมเมนต์บรรทัดเดียวใน .dockerignore แล้วเสีย layer เท่ากับแก้ source ทั้ง file
คุณเปิด .dockerignore แล้วเติมบรรทัดเดียวเข้าไป เป็นคอมเมนต์ ไม่ได้ตัด file ไหนเพิ่ม
docker build รอบถัดไปเหลือขั้นที่ใช้ของเก่าได้ 2 ขั้น
จาก 6 ขั้น
ซึ่งเท่ากับตอนที่คุณแก้ source หนึ่งไบต์เป๊ะ
หน้านี้เอา Dockerfile สองใบที่ต่างกันแค่ลำดับของ COPY
ไปเจอการแก้ 7 แบบ แล้วนับว่าเหลือกี่ขั้นที่ BuildKit บอกว่า CACHED
ธีสิสของหน้านี้อยู่ตรงนี้ — สิ่งที่ตัดสินว่า cache หลุดหรือไม่ ไม่ใช่ว่าคุณแก้อะไร
แต่เป็นว่าสิ่งที่คุณแก้ อยู่ในขอบเขตที่ขั้นนั้นมองเห็นหรือเปล่า และขอบเขตนั้นกว้างกว่า
ที่คนส่วนใหญ่คิด มันรวม file ที่ไม่มีใครอ่าน และรวมตัว .dockerignore เอง
สอง Dockerfile ที่ต่างกันแค่ลำดับ
หัวข้อที่มีชื่อว่า “สอง Dockerfile ที่ต่างกันแค่ลำดับ”ใบหนึ่งคัดลอกทุกอย่างก่อน อีกใบคัดลอกประกาศ dependency ก่อน
FROM alpine:3.22WORKDIR /appCOPY marker.txt .COPY . .RUN cat deps.txt > /app/.deps-installedRUN cat src/main.txt > /app/.builtFROM alpine:3.22WORKDIR /appCOPY marker.txt .COPY deps.txt .RUN cat deps.txt > /app/.deps-installedCOPY . .RUN cat src/main.txt > /app/.builtRUN สองบรรทัดในนี้ไม่ได้ลง package จริง มันแค่อ่าน file แล้วเขียนผลลง image
เพราะสิ่งที่หน้านี้วัดคือ ขั้นไหนถูกทำใหม่ ไม่ใช่ว่าขั้นนั้นใช้เวลาเท่าไร
marker.txt ไม่ได้อยู่ในเรื่อง มันเป็นตัวแยก cache ของแต่ละทรงออกจากกัน
เหตุผลอยู่ในหัวข้อสุดท้าย และมันเป็นกับดักที่กินเวลาไปหนึ่งรอบ
ทุกค่าในหน้านี้มาจาก scripts/docker-cache/spike.py ที่สั่ง docker build จริง
บน Docker 29.4.0 กับ base image alpine:3.22
แต่ละทรง build สองครั้ง · ครั้งแรก --no-cache เพื่อให้จุดตั้งต้นเท่ากัน
แล้วแก้หนึ่งอย่าง แล้ว build อีกครั้ง
หน่วยของหลักฐานทั้งหน้าคือจำนวนขั้นที่ BuildKit รายงานว่า CACHED จำนวนขั้นทั้งหมด
และค่า boolean ว่า layer เป็นชุดเดิมไหม ไม่มีหน่วยเวลาสักช่อง
· probe รันชุดเดิม 2 รอบต่อการเรียกหนึ่งครั้ง และแดงถ้ามีค่าไหนขยับ
touch ไม่ทำให้ cache หลุดเลย
หัวข้อที่มีชื่อว่า “touch ไม่ทำให้ cache หลุดเลย”เปลี่ยนเวลาแก้ไขของ file ที่ถูก COPY โดยไม่แตะเนื้อในสักไบต์
| การแก้ | naive · ขั้นที่ CACHED | ordered · ขั้นที่ CACHED |
|---|---|---|
| ไม่แก้อะไรเลย | 5 จาก 6 | 6 จาก 7 |
touch อย่างเดียว | 5 จาก 6 | 6 จาก 7 |
สองแถวนี้ให้เลขเดียวกัน และ layer ที่ได้เป็นชุดเดิมทั้งคู่
BuildKit คิด cache key ของ COPY จากเนื้อในของ file ไม่ได้คิดจากเวลาแก้ไข
· การ touch เพื่อบังคับให้ build ใหม่ จึงไม่ทำอะไรเลย
ขั้นที่ไม่เคยขึ้นว่า CACHED คือขั้น FROM ซึ่งเป็นการดึง base image
ทุกแถวในหน้านี้จึงเทียบกับเพดานที่ 6 ลบหนึ่ง สำหรับ naive
แก้ source หนึ่งไบต์ แล้วลำดับของ COPY ตัดสินว่าเหลือกี่ขั้น
หัวข้อที่มีชื่อว่า “แก้ source หนึ่งไบต์ แล้วลำดับของ COPY ตัดสินว่าเหลือกี่ขั้น”Dockerfile สองใบนี้สร้าง image ที่ทำงานเหมือนกัน แต่ราคาของการแก้ต่างกัน
| การแก้ | naive | ordered |
|---|---|---|
| แก้ source หนึ่งไบต์ | 2 จาก 6 | 4 จาก 7 |
เติมหนึ่งบรรทัดใน deps.txt | 2 จาก 6 | 2 จาก 7 |
แถวแรกคือเหตุผลทั้งหมดที่คนแยก COPY ออกเป็นสองครั้ง
· ordered เก็บขั้นที่ลง dependency ไว้ได้ ส่วน naive ทิ้งมันทุกครั้งที่แก้ source
แถวที่สองคือราคาที่ ordered ไม่ได้ลด · เมื่อประกาศ dependency เปลี่ยนเอง
ทั้งสองใบเหลือเท่ากัน เพราะขั้นที่ลง dependency ต้องถูกทำใหม่จริง ๆ
ลำดับของ COPY ไม่ได้ทำให้ cache ดีขึ้น มันย้ายว่าอะไรจะพัง
และมันคุ้มเพราะ source เปลี่ยนบ่อยกว่าประกาศ dependency มาก
file ที่ไม่มีขั้นไหนอ่าน ก็ทำให้ cache หลุดเท่ากัน
หัวข้อที่มีชื่อว่า “file ที่ไม่มีขั้นไหนอ่าน ก็ทำให้ cache หลุดเท่ากัน”เพิ่ม
NOTES.mdเข้ามาหนึ่งใบ ไม่มีRUNบรรทัดไหนแตะมันเลย
| การแก้ | naive | ordered |
|---|---|---|
| แก้ source หนึ่งไบต์ | 2 | 4 |
| เพิ่ม file ที่ไม่มีใครอ่าน | 2 | 4 |
เพิ่ม file นั้น โดยมี .dockerignore ตัดไว้ | 5 | 6 |
สองแถวแรกให้เลขเดียวกัน · COPY . . ไม่รู้ว่าใครอ่าน file ไหน มันเห็นแค่ว่า
ชุด file ที่ต้องคัดลอกเปลี่ยนไป
แถวที่สามคือทางแก้ · .dockerignore ตัด file นั้นออกจากสิ่งที่ COPY . . มองเห็น
cache จึงรอดทั้งใบ และ layer ที่ได้เป็นชุดเดิม
นี่คือเหตุผลที่ .dockerignore เป็นเรื่องของความเร็ว ไม่ใช่แค่เรื่องขนาด image
แต่ตัว .dockerignore เอง เป็นรูใหม่
หัวข้อที่มีชื่อว่า “แต่ตัว .dockerignore เอง เป็นรูใหม่”เติมบรรทัดที่ขึ้นต้นด้วย
#ซึ่งไม่ตัด file ไหนเพิ่มสักใบ
| การแก้ | naive | ordered |
|---|---|---|
| แก้ source หนึ่งไบต์ | 2 | 4 |
เติมคอมเมนต์ใน .dockerignore | 2 | 4 |
สองแถวนี้ให้เลขเดียวกันทั้งสอง Dockerfile
ชุด file ที่ถูกคัดลอกไม่เปลี่ยนแม้แต่ใบเดียว สิ่งที่เปลี่ยนคือเนื้อในของ .dockerignore
ซึ่งเข้าไปอยู่ใน cache key ของขั้นที่คัดลอกทั้ง context
ผู้เขียน probe ทำนายไว้ก่อนวัดว่า cache จะไม่หลุด และคำทำนายนั้นผิด
· คำทำนายอยู่ที่ scripts/docker-cache/README-predictions.md พร้อมข้อที่ถูกหักล้าง
ผลในทางปฏิบัติคือ การจัดระเบียบ .dockerignore มีราคา และมันเป็นราคาเดียวกับ
การแก้ code ไม่ใช่ราคาของการแก้คอมเมนต์
—no-cache ไม่ได้ทำให้ทุกขั้นถูกทำใหม่
หัวข้อที่มีชื่อว่า “—no-cache ไม่ได้ทำให้ทุกขั้นถูกทำใหม่”build แรกของทุกทรงในหน้านี้ใช้ธงนั้น และมันยังเหลือหนึ่งขั้นที่ CACHED
| ทรง | ขั้นทั้งหมด | ขั้นที่ CACHED ตอนใส่ --no-cache |
|---|---|---|
| naive | 6 | 1 |
| ordered | 7 | 1 |
ขั้นนั้นคือ WORKDIR /app ซึ่งไม่ได้แตะ file สักใบ มันเปลี่ยนแค่ metadata ของ image
เรื่องเดียวกันนี้เป็นกับดักที่กินเวลาของ probe ไปหนึ่งรอบ
· ร่างแรกใช้ LABEL ที่ต่างกันต่อทรง เพื่อแยก cache ของแต่ละทรงออกจากกัน
มันแยกไม่ได้เลย เพราะขั้นของ metadata ไม่ได้เปลี่ยน cache key ของขั้นที่แตะ file ข้างหลังมัน · และ cache ของ BuildKit อยู่ข้ามการรัน ผลที่ได้จึงขึ้นกับว่ารอบก่อน build อะไรไว้ ไม่ได้ขึ้นกับสิ่งที่แก้
ตอนที่วิธีวัดยังพัง ทุกทรงพิมพ์ตัวเลขเดียวกันหมด
ซึ่งอ่านได้ว่า “การแก้แบบไหนก็ให้ผลเท่ากัน” ซึ่งเป็นข้อสรุปที่ฟังขึ้น แทนที่จะอ่านว่า “ตัวแยกไม่ทำงาน”
ตัวแยกที่ใช้ได้จริงต้องเป็น file ที่ถูก COPY และมีค่าไม่ซ้ำต่อการวัดหนึ่งครั้ง
· ไม่ใช่การไปลบ cache ของเครื่องผู้อ่าน
ตารางรวมทั้งสิบสี่ทรง
หัวข้อที่มีชื่อว่า “ตารางรวมทั้งสิบสี่ทรง”ขั้นที่ยังใช้ของเก่าได้ หลังการแก้แต่ละแบบ
| การแก้ | naive · จาก 6 ขั้น | ordered · จาก 7 ขั้น |
|---|---|---|
| ไม่แก้อะไรเลย | 5 | 6 |
| touch source โดยไม่แตะเนื้อใน | 5 | 6 |
| แก้ source หนึ่งไบต์ | 2 | 4 |
| เติมหนึ่งบรรทัดใน deps.txt | 2 | 2 |
| เพิ่ม file ที่ไม่มีขั้นไหนอ่าน | 2 | 4 |
| เพิ่ม file นั้น โดยมี .dockerignore ตัดไว้ | 5 | 6 |
| เติมคอมเมนต์ใน .dockerignore | 2 | 4 |
หนึ่ง touch ไม่ทำให้ cache หลุด · BuildKit ดูเนื้อในของ file ไม่ได้ดูเวลาแก้ไข
สอง ลำดับของ COPY ไม่ได้ทำให้ cache ดีขึ้น มันย้ายว่าอะไรจะพัง
และย้ายไปอยู่กับสิ่งที่เปลี่ยนไม่บ่อย
สาม COPY . . เห็นทุก file ที่ไม่ได้ถูกตัด รวม file ที่ไม่มีขั้นไหนอ่าน
สี่ .dockerignore ปิดรูนั้น แต่การแก้ตัวมันเอง ราคาเท่ากับการแก้ code
เช็กความเข้าใจ
ข้อ 1 / 3สั่ง touch ใส่ file ที่ถูก COPY เข้า image โดยไม่แก้เนื้อในเลย แล้ว docker build ใหม่ ผลเป็นอย่างไร
วิธีวัดซ้ำ
หัวข้อที่มีชื่อว่า “วิธีวัดซ้ำ”ทุกค่าในหน้านี้สร้างใหม่ได้จากคำสั่งสองบรรทัด
scripts/docker-cache/up.sh # ดึง base image ที่ตรึงไว้ · ต้องใช้เน็ตรอบแรกnpm run verify:docker-cache # วัด 14 ทรง แล้วเทียบกับข้ออ้างทุกข้อprobe สร้างแต่ image แล้วลบทุกใบที่มันสร้างเอง มันไม่สร้าง container สักตัว
และ ไม่ลบ cache ของเครื่องคุณ ทุกทรงแยกกันด้วยค่าไม่ซ้ำใน marker.txt
probe นี้อยู่นอก CI โดยตั้งใจ เพราะมันต้องมี Docker · รายละเอียดอยู่ใน CLAUDE.md
หัวข้อรายการที่กันออกจาก CI
รหัสออก 1 แปลว่าข้ออ้างในหน้านี้ผิด ส่วนรหัสออก 2 แปลว่าวัดไม่ได้ สองอย่างนี้ไม่เหมือนกัน