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

Layer Cache และการเรียงลำดับ Build

ทุก instruction ใน Dockerfile สร้าง layer หนึ่งอัน Docker เก็บแต่ละ layer ใน cache โดยใช้ข้อความ instruction และทุก layer ที่อยู่ก่อนหน้าเป็น key เมื่อรัน docker build อีกครั้ง Docker จะตรวจสอบแต่ละ instruction จากบนลงล่าง:

  • หากไม่มีอะไรเปลี่ยนแปลง Docker จะแสดง CACHED และนำ layer ที่เก็บไว้มาใช้ทันที
  • ทันทีที่ layer ใด layer หนึ่งเปลี่ยนแปลง Docker จะ invalidate ทุก layer ที่อยู่ต่ำกว่า และ rebuild จากจุดนั้น

ดังนั้น ลำดับของ instruction จึงส่งผลโดยตรงต่อความถี่ที่ต้องรอ rebuild เต็มรูปแบบ

วาง instruction ที่เปลี่ยนแปลง น้อย ไว้บนสุดของ Dockerfile และ instruction ที่เปลี่ยนแปลง บ่อย ไว้ล่างสุด Dependencies เปลี่ยนแปลงน้อยกว่า code application มาก

การเรียงลำดับที่ไม่ดี — cache bust ทุกครั้งที่ code เปลี่ยน

หัวข้อที่มีชื่อว่า “การเรียงลำดับที่ไม่ดี — cache bust ทุกครั้งที่ code เปลี่ยน”
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY . . # ← copy ทุกอย่างรวม source
RUN npm ci # ← ติดตั้ง deps ใหม่ทั้งหมดทุกครั้งที่ code เปลี่ยน
CMD ["node", "src/index.js"]

ทุกครั้งที่แก้ไขไฟล์ .js คำสั่ง COPY . . จะเปลี่ยน ทำให้ layer RUN npm ci ถูก invalidate npm ci จะรันตั้งแต่ต้นทุก build แม้ว่า package.json จะไม่ได้เปลี่ยนก็ตาม

# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./ # ← เฉพาะ manifest
RUN npm ci # ← cached ยกเว้น manifest เปลี่ยน
COPY src/ ./src/ # ← copy source หลัง install
CMD ["node", "src/index.js"]

ตอนนี้ npm ci จะ re-execute เฉพาะเมื่อ package.json หรือ package-lock.json เปลี่ยนแปลงจริงๆ การแก้ไข code เพียงอย่างเดียวจะข้ามไปที่ COPY src/ ทันทีและเสร็จภายในไม่กี่วินาที

แต่ละ instruction RUN คือ layer หนึ่งอัน การแยก command ที่เกี่ยวข้องออกเป็น RUN หลายบรรทัดหมายถึง layer มากขึ้นและ intermediate filesystem ที่ต้องเก็บมากขึ้น

# ไม่มีประสิทธิภาพ — สาม layer สำหรับการดำเนินการเดียว
RUN apt-get update
RUN apt-get install -y curl git
RUN 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 ได้จริง

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=type=cache เพื่อแชร์ cache directory ที่คงอยู่ระหว่าง build โดยไม่ต้องเก็บไว้ใน layer เหมาะสำหรับ package manager เป็นพิเศษ

# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY src/ ./src/
CMD ["node", "src/index.js"]

npm cache ที่ /root/.npm จะถูกเก็บไว้ระหว่าง build ใน BuildKit cache แยกต่างหาก ไม่ใช่ใน image layer การ install ครั้งต่อไปจะ resolve package จาก local cache แทนการดาวน์โหลดจาก network ซึ่งเร็วกว่ามากในการเชื่อมต่อที่ช้า

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
ตัวเลือกBenefitCost
เรียง 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

เกิดอะไรขึ้นกับทุก layer ที่อยู่ต่ำกว่า layer ที่เปลี่ยนแปลงใน Docker build?
ทำไมจึงควร COPY package.json ก่อน COPY src/ ใน Node.js Dockerfile?
ประโยชน์ของการรวม RUN หลาย command ด้วย && แทนที่จะเขียน RUN แยกกันคืออะไร?
`--mount=type=cache` ใน BuildKit RUN instruction ทำอะไร?