Layer Cache และการเรียงลำดับ Build
Docker layer cache ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “Docker layer cache ทำงานอย่างไร”ทุก instruction ใน Dockerfile สร้าง layer หนึ่งอัน Docker เก็บแต่ละ layer ใน cache โดยใช้ข้อความ instruction และทุก layer ที่อยู่ก่อนหน้าเป็น key เมื่อรัน docker build อีกครั้ง Docker จะตรวจสอบแต่ละ instruction จากบนลงล่าง:
- หากไม่มีอะไรเปลี่ยนแปลง Docker จะแสดง
CACHEDและนำ layer ที่เก็บไว้มาใช้ทันที - ทันทีที่ layer ใด layer หนึ่งเปลี่ยนแปลง Docker จะ invalidate ทุก layer ที่อยู่ต่ำกว่า และ rebuild จากจุดนั้น
ดังนั้น ลำดับของ instruction จึงส่งผลโดยตรงต่อความถี่ที่ต้องรอ rebuild เต็มรูปแบบ
กฎทอง: เรียงจาก stable ไป volatile
หัวข้อที่มีชื่อว่า “กฎทอง: เรียงจาก stable ไป volatile”วาง instruction ที่เปลี่ยนแปลง น้อย ไว้บนสุดของ Dockerfile และ instruction ที่เปลี่ยนแปลง บ่อย ไว้ล่างสุด Dependencies เปลี่ยนแปลงน้อยกว่า code application มาก
การเรียงลำดับที่ไม่ดี — cache bust ทุกครั้งที่ code เปลี่ยน
หัวข้อที่มีชื่อว่า “การเรียงลำดับที่ไม่ดี — cache bust ทุกครั้งที่ code เปลี่ยน”# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY . . # ← copy ทุกอย่างรวม sourceRUN npm ci # ← ติดตั้ง deps ใหม่ทั้งหมดทุกครั้งที่ code เปลี่ยนCMD ["node", "src/index.js"]ทุกครั้งที่แก้ไขไฟล์ .js คำสั่ง COPY . . จะเปลี่ยน ทำให้ layer RUN npm ci ถูก invalidate npm ci จะรันตั้งแต่ต้นทุก build แม้ว่า package.json จะไม่ได้เปลี่ยนก็ตาม
การเรียงลำดับที่ดี — dependencies cached แยกกัน
หัวข้อที่มีชื่อว่า “การเรียงลำดับที่ดี — dependencies cached แยกกัน”# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./ # ← เฉพาะ manifestRUN npm ci # ← cached ยกเว้น manifest เปลี่ยนCOPY src/ ./src/ # ← copy source หลัง installCMD ["node", "src/index.js"]ตอนนี้ npm ci จะ re-execute เฉพาะเมื่อ package.json หรือ package-lock.json เปลี่ยนแปลงจริงๆ การแก้ไข code เพียงอย่างเดียวจะข้ามไปที่ COPY src/ ทันทีและเสร็จภายในไม่กี่วินาที
รวม RUN step เพื่อลด layer
หัวข้อที่มีชื่อว่า “รวม RUN step เพื่อลด layer”แต่ละ instruction RUN คือ layer หนึ่งอัน การแยก command ที่เกี่ยวข้องออกเป็น RUN หลายบรรทัดหมายถึง layer มากขึ้นและ intermediate filesystem ที่ต้องเก็บมากขึ้น
# ไม่มีประสิทธิภาพ — สาม layer สำหรับการดำเนินการเดียวRUN apt-get updateRUN apt-get install -y curl gitRUN rm -rf /var/lib/apt/lists/*# มีประสิทธิภาพ — layer เดียว, cache entry เดียวRUN apt-get update \ && apt-get install -y curl git \ && rm -rf /var/lib/apt/lists/*รูปแบบที่รวมกันยังทำให้ rm -rf /var/lib/apt/lists/* รันใน layer เดียวกับการ install ซึ่งจะลบ cache bytes ออกจาก image ได้จริง
Pin version ของ package
หัวข้อที่มีชื่อว่า “Pin version ของ package”Version ที่ไม่ได้ pin ทำให้ build ไม่ reproducible และอาจทำให้เกิด cache bust ที่ไม่คาดคิดเมื่อมี version ใหม่ถูก publish
# ไม่ได้ pin — อาจได้ installation ที่ต่างกันในแต่ละวันRUN apt-get install -y curl
# Pin แล้ว — reproducible และชัดเจนRUN apt-get install -y curl=8.5.0-2สำหรับ npm นั้น package-lock.json (ที่ใช้โดย npm ci) จะ pin version ของทุก transitive dependency ไว้แล้ว — ควร commit ไว้เสมอ
BuildKit mount cache (ขั้นสูง)
หัวข้อที่มีชื่อว่า “BuildKit mount cache (ขั้นสูง)”BuildKit รองรับ --mount=type=cache เพื่อแชร์ cache directory ที่คงอยู่ระหว่าง build โดยไม่ต้องเก็บไว้ใน layer เหมาะสำหรับ package manager เป็นพิเศษ
# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm \ npm ciCOPY src/ ./src/CMD ["node", "src/index.js"]npm cache ที่ /root/.npm จะถูกเก็บไว้ระหว่าง build ใน BuildKit cache แยกต่างหาก ไม่ใช่ใน image layer การ install ครั้งต่อไปจะ resolve package จาก local cache แทนการดาวน์โหลดจาก network ซึ่งเร็วกว่ามากในการเชื่อมต่อที่ช้า
ลงมือทำ: เปรียบเทียบ cache
หัวข้อที่มีชื่อว่า “ลงมือทำ: เปรียบเทียบ cache”snippet ด้านล่าง build image อย่างง่ายสองครั้ง ครั้งแรก build จะดาวน์โหลด dependency ครั้งที่สองจะ hit cache สังเกตความแตกต่างของเวลา
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
# GOOD ordering: manifests before source
COPY package.json ./
RUN echo '{}' > package-lock.json && npm install --prefer-offline || true
# Source comes last — cache survives source-only edits
COPY . .
CMD ["node","-e","console.log('Cache-friendly build complete!')"]
# --- First build (cold) ---
# docker build -t myapp:cache .
#
# --- Edit a source file, then rebuild (warm cache) ---
# echo "// change" >> index.js
# docker build -t myapp:cache .
# Notice: "npm install" step shows CACHED on the second runข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| เรียง instruction จากเปลี่ยนน้อยไปมาก (manifest ก่อน source) | cache hit บ่อยขึ้นมาก build รอบถัดไปเร็วขึ้นมากในการพัฒนาต่อเนื่อง | ต้องแยก COPY manifest ออกจาก COPY source อย่างตั้งใจ เพิ่มบรรทัดใน Dockerfile |
รวมหลาย RUN เป็น command เดียวด้วย && | layer น้อยลง cache entry เดียวต่อ operation cleanup มีผลจริง | debug ยากขึ้นเพราะไม่มี layer ย่อยให้ inspect ทีละขั้นตอน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- วาง
COPY . .ไว้ก่อนRUN npm ciทำให้ dependency ถูกติดตั้งใหม่ทุกครั้งที่ code เปลี่ยน แม้package.jsonจะไม่เปลี่ยนเลยก็ตาม - Pin base image ด้วย tag ลอยอย่าง
latestแทนที่จะ pin version ที่ชัดเจน ทำให้ cache และผลลัพธ์ build ไม่ reproducible ระหว่างเครื่อง - แยก
RUN apt-get installกับRUN rm -rf /var/lib/apt/lists/*เป็นคนละ layer ทำให้ cleanup ไม่ช่วยลดขนาด image จริง เพราะ byte ถูก commit ไปแล้วในอีก layer
💡 ตัวอย่างจากของจริง
Pipeline CI ที่ build image หลายร้อยครั้งต่อวัน (เช่น monorepo ขนาดใหญ่บน GitLab CI) จัดลำดับ Dockerfile ให้ dependency manifest อยู่บนสุดเสมอ เพราะ cache hit ที่ layer install เพียงจุดเดียวสามารถประหยัดเวลา build รวมได้หลายชั่วโมงต่อวันเมื่อคูณด้วยจำนวนรอบ build