บทที่ 6 Docker: บรรจุแอปลง container ด้วย Dockerfile และ multi-stage build
แผนบริหารการสอนประจำบทที่ 6
หัวข้อเนื้อหา
- 6.1 ปัญหา "works on my machine" และแนวคิด container
- 6.2 สถาปัตยกรรม Docker Engine: client, daemon และ registry
- 6.3 image, container และ layer
- 6.4 Dockerfile และ build cache
- 6.5
.dockerignoreและขนาดของ image - 6.6 multi-stage build
- 6.7 image, container และข้อมูลที่เปลี่ยนระหว่างรัน
- 6.8 build context กับการส่งไฟล์เกินจำเป็น
- 6.9 cache ที่เร็วขึ้นและ cache ที่ทำให้เข้าใจผิด
- 6.10 multi-stage build กับขอบเขตของผลที่คัดลอก
- ปฏิบัติการที่ 6 บรรจุแอปทีมลง container (Lab A–D)
วัตถุประสงค์เชิงพฤติกรรม
เมื่อศึกษาบทนี้และทำปฏิบัติการครบถ้วนแล้ว ผู้เรียนสามารถปฏิบัติได้ตามผลลัพธ์การเรียนรู้ต่อไปนี้ โดยแต่ละข้อมีงานที่ใช้วัดผลกำกับไว้
| รหัส | ผลลัพธ์การเรียนรู้ | วัดจาก | CLO ของรายวิชา |
|---|---|---|---|
| 6.1 | เขียน Dockerfile จากแม่แบบให้ build image ของแอปและรัน container ที่ตอบ HTTP ได้ | งานข้อ 1 | CLO2 |
| 6.2 | อธิบายว่า image, container และ layer ต่างกันอย่างไร และเหตุใดแอปของตนจึงควรรันใน container | งานข้อ 2 | CLO1 |
| เตรียมงานต่อยอด | เสนอหัวข้องานต่อยอดที่เรียนด้วยตนเองนอกเนื้อหาหลัก พร้อมแหล่งข้อมูลที่เชื่อถือได้ 2 แหล่ง | งานข้อ 3 | CLO5 (จุดตรวจที่ 1) |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO1 สามารถนำความต้องการและสถาปัตยกรรมการส่งมอบซอฟต์แวร์ที่กำหนดมาใช้ โดยอธิบาย Git workflow, CI/CD pipeline flow และการใช้ Containerization ได้อย่างเหมาะสมกับบริบทของงาน
- CLO2 สามารถกำหนดค่าและใช้งาน DevOps workflow ที่กำหนดมา เพื่อให้กระบวนการพัฒนา–ทดสอบ–ส่งมอบซอฟต์แวร์เป็นไปอย่างถูกต้องและมีประสิทธิภาพ
- CLO5 แสวงหาความรู้เพิ่มเติมและสร้างสรรค์ผลงานจากการเรียนรู้ด้วยตัวเอง
บทนี้เป็นบทแรกของงานต่อยอดด้วยตนเอง (CLO5) ซึ่งมีจุดตรวจ 3 ครั้งในบทที่ 6, 10 และ 13 จุดตรวจครั้งนี้ประเมินเพียงหัวข้อและแหล่งข้อมูลเพื่อให้ข้อเสนอแนะ ไม่มีคะแนน คะแนนทั้งหมดของงานต่อยอดให้ที่บทที่ 13 และต้องผ่านจุดตรวจครบทั้ง 3 ครั้ง
วิธีสอนและกิจกรรมการเรียนการสอน
- บรรยายแนวคิดตามหัวข้อ 6.1–6.6 ประกอบตัวอย่างที่ซับซ้อนขึ้นตามลำดับ และใช้คำถามตรวจความเข้าใจท้ายหัวข้อเป็นกิจกรรมอภิปรายเป็นคู่
- สาธิตการ build image ของแอปสองครั้ง โดยครั้งที่สองแก้เฉพาะไฟล์ใน
src/ให้เห็น layer ที่ใช้ cache - ให้ผู้เรียนอ่านผล
docker historyแล้วชี้บรรทัดของ Dockerfile ที่สร้าง layer ใหญ่ - ปฏิบัติการ Lab A–D บน repository ทีม แต่ละ Lab ประกอบด้วยส่วนฝึกตามตัวอย่างและส่วนที่ผู้เรียนต้องตัดสินใจเอง
- แนะนำงานต่อยอดด้วยตนเอง และมอบหมายใบงาน CLO1 แบบฝึกหัดท้ายบท Learning Log ครั้งที่ 6 และการศึกษาค้นคว้าเพิ่มเติม
สื่อการเรียนการสอน
- เอกสารประกอบการสอนบทที่ 6 (บทความนี้) และหน้ารวมรายวิชา 10306343
- เครื่อง Ubuntu 24.04 ที่ติดตั้ง Docker Engine แล้ว และ repository ทีม
devops-appที่ผ่าน M4 จากบทที่ 5 - เอกสารทางการ Docker Docs หัวข้อ What is Docker?, Storage drivers และ Multi-stage builds
- (ผู้สอน) Open Container Initiative: OCI Image Format Specification
การวัดผลและการประเมินผล
กำหนดส่ง: ก่อนเริ่มเรียนบทที่ 8 เพื่อเว้นระยะหนึ่งบท ยกเว้นงานข้อ 3 (หัวข้องานต่อยอด) ส่งก่อนเริ่มเรียนบทที่ 7 เพื่อให้ผู้สอนให้ข้อเสนอแนะได้ทัน
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | M5 container ของแอปทีม | ลิงก์ pull request ของตนเองที่ merge แล้ว (Lab B, C หรือ D) และผลรันของตนเองตั้งแต่ docker build จนถึง curl ที่ได้ HTTP 200 จาก container ที่ build จาก main | 6.1 | CLO2 |
| 2 | ใบงาน image, container และ layer (CLO1 ชิ้นที่ 3) | คำอธิบายรายบุคคลไม่เกิน 1 หน้า โดยอ้างผล docker history และ docker ps ของ image ทีม | 6.2 | CLO1 |
| 3 | หัวข้องานต่อยอด (CLO5 จุดตรวจที่ 1) | หัวข้อ เหตุผลที่เลือก สิ่งที่จะสร้างหรือสาธิตในบทที่ 13 และแหล่งข้อมูลที่เชื่อถือได้ 2 แหล่ง (อย่างน้อย 1 แหล่งเป็นเอกสารทางการ) | เตรียมงานต่อยอด | CLO5 |
| 4 | Learning Log ครั้งที่ 6 | ตอบ 4 หัวข้อ: สิ่งที่เรียนในบทนี้ · คำสั่งหรือข้อความ error ที่พบและแก้อย่างไร · แหล่งที่ใช้ค้นหาคำตอบ (ระบุลิงก์เอกสารทางการอย่างน้อย 1 แหล่ง) · ส่วนใดของ Dockerfile ที่ยังไม่เข้าใจ | — | บันทึกสะท้อนการเรียนรู้ ไม่มีหมวดคะแนน portfolio แยก |
เกณฑ์ให้คะแนน
| ข้อ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| 1 | main มี Dockerfile แบบ multi-stage และ .dockerignore · pull request ของตนผ่าน CI และ review · ผล curl จาก container ได้ HTTP 200 และแสดงเวอร์ชันของแอป | ไม่มี pull request ของตนเอง หรือ container ไม่ตอบ หรือผลรันมาจาก npm start บนเครื่องแทน container |
| 3 | หัวข้ออยู่นอกเนื้อหาหลักของรายวิชา มีสิ่งที่จะสาธิตชัดเจน และมีแหล่งข้อมูลที่เชื่อถือได้ 2 แหล่ง | หัวข้อซ้ำกับ Lab ในรายวิชา หรือไม่มีแหล่งข้อมูล |
| 4 | ตอบครบ 4 หัวข้อ และมีลิงก์เอกสารทางการ | ขาดหัวข้อ หรือไม่มีแหล่งอ้างอิง |
ข้อ 2 ใช้ rubric CLO1 3 ระดับ
| ระดับ | ลักษณะของคำตอบ |
|---|---|
| 3 ดี | อธิบายความต่างและความสัมพันธ์ของ image, container และ layer ถูกต้องโดยอ้างผลจริงของทีม ให้เหตุผลการใช้ container ที่โยงกับบริบทของแอป และเทียบกับทางเลือกอื่นอย่างน้อย 1 ทาง (เช่น ติดตั้ง Node.js บนเครื่องปลายทางโดยตรง หรือ virtual machine) พร้อมข้อแลกเปลี่ยน |
| 2 ผ่าน | อธิบายความต่างของ image, container และ layer ถูกต้อง ชี้ได้ว่าบรรทัดใดใน Dockerfile สร้าง layer ใดจากผล docker history และให้เหตุผลการใช้ container ที่โยงกับแอปของทีม |
| 1 ยังไม่ผ่าน | ท่องนิยามโดยไม่อ้างผลของทีม หรือสับสนระหว่าง image กับ container |
ข้อ 3 ใช้ rubric CLO5 (จุดตรวจที่ 1 ประเมินเฉพาะการวางแผน)
| ระดับ | ลักษณะ |
|---|---|
| 3 ดี | หัวข้อมีส่วนที่ปรับเองให้เข้ากับแอปของทีม และระบุได้ว่าจะสอนเพื่อนในขั้นใด |
| 2 ผ่าน | หัวข้อใช้กับแอปเดียวกันได้ อธิบายแหล่งข้อมูลได้ และมีแผนตามจุดตรวจทั้ง 3 ครั้ง |
| 1 ยังไม่ผ่าน | ไม่มีหัวข้อ หรือคัดลอกหัวข้อและแผนจากผู้อื่น |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
การคิดคะแนนฉบับปัจจุบัน
มติผู้สอน 28 ต.ค. 2569: เข้าเรียน 10% · ใบงานบทที่ 1–13 รวม 40% · FP 20% · FW 20% · โครงงาน 10% ส่วนบทที่ 14–15 ฝึกเพื่อรับข้อเสนอแนะ ไม่นับคะแนน
ใบงาน C06-WS01 มีคะแนนดิบเต็ม 20 คะแนน แยกตามผลลัพธ์ด้านล่าง ระบบนำไปรวมในหมวดใบงาน 40% โดยไม่เพิ่มคะแนน milestone, portfolio หรือจุดตรวจเป็นหมวดแยก
| ผลลัพธ์ | หลักฐานตามงานส่ง | คะแนนดิบเต็ม |
|---|---|---|
| LLO6.1 | งานข้อ 1 | 10 |
| LLO6.2 | งานข้อ 2 | 10 |
ประเมินงานแต่ละข้อจากหลักฐานของข้อนั้นโดยตรง ไม่ตัดสิทธิ์รับคะแนนของอีกข้อเพราะ milestone หรือจุดตรวจอื่นยังไม่ผ่าน งานที่ยังทำไม่ได้ให้บันทึกปัญหาและหลักฐานที่มีตามจริง
ใช้เกณฑ์ความถูกต้อง เหตุผล และหลักฐานที่ระบุในงานแต่ละข้อประกอบการประเมินราย LLO งานเตรียมพื้นและ Learning Log ที่ไม่มีรหัส LLO ใช้ให้ข้อเสนอแนะ ไม่เพิ่มคะแนนดิบให้ใบงาน
บทนำ
บทนี้นำแอปของทีมซึ่ง build และ test ได้อัตโนมัติแล้วในบทที่ 4–5 มาบรรจุลงใน container ด้วย Docker เพื่อให้สิ่งที่ส่งมอบไม่ใช่แค่ซอร์สโค้ด แต่เป็น image ที่มีทั้งแอป runtime และ dependency อยู่ในตัว เนื้อหาเริ่มจากปัญหาที่ container แก้ สถาปัตยกรรมของ Docker Engine ความหมายของ image, container และ layer การเขียน Dockerfile ให้ใช้ build cache ได้ดี การลดขนาดและความเสี่ยงของ image ด้วย .dockerignore และการใช้ multi-stage build แยกขั้น test ออกจาก image ที่ใช้รันจริง
ความสำคัญ: หลัก build, release, run ในบทที่ 4 ต้องการสิ่งที่ build แล้วแก้ไขไม่ได้และระบุรุ่นได้ container image คือรูปแบบของสิ่งนั้นที่อุตสาหกรรมใช้ บทที่ 7 จะรัน image นี้คู่กับฐานข้อมูล บทที่ 8 จะส่ง image ขึ้น registry แล้ว deploy ไปยังเครื่องปลายทาง และบทที่ 10 จะสแกนช่องโหว่ของ image ความถูกต้องของ Dockerfile ในบทนี้จึงส่งผลต่อทุกบทที่เหลือ
ความเชื่อมโยงกับบทอื่น: บทนี้ใช้แนวคิด process, port และไฟล์จากบทที่ 1 คำสั่ง npm ci และหลัก immutable artifact จากบทที่ 4 และ required status check จากบทที่ 5 ผลผลิตของบทนี้ถูกใช้ต่อตามตารางต่อไปนี้
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
Dockerfile แบบ multi-stage | 7 (บริการแอปใน Compose) · 8 (build และ push ใน workflow CD) · 10 (สแกน image) |
.dockerignore | 9 และ 10 (กันไฟล์ .env และความลับไม่ให้เข้า image) |
image devops-app พร้อม tag เวอร์ชัน | 8 (tag ตามเวอร์ชันบน ghcr.io) · 12 (rollback ด้วย tag ก่อนหน้า) |
| หัวข้องานต่อยอด | จุดตรวจที่ 2 ในบทที่ 10 และการสาธิตในบทที่ 13 |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
ความรู้พื้นฐาน: process, port และสิทธิ์ของไฟล์จากบทที่ 1 คำสั่ง npm ci และ npm test จากบทที่ 4 และการทำงานผ่าน pull request ที่ต้องผ่าน CI จากบทที่ 5
เครื่องมือ: เครื่อง Ubuntu ที่ติดตั้ง Docker Engine แล้ว ตรวจได้ด้วย docker version ซึ่งต้องแสดงทั้งส่วน Client และ Server และผู้ใช้ต้องสั่ง docker ได้โดยไม่ใช้ sudo (ผู้ดูแลระบบเพิ่มผู้ใช้เข้ากลุ่ม docker แล้วผู้ใช้ออกจากระบบและเข้าใหม่) รวมถึงโฟลเดอร์ ~/devops-app ที่ตรงกับ main ล่าสุด
รุ่นซอฟต์แวร์ที่ใช้ทดสอบ:
| ซอฟต์แวร์ | รุ่นที่ทดสอบ |
|---|---|
| Docker Engine (Client และ Server) | 29.8.2 พร้อม containerd 2.3.6 ทดสอบเมื่อ 19 ต.ค. 2569 |
| base image | image จำลองชื่อ node:22-slim ที่ผู้เขียนสร้างจาก Node.js 22.22.0 และ npm 10.9.4 บนเครื่องผู้เขียน ไม่ใช่ image ทางการจาก Docker Hub |
| แอป | devops-app เวอร์ชัน 0.2.0 จากบทที่ 4–5 |
สภาพแวดล้อมที่ใช้เขียนบทความดึง image จาก Docker Hub ไม่ได้ ผู้เขียนจึงสร้าง base image จำลองที่มีชื่อ tag เดียวกับ image ทางการ (node:22-slim) จากไฟล์ของ Node.js บนเครื่อง เพื่อทดสอบ Dockerfile, build cache, .dockerignore, multi-stage build และการรัน container ได้จริง ผลที่เกี่ยวกับขนาดของ image และ layer ของ base image (เช่น 150MB และ 212MB) จึงต่างจาก image ทางการ ส่วนขนาดของ layer ที่ Dockerfile ของแอปสร้างเอง (เช่น npm ci 9.6MB) และพฤติกรรมของคำสั่งต่าง ๆ ตรงกับการใช้งานจริง ผู้เรียนจะเห็นขนาดและเลข digest ต่างจากตัวอย่าง
ค่าใช้จ่ายและข้อมูลลับ: Docker Engine บน Linux ใช้ได้โดยไม่มีค่าใช้จ่าย การดึง image สาธารณะจาก Docker Hub แบบไม่ล็อกอินมีการจำกัดจำนวนครั้งต่อช่วงเวลา ซึ่งอาจกระทบห้องปฏิบัติการที่ใช้ IP เดียวกันหลายเครื่อง ประเด็นด้านความลับที่สำคัญที่สุดของบทนี้คือ ไฟล์ทุกไฟล์ที่ถูก COPY เข้า image จะอยู่ใน layer ของ image ตลอดไป แม้คำสั่งถัดไปจะลบไฟล์นั้น ไฟล์ .env และความลับอื่นจึงต้องถูกกันด้วย .dockerignore ตั้งแต่ก่อน build ครั้งแรก
6.1 ปัญหา "works on my machine" และแนวคิด container
แอปหนึ่งตัวทำงานได้เมื่อสามสิ่งตรงกัน คือ โค้ดของแอป runtime ที่รันโค้ด (สำหรับแอปของรายวิชาคือ Node.js) และ dependency ที่แอปเรียกใช้ บทที่ 4 ทำให้ dependency ตรงกันด้วย lock file แต่รุ่นของ Node.js และสภาพของระบบปฏิบัติการบนเครื่องปลายทางยังไม่อยู่ในการควบคุมของทีม ปัญหาที่รู้จักกันในชื่อ "works on my machine" หรือรันได้บนเครื่องผู้พัฒนาเท่านั้น จึงยังเกิดได้
container แก้ปัญหานี้ด้วยการบรรจุแอปพร้อม runtime และ dependency ลงในหน่วยเดียวที่รันได้เหมือนกันบนเครื่องใดก็ได้ที่มี container runtime เอกสารของ Docker อธิบายว่า Docker ใช้ความสามารถ namespaces ของ Linux แยกสภาพแวดล้อมของ container แต่ละตัว (Docker Docs: What is Docker?)
ระดับที่ 1: container ไม่ใช่ virtual machine container ทุกตัวบนเครื่องเดียวกันใช้ kernel ของระบบปฏิบัติการเครื่องนั้นร่วมกัน สิ่งที่แยกคือมุมมองของ process ต่อ filesystem, เครือข่าย และ process อื่น จึงเริ่มทำงานได้เร็วกว่าการบูต virtual machine
ระดับที่ 2: ผลการทดสอบในบทนี้แสดงว่าคำสั่ง node -e "console.log(require('os').hostname())" ที่รันใน container พิมพ์ชื่อเครื่องเป็นรหัสสุ่มเช่น 956b4938d266 ซึ่งตรงกับ CONTAINER ID ไม่ใช่ชื่อเครื่องจริง แสดงว่า process ใน container เห็นสภาพแวดล้อมที่แยกจากเครื่องหลัก
ระดับที่ 3: container ไม่ได้ทำให้แอปถูกต้องขึ้นเอง หากโค้ดมี bug container จะบรรจุ bug นั้นไปด้วยอย่างซื่อสัตย์ ประโยชน์ของ container คือความ "เหมือนเดิม" ระหว่างเครื่อง ส่วนความ "ถูกต้อง" ยังเป็นหน้าที่ของ test และ CI ซึ่งเป็นเหตุผลที่หัวข้อ 6.6 ให้ npm test รันระหว่าง build image ด้วย
คำถามตรวจความเข้าใจ 6.1: เครื่องปลายทางของทีมติดตั้ง Node.js รุ่น 18 ส่วนแอปทดสอบบนรุ่น 22 อธิบายว่าการส่งมอบเป็น image ช่วยแก้ปัญหานี้อย่างไร และเครื่องปลายทางยังต้องติดตั้งอะไร
6.2 สถาปัตยกรรม Docker Engine: client, daemon และ registry
Docker แบ่งการทำงานเป็นสามส่วน (Docker Docs: What is Docker?)
| ส่วน | หน้าที่ | ตัวอย่างที่เห็นในบทนี้ |
|---|---|---|
client (docker) | รับคำสั่งจากผู้ใช้แล้วส่งให้ daemon | docker build, docker run, docker ps |
daemon (dockerd) | build image, จัดการและรัน container | ส่วน Server ในผล docker version |
| registry | เก็บและแจกจ่าย image โดยมี Docker Hub เป็นค่าเริ่มต้น | ที่มาของ node:22-slim · บทที่ 8 ใช้ ghcr.io |
ระดับที่ 1: ผล docker version แยกเป็นสองส่วน คือ Client และ Server ถ้าเห็นเฉพาะ Client พร้อมข้อความ Cannot connect to the Docker daemon แสดงว่า daemon ไม่ได้ทำงาน คำสั่งใดที่ต้องให้ daemon ทำงานจะล้มทั้งหมด
ระดับที่ 2: คำสั่ง docker run กับ image ที่ยังไม่มีในเครื่องจะทำให้ daemon ดึง image จาก registry ก่อนโดยอัตโนมัติ เมื่อดึงไม่ได้จะได้ข้อความ error จาก daemon เช่นผลที่พบระหว่างเตรียมบทความ
Error response from daemon: failed to resolve reference "docker.io/library/node:22-alpine": failed to do request: Head "https://registry-1.docker.io/v2/library/node/manifests/22-alpine": Forbiddenชื่อ node:22-alpine แบบย่อขยายเป็น docker.io/library/node:22-alpine คือ registry Docker Hub, namespace library (image ทางการ), ชื่อ node และ tag 22-alpine
ระดับที่ 3: image ที่ build ด้วย Docker ใช้กับ registry และ runtime ของผู้ผลิตรายอื่นได้ เพราะ Open Container Initiative (OCI) กำหนดมาตรฐานของ image manifest, filesystem layer และ configuration ไว้ร่วมกัน (OCI Image Format Specification) บทที่ 8 จึงส่ง image เดียวกันขึ้น ghcr.io ได้โดยไม่ต้องแปลงรูปแบบ
คำถามตรวจความเข้าใจ 6.2: สมาชิกคนหนึ่งสั่ง docker ps แล้วได้ข้อความ Cannot connect to the Docker daemon at unix:///var/run/docker.sock ระบุว่าส่วนใดของสถาปัตยกรรมมีปัญหา และส่วนใดยังทำงานได้
6.3 image, container และ layer
Docker Docs นิยามว่า "An image is a read-only template" และ "A container is a runnable instance of an image" (Docker Docs: What is Docker?) ความสัมพันธ์ของทั้งสองคล้ายกับคลาสและ object ในการเขียนโปรแกรมเชิงวัตถุ image หนึ่งตัวสร้าง container ได้หลายตัว
image ประกอบด้วย layer ซ้อนกัน คำสั่งใน Dockerfile ที่แก้ไข filesystem จะสร้าง layer ใหม่ เมื่อรัน container Docker เพิ่ม writable layer บาง ๆ ไว้บนสุด การเขียนไฟล์ใน container เกิดใน layer นี้ด้วยกลไก copy-on-write และ writable layer จะหายไปเมื่อลบ container (Docker Docs: Storage drivers)
ผล docker history ของ image แอปแบบ stage เดียวในห้องทดสอบ (base image จำลอง)
CREATED BY SIZE
CMD ["node" "src/server.js"] 0B
EXPOSE [3000/tcp] 0B
COPY . . # buildkit 49.2kB
RUN /bin/sh -c npm ci --omit=dev # buildkit 9.6MB
COPY package.json package-lock.json ./ # bui… 45.1kB
WORKDIR /app 4.1kB
150MBผลอ่านจากล่างขึ้นบน บรรทัดล่างสุดคือ base image บรรทัด RUN npm ci สร้าง layer ใหญ่ที่สุดของแอปเพราะติดตั้ง node_modules ส่วน CMD และ EXPOSE เป็นเพียง metadata จึงมีขนาด 0B
ระดับที่ 1: image ไม่เปลี่ยนเมื่อรัน container การแก้ไฟล์ใน container ที่กำลังรันไม่ทำให้ image เปลี่ยน และ container ใหม่ที่สร้างจาก image เดิมจะไม่เห็นการแก้นั้น
ระดับที่ 2: ข้อมูลที่ต้องคงอยู่ เช่น ข้อมูลของฐานข้อมูล ห้ามเก็บไว้ใน writable layer เพราะหายเมื่อลบ container บทที่ 7 จึงใช้ volume สำหรับข้อมูลเหล่านี้
ระดับที่ 3: Docker Engine รุ่น 29.0 ขึ้นไปที่ติดตั้งใหม่ใช้ containerd image store เป็นค่าเริ่มต้น (Docker Docs: Storage drivers) ผล docker info ของเครื่องทดสอบแสดง Storage Driver: overlayfs และ driver-type: io.containerd.snapshotter.v1 เครื่องในห้องปฏิบัติการที่อัปเกรดจากรุ่นเก่าอาจแสดงต่างไป แต่แนวคิด layer และ copy-on-write เหมือนกัน
คำถามตรวจความเข้าใจ 6.3: สมาชิกคนหนึ่งเข้าไปใน container ที่กำลังรันแล้วแก้ไฟล์ src/app.js จากนั้นลบ container และรันใหม่จาก image เดิม การแก้นั้นยังอยู่หรือไม่ เพราะเหตุใด
6.4 Dockerfile และ build cache
Dockerfile คือไฟล์ข้อความที่ระบุขั้นตอนสร้าง image ทีละคำสั่ง Dockerfile แบบ stage เดียวของแอปมีดังนี้
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]| คำสั่ง | หน้าที่ |
|---|---|
FROM node:22-slim | ใช้ image ทางการของ Node.js รุ่น 22 แบบ slim เป็นฐาน |
WORKDIR /app | กำหนดโฟลเดอร์ทำงานใน image (สร้างให้ถ้ายังไม่มี) |
COPY package.json package-lock.json ./ | คัดลอกเฉพาะไฟล์ที่ใช้ติดตั้ง dependency |
RUN npm ci --omit=dev | ติดตั้ง dependency ตาม lock file โดยไม่ติดตั้ง devDependencies |
COPY . . | คัดลอกโค้ดที่เหลือ |
EXPOSE 3000 | บันทึกว่าแอปฟังพอร์ต 3000 (เป็นเอกสาร ไม่ได้เปิดพอร์ตเอง) |
CMD ["node", "src/server.js"] | คำสั่งเริ่มต้นเมื่อรัน container |
ระดับที่ 1 build cache: Docker เก็บผลของแต่ละคำสั่งไว้ ถ้าคำสั่งและไฟล์ที่เกี่ยวข้องไม่เปลี่ยน การ build ครั้งถัดไปจะใช้ layer เดิม ผลทดสอบเมื่อแก้เฉพาะ src/app.js แล้ว build ใหม่
#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
#9 [5/5] COPY . .
#9 DONE 0.0sระดับที่ 2 ลำดับของคำสั่ง: เหตุที่ Dockerfile คัดลอก package.json และ lock file ก่อน แล้วจึงคัดลอกโค้ดที่เหลือ คือเพื่อให้ขั้น npm ci ซึ่งช้าที่สุดใช้ cache ได้ตราบที่ dependency ไม่เปลี่ยน ถ้าเขียน COPY . . ก่อน RUN npm ci การแก้โค้ดบรรทัดเดียวจะทำให้ cache ของทุกคำสั่งหลังจากนั้นใช้ไม่ได้และต้องติดตั้ง dependency ใหม่ทุกครั้ง
ระดับที่ 3 รูปแบบ JSON ของ CMD: เครื่องมือตรวจ Dockerfile ที่มากับ Docker (docker build --check) เตือนเมื่อเขียน CMD node src/server.js แบบข้อความ ผลทดสอบ
WARNING: JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for CMD to prevent unintended behavior related to OS signalsรูปแบบ JSON ทำให้ node เป็น process หลักของ container โดยตรงแทนที่จะรันผ่าน shell ซึ่งเกี่ยวกับการรับสัญญาณหยุดการทำงาน หัวข้อ 6.6 และ Lab D จะแสดงผลที่วัดได้จริงของเรื่องนี้
คำถามตรวจความเข้าใจ 6.4: ทีมหนึ่งเขียน Dockerfile โดยมี COPY . . อยู่ก่อน RUN npm ci อธิบายผลต่อเวลา build เมื่อแก้ README หนึ่งบรรทัด และเสนอวิธีแก้
6.5 .dockerignore และขนาดของ image
คำสั่ง docker build . ส่งไฟล์ทั้งหมดในโฟลเดอร์ (build context) ให้ daemon ไฟล์ .dockerignore ระบุไฟล์ที่ไม่ต้องส่ง ในลักษณะเดียวกับ .gitignore ผลทดสอบเทียบ image ที่ build จากโฟลเดอร์ซึ่งมี node_modules และ .git อยู่ ก่อนและหลังเพิ่ม .dockerignore
ไฟล์ใน /app ของ image | ขนาด layer COPY . . | |
|---|---|---|
ไม่มี .dockerignore | .git .github .gitignore Dockerfile README.md node_modules package-lock.json package.json scripts src test | 4.96MB |
มี .dockerignore | .gitignore README.md node_modules package-lock.json package.json scripts src test | 49.2kB |
.dockerignore ที่ใช้ทดสอบ
node_modules
.git
.github
*.log
.env
Dockerfile
.dockerignoreรายการนี้ยังไม่กันโฟลเดอร์ test เพราะ multi-stage build ในหัวข้อ 6.6 ต้องใช้ไฟล์ test ใน stage ที่รัน npm test
ระดับที่ 1: node_modules ใน image รุ่นที่มี .dockerignore มาจาก RUN npm ci ภายใน image ไม่ได้มาจากเครื่องผู้พัฒนา ซึ่งเป็นสิ่งที่ต้องการ เพราะ node_modules บนเครื่องผู้พัฒนาอาจไม่ตรงกับ lock file
ระดับที่ 2: ประโยชน์ด้านขนาดเป็นเพียงส่วนหนึ่ง ประโยชน์ที่สำคัญกว่าคือความปลอดภัย image ที่ไม่มี .dockerignore มีโฟลเดอร์ .git ทั้งประวัติอยู่ข้างใน และถ้าโฟลเดอร์มีไฟล์ .env ไฟล์นั้นจะถูกบรรจุเข้า image และเผยแพร่ไปพร้อมกับ image ในบทที่ 8
ระดับที่ 3: ไฟล์ที่ถูกคัดลอกเข้า layer หนึ่งแล้วยังคงอยู่ใน layer นั้นแม้คำสั่งถัดไปจะลบออก เพราะแต่ละ layer แก้ไขไม่ได้ การลบความลับด้วย RUN rm .env หลัง COPY จึงไม่ได้ลบออกจาก image การป้องกันที่ถูกต้องคือไม่ให้ไฟล์เข้า build context ตั้งแต่ต้น
คำถามตรวจความเข้าใจ 6.5: สมาชิกคนหนึ่งเสนอให้แก้ปัญหาไฟล์ .env หลุดเข้า image ด้วยการเพิ่ม RUN rm -f .env ต่อท้าย COPY . . อธิบายว่าวิธีนี้ไม่ปลอดภัยเพราะเหตุใด
6.6 multi-stage build
multi-stage build คือ Dockerfile ที่มี FROM หลายครั้ง แต่ละ FROM เริ่ม stage ใหม่ ตั้งชื่อ stage ด้วย AS และคัดเฉพาะผลลัพธ์ที่ต้องการจาก stage ก่อนหน้าด้วย COPY --from= image สุดท้ายจึงไม่มีเครื่องมือหรือไฟล์ที่ใช้เฉพาะตอน build ติดไป (Docker Docs: Multi-stage builds)
Dockerfile แบบ multi-stage ของแอปมีสอง stage: stage build ติดตั้ง dependency ทั้งหมดและรัน test ส่วน stage สุดท้ายติดตั้งเฉพาะ dependency ที่ใช้รันและคัดลอกเฉพาะโฟลเดอร์ src จาก stage build
# Stage 1: ติดตั้ง dependency ทั้งหมดและรัน test
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm test
# Stage 2: image สำหรับรันจริง
FROM node:22-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/src ./src
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]ระดับที่ 1: ไฟล์ใน /app ของ image สุดท้าย (ทดสอบแล้ว) เหลือเพียง node_modules package-lock.json package.json src โฟลเดอร์ test และ scripts อยู่ใน stage build เท่านั้น
ระดับที่ 2: เพราะ stage สุดท้ายคัดลอกจาก stage build stage นั้นจึงต้อง build ให้สำเร็จก่อน ถ้า npm test ล้ม การ build ทั้งหมดจะล้มและไม่มี image ใหม่เกิดขึ้น ผลทดสอบเมื่อทำให้ test ล้มโดยตั้งใจ
#10 0.735 not ok 4 - GET /api/items/1 returns Linux
#10 0.741 # fail 1
ERROR: failed to build: failed to solve: process "/bin/sh -c npm test" did not complete successfully: exit code: 1image ที่มีโค้ดซึ่ง test ไม่ผ่านจึงไม่ถูกสร้าง ซึ่งเป็น Quality Gate อีกชั้นต่อจาก CI ในบทที่ 5
ระดับที่ 3: USER node ทำให้แอปใน container รันด้วยผู้ใช้ที่ไม่ใช่ root (ทดสอบแล้วว่า uid เป็น 1000) ซึ่งเป็นการใช้หลักให้สิทธิ์เท่าที่จำเป็นจากบทที่ 1 กับ container ส่วนเรื่องการหยุด container พบว่า docker stop ใช้เวลาประมาณ 10 วินาทีและ container จบด้วย exit code 137 เพราะ node ซึ่งเป็น process หลัก (PID 1) ไม่ได้จัดการสัญญาณ SIGTERM จน Docker ต้องส่ง SIGKILL เมื่อรันด้วย docker run --init การหยุดใช้เวลาประมาณ 0.1 วินาทีและจบด้วย 143 (ทดสอบแล้วทั้งสองกรณี) ข้อที่ต้องยืนยัน: คำอธิบายกลไก PID 1 และค่าเวลารอเริ่มต้นของ docker stop เขียนจากผลการทดสอบ ยังไม่ได้ตรวจกับเอกสาร Docker ในรายการแหล่งอ้างอิงของบท
คำถามตรวจความเข้าใจ 6.6: อธิบายว่าเหตุใด image สุดท้ายของ Dockerfile ข้างต้นจึงไม่มีโฟลเดอร์ test และถ้าลบบรรทัด COPY --from=build /app/src ./src ออก แล้วเขียน COPY src ./src แทน ขั้น npm test ยังมีผลต่อการ build หรือไม่
6.7 image, container และข้อมูลที่เปลี่ยนระหว่างรัน
image เป็นแม่แบบสำหรับสร้าง container ส่วน container มีสถานะระหว่างทำงาน การสร้าง container สองตัวจาก image เดียวกันจึงไม่ได้หมายความว่ามีข้อมูลระหว่างรันชุดเดียวกัน หากแต่ละตัวเขียนไฟล์ลง writable layer ของตนเอง ไฟล์เหล่านั้นจะแยกกัน การแยกแม่แบบออกจากข้อมูลเป็นพื้นฐานของการออกแบบให้แทนที่ container ได้โดยไม่สูญเสียสิ่งที่ต้องรักษา
ตัวอย่างสมมติของ devops-app เขียนไฟล์รายงานไว้ใน /app/reports ภายใน container แล้วผู้ดูแลสร้าง container ใหม่จาก image เดิม หากไม่ได้ใช้แหล่งเก็บถาวรที่ออกแบบไว้ รายงานในตัวเดิมจะไม่ปรากฏในตัวใหม่ การสรุปว่า “Docker ทำข้อมูลหาย” จึงไม่อธิบายกลไก ความผิดอยู่ที่เก็บข้อมูลสำคัญไว้ในชั้นที่ตั้งใจให้ทดแทนได้ บทที่ 7 จะใช้ volume แยกวงจรชีวิตของข้อมูลออกจาก container
ตัวอย่างที่ไม่ถูกต้องอีกแบบคือเข้าไปติดตั้งแพ็กเกจเพิ่มใน container ที่กำลังรันแล้วถือว่าแก้ Dockerfile แล้ว ผลที่ได้เป็นการเปลี่ยนเฉพาะ instance นั้น การ build ใหม่ยังไม่มีแพ็กเกจดังกล่าว วิธีที่ตรวจสอบได้คือปรับ Dockerfile สร้าง image ใหม่ ทดสอบ และแทนที่ container พร้อมเก็บตัวระบุ image ที่ใช้จริง
การแยกชั้นนี้ช่วยตอบคำถามเวลาตรวจเหตุ: ถ้าเปิด container ใหม่แล้วอาการหาย อาจเกี่ยวกับสถานะที่สะสม แต่ยังต้องพิสูจน์ว่าคือไฟล์ cache การใช้หน่วยความจำ หรือค่าภายนอก การ restart เป็นการบรรเทาอาการได้ แต่ยังไม่ใช่คำอธิบายสาเหตุ หากไม่มี log และข้อมูลก่อน restart หลักฐานสำคัญอาจหายไป
6.8 build context กับการส่งไฟล์เกินจำเป็น
build context คือชุดไฟล์ที่ builder เข้าถึงตามการเรียก build การใช้ COPY . . โดยไม่กำหนดขอบเขตจึงอาจนำไฟล์ที่ไม่จำเป็นเข้า image เช่น log, dependency ในเครื่อง หรือไฟล์ค่าลับ การมี .gitignore ไม่ได้แทน .dockerignore เพราะเครื่องมือสองตัวอ่านกติกาคนละชุด การตรวจควรอ่านไฟล์ที่จะส่งให้ builder และไฟล์ที่อยู่ใน image สุดท้ายโดยตรง
ตัวอย่างที่ผิดคือคัดลอก .env เข้า image แล้วลบออกในคำสั่งชั้นถัดไป โดยเข้าใจว่าไฟล์จะหายจากทุกส่วนของ image การออกแบบที่เหมาะสมคือไม่ส่งความลับเข้า build context ตั้งแต่ต้น และถ้าขั้น build ต้องใช้ความลับให้ใช้กลไก build secret ที่เหมาะสม การลบภายหลังไม่ใช่หลักฐานว่าไม่เคยมีข้อมูลอยู่ในชั้นก่อนหน้า
อีกตัวอย่างคือคัดลอก node_modules จากเครื่องพัฒนาที่ใช้ระบบปฏิบัติการหรือสถาปัตยกรรมต่างจาก image แพ็กเกจที่มีส่วน native อาจใช้ไม่ได้ แม้ไฟล์ JavaScript ของแอปเหมือนกัน แนวทางที่สอดคล้องกว่าคือให้ขั้น build ติดตั้ง dependency ในสภาพแวดล้อมเป้าหมายตาม lock file แล้วคัดลอกเฉพาะผลผลิตที่ต้องใช้
การเลือก base image จึงควรพิจารณาความเข้ากันได้ อายุการสนับสนุน ขนาด และเครื่องมือที่ต้องใช้ ไม่เลือกจากขนาดเล็กที่สุดอย่างเดียว หากไม่มีเครื่องมือ shell หรือโปรแกรมตรวจ health ตามที่ compose เรียกใช้ healthcheck จะล้มได้แม้แอปตอบปกติ การตรวจ “คำสั่งนี้มีอยู่ใน image หรือไม่” เป็นขั้นที่แยกจากการตรวจแอป
6.9 cache ที่เร็วขึ้นและ cache ที่ทำให้เข้าใจผิด
Docker ตรวจความสามารถในการใช้ cache ตามคำสั่งและข้อมูลที่เกี่ยวข้อง เมื่อชั้นหนึ่งใช้ cache ไม่ได้ ชั้นถัดไปที่พึ่งลำดับนั้นต้องสร้างใหม่ เอกสารจึงแนะนำให้จัดสิ่งที่เปลี่ยนไม่บ่อยก่อนสิ่งที่เปลี่ยนบ่อย เช่น คัดลอก manifest กับ lock file แล้วติดตั้ง dependency ก่อนคัดลอก source (Docker Docs: Build cache invalidation)
เปรียบเทียบ Dockerfile สองแนวทางในสถานการณ์สมมติ แนวทางแรก COPY . . ก่อน RUN npm ci เมื่อแก้ข้อความ README ก็อาจกระทบชั้นติดตั้ง dependency แนวทางที่สองคัดลอก package.json กับ package-lock.json ก่อน RUN npm ci แล้วคัดลอก source ภายหลัง การแก้เฉพาะ source จึงมีโอกาสใช้ชั้น dependency เดิมได้ ทั้งนี้ผลจริงต้องดู build log และขอบเขตไฟล์ ไม่ควรอ้างจำนวนวินาทีที่ประหยัดได้โดยไม่วัด
ความเข้าใจที่ไม่ถูกต้องคือ build วันนี้แล้วแพ็กเกจระบบต้องเป็นรุ่นล่าสุดเสมอ เอกสาร Docker ระบุว่า cache ของ RUN ไม่หมดอายุเพียงเพราะเวลาผ่านไป คำสั่งติดตั้งแพ็กเกจเดิมอาจใช้ชั้น cache เดิม การอัปเดตเพื่อแก้ช่องโหว่จึงต้องมีนโยบาย refresh และทดสอบ ไม่ใช่อาศัยการเรียก build ซ้ำอย่างเดียว
อีกความเข้าใจผิดคือใช้ --no-cache ทุกครั้งแล้วได้ reproducible build แน่นอน การไม่ใช้ cache เพียงบังคับให้ทำขั้นใหม่ แต่ถ้าขั้นนั้นดาวน์โหลดสิ่งที่เปลี่ยนได้ ผลใหม่อาจต่างจากเดิม การทำซ้ำได้ต้องควบคุม inputs ส่วนการไม่ใช้ cache เป็นเพียงเครื่องมือหนึ่งในการตรวจหรือ refresh
6.10 multi-stage build กับขอบเขตของผลที่คัดลอก
multi-stage build ใช้หลาย FROM เพื่อแยกขั้นสร้าง ทดสอบ และรัน โดยคัดลอกเฉพาะผลผลิตที่ต้องใช้ไปยังขั้นสุดท้าย กลไกนี้ลดการนำเครื่องมือ build ที่ไม่จำเป็นติดไปใน runtime แต่ไม่ได้พิสูจน์ว่า image ปลอดช่องโหว่หรือ test ผ่านโดยอัตโนมัติ ต้องตรวจว่าขั้นที่ใช้ build อ้างอิงผลจากขั้น test จริงหรือไม่ และยังต้องสแกน image ที่จะส่งมอบ (Docker Docs: Multi-stage builds)
ตัวอย่างที่ผิดคือมี stage ชื่อ test แต่ขั้นสุดท้ายไม่ได้พึ่งพา stage นั้น แล้วสรุปว่าการ build final บังคับรัน test ทุกครั้ง ชื่อ stage ไม่มีผลเป็น quality gate หากกราฟการพึ่งพาไม่ได้บังคับ การแก้คือออกแบบลำดับให้ผลทดสอบเป็นเงื่อนไขที่ pipeline ตรวจจริง หรือเรียก target สำหรับ test อย่างชัดเจนก่อนอนุญาตส่ง final image
ตัวอย่างอีกแบบคือ runtime stage คัดลอกทั้งไดเรกทอรีจาก builder จนได้ไฟล์ทดสอบ cache และเครื่องมือที่ตั้งใจแยกออกมาด้วย การใช้หลาย stage จึงไม่ใช่เป้าหมายในตัวเอง ต้องกำหนดรายการสิ่งที่ runtime จำเป็นต้องมี เช่น source หรือไฟล์ที่แปลงแล้ว production dependencies และค่าตั้งที่ไม่ใช่ความลับ แล้วตรวจ image สุดท้ายตามรายการนั้น
ก่อนส่งต่อ image ควรตรวจทั้งกรณีปกติและกรณีล้ม ได้แก่ เริ่มแอปและตอบ endpoint ได้ รันด้วยบัญชีที่ตั้งใจไว้ เขียนเฉพาะตำแหน่งที่อนุญาตได้ และรายงานสถานะผิดเมื่อค่าตั้งจำเป็นขาด การผ่าน docker build เพียงอย่างเดียวบอกว่าขั้นสร้างสำเร็จตาม Dockerfile แต่ยังไม่ยืนยันว่าแอปใน image ทำงานตามสัญญาแล้ว
กรณีอธิบายต่อเนื่อง: image สร้างได้แต่ healthcheck ล้ม
กำหนดผลคาดหมายของสถานการณ์สมมติว่า docker build สำเร็จ แอปรันและตอบ /health ได้จากการเรียกโดยตรง แต่สถานะ container เป็น unhealthy จึงมีสมมติฐานแยกกันอย่างน้อยสามข้อ: ตัวตรวจเรียกพอร์ตผิด ตัวตรวจใช้ path ผิด หรือคำสั่งตรวจไม่มีใน runtime image การ build ผ่านยังไม่เลือกข้อใดให้ ต้องอ่านคำสั่ง healthcheck และผลของการตรวจนั้น
หาก Dockerfile builder มี curl แต่ runtime stage ไม่ได้ติดตั้ง การทดสอบด้วยคำสั่งใน builder ไม่ใช่หลักฐานว่า runtime เรียกได้ การคัดลอกผลผลิตข้าม stage ไม่คัดลอกโปรแกรมทั้งหมดโดยอัตโนมัติ วิธีแก้ต้องเลือกตัวตรวจที่มีอยู่จริงหรือเพิ่มสิ่งจำเป็นอย่างมีเหตุผล แล้วทดสอบ image สุดท้าย ไม่เปิด shell ใน builder แล้วประกาศว่าแก้แล้ว
ถ้าคำสั่งมีอยู่แต่ใช้ localhost:8080 ในขณะที่แอปฟัง 3000 การเพิ่ม retry ไม่แก้พอร์ตผิด ส่วนถ้าพอร์ตถูกแต่ endpoint คืน 500 เพราะค่าตั้งขาด ต้องแก้สัญญาค่าตั้งหรือพฤติกรรมแอป ไม่เปลี่ยนตัวตรวจให้พิมพ์ ok เพื่อให้ healthy ตัวตรวจต้องรักษาความหมายของความพร้อม ไม่ใช่เพียงทำให้สถานะแสดงสีเขียว
การเปรียบเทียบก่อนและหลังควรเก็บ digest หรือ image ID ที่เกี่ยวข้อง คำสั่งตัวตรวจ ผลที่ได้ และ response จาก endpoint หาก rebuild แล้วใช้ tag เดิมโดยไม่ตรวจว่า container ถูกสร้างจาก image ใหม่จริง ผลการทดลองอาจเป็นของตัวเก่า การตรวจ artifact identity จึงมีผลแม้เป็นการทดลองบนเครื่องเดียว
กรณีนี้แสดงความต่างของข้อสรุปสามระดับ: build ผ่านยืนยันขั้นสร้าง runtime เริ่มได้ยืนยันการเริ่มกระบวนการ และ healthcheck ผ่านยืนยันเฉพาะเงื่อนไขที่ตัวตรวจออกแบบไว้ ทั้งสามยังไม่ทดแทนการทดสอบฟังก์ชันหลักของแอป จึงต้องส่งผลแต่ละระดับต่อไปยัง pipeline อย่างแยกแยะ
ปฏิบัติการที่ 6 บรรจุแอปทีมลง container
ปฏิบัติการนี้ประกอบด้วย Lab A–D ทุกคน build และรัน image บนเครื่องของตนเอง ส่วนการเปลี่ยนแปลงที่เข้า repository แบ่งผู้รับผิดชอบดังนี้ เพื่อให้ทุกคนมี pull request ของตนเองใน milestone M5
| pull request | Lab | ผู้รับผิดชอบ | ผู้ตรวจทาน |
|---|---|---|---|
เพิ่ม Dockerfile แบบ stage เดียว | B | สมาชิกคนที่ 1 | สมาชิกคนที่ 2 |
เพิ่ม .dockerignore | C | สมาชิกคนที่ 2 | สมาชิกคนที่ 3 |
เปลี่ยน Dockerfile เป็น multi-stage | D | สมาชิกคนที่ 3 | สมาชิกคนที่ 1 |
pull request ทุกรายการต้องผ่าน CI จากบทที่ 5 ก่อน merge workflow CI ยังไม่ได้ build image ใน runner การตรวจ image จึงทำบนเครื่องของผู้ตรวจทานตามขั้นที่ระบุในแต่ละ Lab แต่ละ Lab แบ่งเป็นส่วน ฝึกตามตัวอย่าง และส่วน ตัดสินใจเอง (A.3, B.4, C.3 และ D.4)
ผลลัพธ์ในปฏิบัติการนี้ได้จากการรันจริงบน Docker Engine 29.8.2 โดยใช้ base image จำลองตามที่ระบุในบทนำ เลข CONTAINER ID, digest และขนาดของ image จะต่างจากตัวอย่าง ข้อความความคืบหน้าของ docker build มีหลายรูปแบบตามการตั้งค่า terminal ตัวอย่างในบทนี้ใช้ --progress=plain เมื่อต้องการแสดงทุกขั้น
Lab A: รัน image สำเร็จรูป
สถานการณ์: ก่อนเขียน Dockerfile เอง ทีมต้องคุ้นกับวงจรชีวิตของ container โดยใช้ image ทางการของ Node.js ที่จะใช้เป็นฐานของแอป
เป้าหมาย: ดึงและรัน image สำเร็จรูป ดูรายการ image และ container และเก็บกวาด container ที่หยุดแล้ว (พื้นฐานของ LLO 6.1)
ฝึกตามตัวอย่าง
A.1 ตรวจ Docker และรัน image ทางการ (สมาชิกทุกคน)
docker version
docker run --rm node:22-slim node -v
docker imagesv22.22.0
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
node:22-slim 6c34e5d4d221 200MB 50.3MBผล docker version ต้องมีทั้งส่วน Client และ Server ครั้งแรกที่รัน Docker จะดึง node:22-slim จาก Docker Hub ก่อน จึงมีบรรทัดความคืบหน้าของการดาวน์โหลดเพิ่มเติม (ผู้เขียนใช้ image จำลองจึงไม่มีบรรทัดดังกล่าว) ตัวเลือก --rm ลบ container ให้ทันทีเมื่อคำสั่งจบ
A.2 รัน container แบบไม่ลบอัตโนมัติ
docker run node:22-slim node -e "console.log('hello from container', process.version)"
docker run node:22-slim node -e "console.log(require('os').hostname())"
docker ps
docker ps -ahello from container v22.22.0
956b4938d266
CONTAINER ID IMAGE STATUS
956b4938d266 node:22-slim Exited (0) Less than a second ago
2eafc8c49bf6 node:22-slim Exited (0) Less than a second agodocker ps แสดงเฉพาะ container ที่กำลังรัน ซึ่งว่างเปล่าเพราะทั้งสองตัวจบแล้ว ส่วน docker ps -a แสดง container ทั้งหมดรวมที่หยุดแล้ว (ตัวอย่างข้างต้นแสดงเฉพาะคอลัมน์ที่สำคัญ) ชื่อเครื่องที่ container พิมพ์ตรงกับ CONTAINER ID ตามที่อธิบายในหัวข้อ 6.1
ตัดสินใจเอง A.3: container ที่หยุดแล้วจาก A.2 ยังค้างอยู่ในเครื่อง ค้นจากเอกสาร Docker CLI แล้วเลือกคำสั่งลบ container ที่หยุดแล้วทั้งหมดในครั้งเดียว โดยไม่ลบ image และยืนยันผลด้วย docker ps -a
ผลลัพธ์ที่คาดหวัง A.3: docker ps -a ไม่แสดง container ที่หยุดแล้ว และ docker images ยังมี node:22-slim
จุดตรวจ A: สมาชิกทุกคนรัน node:22-slim ได้และอธิบายความต่างของ docker ps กับ docker ps -a ได้
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
permission denied while trying to connect to the Docker daemon socket | ผู้ใช้ไม่ได้อยู่ในกลุ่ม docker | แจ้งผู้ดูแลเครื่อง หรือออกจากระบบแล้วเข้าใหม่หลังถูกเพิ่มเข้ากลุ่ม |
Cannot connect to the Docker daemon | daemon ไม่ทำงาน | แจ้งผู้ดูแลเครื่องให้เริ่มบริการ Docker |
ดึง image ไม่ได้ ข้อความมี toomanyrequests หรือ Forbidden | ถูกจำกัดอัตราการดึง หรือเครือข่ายปิดกั้น registry | รอแล้วลองใหม่ หรือใช้ image ที่ผู้สอนดึงไว้ล่วงหน้า |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab B: Dockerfile ของแอป
สถานการณ์: ทีมต้องการ image ของ devops-app ที่รันได้บนเครื่องใดก็ได้ที่มี Docker
เป้าหมาย: เขียน Dockerfile จากแม่แบบ build image และรัน container ที่ตอบ HTTP 200 (LLO 6.1)
ฝึกตามตัวอย่าง
B.1 เขียน Dockerfile (สมาชิกคนที่ 1)
cd ~/devops-app
git switch main
git pull
git switch -c docker/add-dockerfile
nano DockerfileFROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]B.2 build image และรัน container (สมาชิกทุกคน หลังจากสมาชิกคนที่ 1 push branch แล้ว ให้ checkout branch นั้นมา build)
docker build -t devops-app:0.2.0 .
docker images devops-app
docker run -d --name devops-app -p 8080:3000 devops-app:0.2.0
docker psIMAGE ID DISK USAGE CONTENT SIZE EXTRA
devops-app:0.2.0 00dab08f0bb1 217MB 53.1MB
4ebeb6a6be885fe312c70a345a74576a688422e4fb6bcb6ac594b076ba3ad391
NAMES IMAGE STATUS PORTS
devops-app devops-app:0.2.0 Up 1 second 0.0.0.0:8080->3000/tcp| ตัวเลือก | ความหมาย |
|---|---|
-t devops-app:0.2.0 | ตั้งชื่อและ tag ให้ image ตามเวอร์ชันจากบทที่ 4 |
-d | รัน container เบื้องหลัง แล้วพิมพ์ CONTAINER ID |
--name devops-app | ตั้งชื่อ container ให้อ้างถึงได้ง่าย |
-p 8080:3000 | ส่งการเชื่อมต่อที่พอร์ต 8080 ของเครื่อง ไปยังพอร์ต 3000 ใน container |
เรียกแอปผ่านพอร์ตของเครื่องและดู log ของ container
curl -i http://localhost:8080/health
docker logs devops-appHTTP/1.1 200 OK
...
{"status":"ok","version":"0.2.0"}
listening on port 3000แอปใน container ยังฟังพอร์ต 3000 ตามโค้ดเดิม ผู้ใช้เข้าทางพอร์ต 8080 ของเครื่องเพราะการ map ด้วย -p ข้อความ listening on port 3000 ที่แอปพิมพ์ออก stdout กลายเป็น log ของ container
B.3 หยุดและลบ container แล้วเปิด pull request
docker stop devops-app
docker rm devops-appสมาชิกคนที่ 1 commit Dockerfile แล้วเปิด pull request สมาชิกคนที่ 2 ตรวจทานโดย build และ curl ตาม B.2 บนเครื่องของตน แล้วจึงกด Approve และ merge หลัง CI ผ่าน
git add Dockerfile
git commit -m "เพิ่ม Dockerfile สำหรับบรรจุแอปลง container"
git push -u origin docker/add-dockerfileตัดสินใจเอง B.4 (LLO 6.2): รัน container ตัวที่สองจาก image devops-app:0.2.0 เดิมให้ทำงานพร้อมกับตัวแรก โดยทั้งสองตัวต้องตอบ /health ได้จากเครื่อง ครั้งแรกให้ลองใช้พอร์ต 8080 ซ้ำเพื่อดูว่าเกิดอะไรขึ้น แล้วแก้ให้ถูก จากนั้นอธิบายด้วยผลที่ได้ว่า image หนึ่งตัวกับ container สองตัวสัมพันธ์กันอย่างไร
ผลลัพธ์ที่คาดหวัง B.4: docker ps แสดง container สองตัวจาก image เดียวกันที่ map คนละพอร์ต และคำอธิบายที่ใช้ได้ในใบงานงานข้อ 2
จุดตรวจ B: main มี Dockerfile · สมาชิกทุกคนได้ HTTP 200 จาก container ที่ build จาก Dockerfile ของทีม
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
COPY failed: file not found ที่ package-lock.json | build จาก branch ที่ยังไม่มี lock file หรือสั่ง build นอกโฟลเดอร์ repository | cd ~/devops-app และตรวจว่าอยู่บน branch ที่มี lock file |
curl: (56) Recv failure หรือ Connection reset | แอปใน container ยังเริ่มไม่เสร็จ หรือล้มตอนเริ่ม | รอ 1–2 วินาที แล้วดู docker logs devops-app |
Conflict. The container name "/devops-app" is already in use | มี container ชื่อเดิมค้างอยู่ | docker rm -f devops-app แล้วรันใหม่ |
-p 3000:8080 แล้วเรียกไม่ได้ | สลับลำดับพอร์ต | ลำดับคือ พอร์ตเครื่อง:พอร์ตใน container |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab C: .dockerignore และเทียบขนาด image
สถานการณ์: image จาก Lab B บรรจุทุกไฟล์ในโฟลเดอร์ของผู้ build รวมถึงไฟล์ที่ไม่ควรอยู่ใน image
เป้าหมาย: ตรวจว่า image มีไฟล์อะไรบ้าง เพิ่ม .dockerignore แล้วเทียบผลก่อนและหลัง
ฝึกตามตัวอย่าง
C.1 ดูไฟล์ใน image และขนาดของ layer (สมาชิกทุกคน)
image จาก Lab B ไม่มีคำสั่ง ls ที่แน่นอนในทุกรุ่นของ base image จึงใช้ Node.js อ่านรายการไฟล์แทน
docker run --rm devops-app:0.2.0 node -e "console.log(require('fs').readdirSync('.').join(' '))"
docker history devops-app:0.2.0.git .github .gitignore Dockerfile README.md node_modules package-lock.json package.json scripts src test
CREATED BY SIZE
CMD ["node" "src/server.js"] 0B
EXPOSE [3000/tcp] 0B
COPY . . # buildkit 4.96MB
RUN /bin/sh -c npm ci --omit=dev # buildkit 9.6MB
...ผลข้างต้นได้จากโฟลเดอร์ที่มี node_modules และ .git อยู่ image จึงมีทั้งประวัติ Git และ node_modules จากเครื่องผู้ build ทับของที่ npm ci ติดตั้งไว้
C.2 เพิ่ม .dockerignore แล้ว build ใหม่ (สมาชิกคนที่ 2 เปิด pull request ทุกคนทดสอบ)
git switch main
git pull
git switch -c docker/add-dockerignore
nano .dockerignorenode_modules
.git
.github
*.log
.env
Dockerfile
.dockerignoredocker build -t devops-app:0.2.0 .
docker run --rm devops-app:0.2.0 node -e "console.log(require('fs').readdirSync('.').join(' '))"
docker history devops-app:0.2.0.gitignore README.md node_modules package-lock.json package.json scripts src test
...
COPY . . # buildkit 49.2kB
...layer ของ COPY . . ลดจาก 4.96MB เหลือ 49.2kB และ .git, .github กับ Dockerfile ไม่อยู่ใน image แล้ว โฟลเดอร์ test ยังอยู่โดยตั้งใจ เพราะ Lab D ต้องใช้ใน stage ที่รัน test commit และเปิด pull request
git add .dockerignore
git commit -m "เพิ่ม .dockerignore กันไฟล์ที่ไม่ควรอยู่ใน image"
git push -u origin docker/add-dockerignoreตัดสินใจเอง C.3: .env ถูกกันด้วย .gitignore ตั้งแต่บทที่ 2 จึงไม่อยู่ใน repository แต่อาจมีอยู่ในโฟลเดอร์ของผู้ build ออกแบบการทดลองที่พิสูจน์ว่า (1) ถ้าไม่มี .dockerignore ไฟล์ .env ในโฟลเดอร์จะถูกบรรจุเข้า image และอ่านเนื้อหาได้จาก container และ (2) เมื่อมี .dockerignore ของทีมแล้วไฟล์นั้นไม่อยู่ใน image ใช้ค่าสมมติที่ไม่ใช่รหัสผ่านจริงเท่านั้น และลบไฟล์ทดลองทั้งหมดเมื่อเสร็จ
ผลลัพธ์ที่คาดหวัง C.3: ผลการทดลองสองกรณีที่ต่างกันชัดเจน และ git status หลังทดลองไม่มีไฟล์ค้าง
จุดตรวจ C: main มี .dockerignore · image ที่ build จาก main ไม่มี .git
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
build ล้มเพราะหา src ไม่พบ | ใส่ src หรือ * ใน .dockerignore | ลบบรรทัดนั้น .dockerignore ควรกันเฉพาะสิ่งที่ไม่ต้องใช้ |
.git ยังอยู่ใน image | ชื่อไฟล์ผิด เช่น dockerignore ไม่มีจุดนำหน้า หรือวางผิดโฟลเดอร์ | ไฟล์ต้องชื่อ .dockerignore และอยู่ในโฟลเดอร์เดียวกับที่ส่งเป็น build context |
| ผลขนาดไม่เปลี่ยน | โฟลเดอร์ไม่มี node_modules อยู่แล้วตั้งแต่ต้น | เป็นไปได้ ให้เทียบรายการไฟล์ใน image แทนขนาด |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab D: multi-stage build
สถานการณ์: image ปัจจุบันมีไฟล์ test อยู่ด้วยทั้งที่ไม่ได้ใช้ตอนรัน แอปรันด้วยผู้ใช้ root และการ build ไม่ได้ตรวจว่า test ผ่าน
เป้าหมาย: เปลี่ยน Dockerfile เป็น multi-stage ที่รัน test ระหว่าง build และให้ image สุดท้ายมีเฉพาะสิ่งที่ใช้รัน (LLO 6.1)
ฝึกตามตัวอย่าง
D.1 เปลี่ยนเป็น multi-stage (สมาชิกคนที่ 3)
git switch main
git pull
git switch -c docker/multi-stage
nano Dockerfileแทนเนื้อหาทั้งไฟล์ด้วย Dockerfile แบบ multi-stage ในหัวข้อ 6.6 แล้ว build และตรวจผล
docker build --progress=plain -t devops-app:0.2.0 . 2>&1 | grep -E "^#[0-9]+ \[|# pass|# fail"
docker run --rm devops-app:0.2.0 node -e "console.log(require('fs').readdirSync('.').join(' '), process.getuid())"#4 [build 1/6] FROM docker.io/library/node:22-slim@sha256:...
#6 [build 2/6] WORKDIR /app
#7 [build 3/6] COPY package.json package-lock.json ./
#8 [build 4/6] RUN npm ci
#9 [build 5/6] COPY . .
#10 [stage-1 4/5] RUN npm ci --omit=dev
#11 [build 6/6] RUN npm test
#11 0.810 # pass 4
#11 0.810 # fail 0
#12 [stage-1 5/5] COPY --from=build /app/src ./src
node_modules package-lock.json package.json src 1000ขั้นของ stage build และ stage สุดท้าย (stage-1) รันสลับกันได้เพราะ BuildKit รันขั้นที่ไม่ขึ้นต่อกันพร้อมกัน image สุดท้ายมีเพียง 4 รายการ และ process รันด้วย uid 1000 (ผู้ใช้ node) ไม่ใช่ 0 (root) ตัวเลข # pass ขึ้นกับจำนวน test ใน main ของทีม
D.2 ยืนยันว่า test ที่ล้มทำให้ build ล้ม
แก้ test ของตนเองจากบทที่ 5 ให้ผิดชั่วคราว build แล้วคืนค่า
sed -i "s/'Linux'/'linux'/" test/items-somchai.test.js
docker build -t devops-app:broken . 2>&1 | tail -3
git restore test/items-somchai.test.js#10 0.741 # fail 1
ERROR: failed to build: failed to solve: process "/bin/sh -c npm test" did not complete successfully: exit code: 1ชื่อไฟล์และค่าที่แก้ให้ใช้ของตนเอง เมื่อยืนยันแล้ว commit Dockerfile และเปิด pull request สมาชิกคนที่ 1 ตรวจทานด้วยการ build และ curl บนเครื่องของตน
git add Dockerfile
git commit -m "เปลี่ยน Dockerfile เป็น multi-stage และรันด้วยผู้ใช้ node"
git push -u origin docker/multi-stageD.3 หลัง merge: ทุกคนตรวจ M5 จาก main
cd ~/devops-app
git switch main
git pull
docker build -t devops-app:0.2.0 .
docker run -d --name devops-app -p 8080:3000 devops-app:0.2.0
curl -i http://localhost:8080/health
time docker stop devops-app
docker rm devops-appHTTP/1.1 200 OK
...
{"status":"ok","version":"0.2.0"}
devops-app
real 0m10.152sคัดลอกผลตั้งแต่ docker build จนถึง curl เป็นหลักฐานงานข้อ 1 สังเกตว่า docker stop ใช้เวลาประมาณ 10 วินาทีตามที่อธิบายในหัวข้อ 6.6 ระดับที่ 3
ตัดสินใจเอง D.4: ตรวจ Dockerfile ต่อไปนี้ด้วย docker build --check ซึ่งมากับ Docker Engine แก้ให้ไม่มีคำเตือน แล้วอธิบายว่าแต่ละคำเตือนหมายความว่าอะไร โดยอ่านจากลิงก์ที่คำเตือนให้มา (บันทึกไฟล์เป็น Dockerfile.check ในโฟลเดอร์ทดลอง ไม่ commit)
FROM node:22-slim as build
WORKDIR app
COPY . .
CMD node src/server.jsผลลัพธ์ที่คาดหวัง D.4: docker build --check ของไฟล์ที่แก้แล้วแสดง Check complete, no warnings found. และคำอธิบายครบทุกคำเตือน
จุดตรวจ D: main มี Dockerfile แบบ multi-stage ที่รัน npm test และใช้ USER node · สมาชิกทุกคนมีผลรันของ D.3
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
stage build ล้มที่ npm test ด้วย Cannot find module | .dockerignore กันโฟลเดอร์ test หรือ src | ตรวจ .dockerignore ให้ตรงกับ Lab C |
image สุดท้ายไม่มี src แอปล้มด้วย Cannot find module '/app/src/server.js' | พิมพ์ path ใน COPY --from=build ผิด | ตรวจให้เป็น COPY --from=build /app/src ./src |
EACCES: permission denied เมื่อแอปพยายามเขียนไฟล์ | USER node ไม่มีสิทธิ์เขียนใน /app | แอปของรายวิชาไม่เขียนไฟล์ จึงไม่ควรเกิด หากเกิดให้ตรวจว่าแก้โค้ดแอปโดยไม่ตั้งใจหรือไม่ |
| build ใหม่แล้วยังได้ผลเดิม | ใช้ tag เดิมแต่รัน container เก่าที่ยังไม่ลบ | docker rm -f devops-app แล้วรันใหม่จาก image ที่ build ล่าสุด |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
เก็บกวาดเมื่อจบปฏิบัติการ
- หยุดและลบ container ที่ยังรันด้วย
docker psแล้วdocker rm -f ชื่อcontainer - ลบ image ทดลองที่ไม่ใช้ เช่น
docker rmi devops-app:broken test:noignore test:ignore(ถ้ามี) และลบ image ที่ไม่มี tag ด้วยdocker image prune -f - ลบไฟล์
.envทดลองและโฟลเดอร์ทดลองของ D.4 แล้วตรวจด้วยgit status - เก็บ image
devops-app:0.2.0ไว้ใช้ในบทที่ 7
สรุป
container บรรจุแอปพร้อม runtime และ dependency ลงในหน่วยเดียวที่รันได้เหมือนกันบนเครื่องใดก็ได้ที่มี container runtime แก้ปัญหา "works on my machine" ที่ lock file ในบทที่ 4 แก้ได้เพียงบางส่วน Docker ประกอบด้วย client ที่รับคำสั่ง daemon ที่ build และรัน container และ registry ที่เก็บ image
image เป็นแม่แบบแบบอ่านอย่างเดียวที่ประกอบด้วย layer ส่วน container เป็น instance ที่รันจาก image และมี writable layer ของตนเองซึ่งหายไปเมื่อลบ container Dockerfile ที่ดีวางคำสั่งที่เปลี่ยนน้อยไว้ก่อนเพื่อใช้ build cache ได้ และใช้ .dockerignore กันไฟล์ที่ไม่จำเป็นและไฟล์ลับออกจาก build context เพราะไฟล์ที่เข้า layer แล้วลบออกภายหลังไม่ได้
multi-stage build แยก stage ที่ติดตั้งเครื่องมือและรัน test ออกจาก image ที่ใช้รันจริง image สุดท้ายจึงเล็กกว่าและมีเฉพาะสิ่งที่จำเป็น และการ build จะล้มเมื่อ test ไม่ผ่าน การรันด้วย USER node เป็นการใช้หลักให้สิทธิ์เท่าที่จำเป็นกับ container
สิ่งที่ส่งต่อไปยังโครงงานและ Capstone
| สิ่งที่ได้ | นำไปใช้ที่ |
|---|---|
Dockerfile แบบ multi-stage และ M5 | บทที่ 7 บริการแอปใน compose.yaml · บทที่ 8 build และ push ใน workflow CD · บทที่ 10 สแกนช่องโหว่ของ image |
.dockerignore | บทที่ 9 จุดตรวจความลับครั้งที่ 2 · บทที่ 10 ลดสิ่งที่ต้องสแกน |
| image ที่ tag ตามเวอร์ชัน | บทที่ 8 tag บน ghcr.io · บทที่ 12 rollback ด้วย tag ก่อนหน้า |
| ใบงาน CLO1 ชิ้นที่ 3 | ใช้เตรียมสอบปากเปล่าบทที่ 12 ส่วนการใช้ container |
| หัวข้องานต่อยอด | จุดตรวจที่ 2 บทที่ 10 · สาธิตบทที่ 13 |
| Learning Log ครั้งที่ 6 | portfolio ที่ส่งในบทที่ 13 |
สรุปคำสั่งในบทนี้
| คำสั่ง | หน้าที่ |
|---|---|
docker version, docker info | ตรวจ client, daemon และ storage driver |
docker run --rm IMAGE คำสั่ง | รัน container แล้วลบเมื่อจบ |
docker run -d --name ชื่อ -p พอร์ตเครื่อง:พอร์ตcontainer IMAGE | รัน container เบื้องหลังพร้อมตั้งชื่อและ map พอร์ต |
docker run --init ... | รัน container โดยมี init process จัดการสัญญาณ |
docker ps, docker ps -a | แสดง container ที่รันอยู่ และทั้งหมด |
docker logs ชื่อ | ดู log (stdout/stderr) ของ container |
docker exec ชื่อ คำสั่ง | รันคำสั่งใน container ที่กำลังทำงาน |
docker stop ชื่อ, docker rm ชื่อ, docker rm -f ชื่อ | หยุด ลบ และบังคับลบ container |
docker container prune -f | ลบ container ที่หยุดแล้วทั้งหมด |
docker build -t ชื่อ:tag . | build image จาก Dockerfile ในโฟลเดอร์ปัจจุบัน |
docker build --progress=plain ... | แสดงผลทุกขั้นของการ build |
docker build --check . | ตรวจ Dockerfile ตามกฎของ Docker โดยไม่ build |
docker images, docker history IMAGE | แสดง image และ layer ของ image |
docker rmi IMAGE, docker image prune -f | ลบ image และลบ image ที่ไม่มี tag |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน
ชุด FP10306343-P06 จำนวน 12 ข้อ ข้อละ 1 คะแนน รวม 12 คะแนน ครอบคลุมสถาปัตยกรรม Docker Engine, ความต่างระหว่าง image/container/layer, การเขียน Dockerfile และการใช้ build cache, .dockerignore และ multi-stage build
เข้าสู่ระบบห้องเรียนด้วยบัญชีที่ลงทะเบียนในรายวิชา 10306343 แล้วตอบคำถามให้ครบ ระบบตรวจคำตอบและแสดงคำอธิบายเพื่อทบทวน เมื่อทำครบและส่งผล ระบบจะบันทึกคะแนนไว้กับบัญชีของผู้เรียน สามารถทำซ้ำเพื่อทบทวนได้ ส่วนคะแนนที่นำไปรวมในรายวิชาเป็นไปตามเกณฑ์และกำหนดส่งในสมุดคะแนน
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
ชุด FW10306343-P06 จำนวน 5 ข้อ ข้อละ 4 คะแนน รวม 20 คะแนน เป็นงานเขียนตอบที่ดัดแปลงจากแบบฝึกหัดเดิมด้วยแอป Python และสถานการณ์ใหม่ ครอบคลุมบทบาทของ client, daemon และ registry การเปิดพอร์ตของ container กับ address ที่แอปฟัง การอ่าน docker history เพื่อลดขนาด image การจัดลำดับ Dockerfile ให้ใช้ build cache และ multi-stage build ที่ต้องบังคับให้ test ถูกรัน
เข้าสู่ระบบห้องเรียนด้วยบัญชีที่ลงทะเบียนในรายวิชา 10306343 แล้วเปิดรอบส่งงานจากลิงก์ด้านล่าง แต่ละข้อบอกสิ่งที่ต้องส่งและเกณฑ์ที่ใช้ตรวจ พิมพ์คำตอบหรือแนบไฟล์ได้สูงสุด 8 ไฟล์ ผู้สอนตรวจตามเกณฑ์และแจ้งผลรายเกณฑ์ คะแนนที่นำไปรวมคือครั้งที่ดีที่สุดที่ตรวจแล้วและส่งก่อนกำหนดในสมุดคะแนน
ใช้ประกอบการตรวจตามรูบริก ไม่เปิดให้นักศึกษา
แบบฝึกหัดเพิ่มเติมสำหรับอภิปรายและเขียนตอบ
แบบฝึกหัดแบ่งเป็นสามกลุ่ม ได้แก่ ความเข้าใจ การประยุกต์ และการวิเคราะห์ปัญหา แต่ละข้อระบุผลลัพธ์การเรียนรู้ที่ฝึก
ก. ความเข้าใจ
- ก1 (LLO 6.2) อธิบายความต่างระหว่าง image กับ container และระบุว่าการแก้ไฟล์ขณะ container ทำงานเกิดใน layer ใด
- ก2 (LLO 6.2) ระบุหน้าที่ของ client, daemon และ registry และระบุว่าคำสั่ง
docker run node:22-slim node -vบนเครื่องที่ยังไม่มี image นี้เกี่ยวข้องกับส่วนใดบ้างตามลำดับ - ก3 (LLO 6.1) อธิบายความหมายของ
-p 8080:3000และระบุว่าแอปใน container ฟังพอร์ตใด
ข. การประยุกต์
- ข1 (LLO 6.2) จากผล
docker historyต่อไปนี้ ระบุบรรทัดของ Dockerfile ที่สร้าง layer ใหญ่ที่สุดของแอป (ไม่นับ base image) และบรรทัดที่ไม่เพิ่มขนาด พร้อมเหตุผล
CREATED BY SIZE
CMD ["node" "src/server.js"] 0B
EXPOSE [3000/tcp] 0B
USER node 0B
COPY /app/src ./src # buildkit 20.5kB
RUN /bin/sh -c npm ci --omit=dev # buildkit 9.6MB
COPY package.json package-lock.json ./ # bui… 45.1kB
WORKDIR /app 4.1kB
ENV NODE_ENV=production 0B
150MB- ข2 (LLO 6.1) เขียนคำสั่งรัน container จาก image
devops-app:0.2.0ชื่อstagingให้เข้าถึงได้ที่พอร์ต 9000 ของเครื่อง และคำสั่งดู log ของ container นั้น - ข3 (LLO 6.1) เรียงคำสั่งต่อไปนี้ให้เป็น Dockerfile ที่ใช้ build cache ได้ดีที่สุด:
COPY . .·FROM node:22-slim·RUN npm ci --omit=dev·CMD ["node", "src/server.js"]·COPY package.json package-lock.json ./·WORKDIR /app
ค. การวิเคราะห์ปัญหา
- ค1 (LLO 6.1) สมาชิกคนหนึ่ง build image ด้วย Dockerfile แบบ multi-stage ของทีมสำเร็จ ทั้งที่รู้ว่าเพิ่งแก้ test ให้ผิด เมื่อตรวจพบว่าเขาแก้บรรทัด
COPY --from=build /app/src ./srcเป็นCOPY src ./srcวิเคราะห์ว่าเหตุใด build จึงสำเร็จ - ค2 (LLO 6.2) ทีมหนึ่ง push image ขึ้น registry สาธารณะแล้วพบว่าไม่ได้ใช้
.dockerignoreและโฟลเดอร์ที่ build มีไฟล์.envวิเคราะห์ความเสี่ยงและสิ่งที่ทีมต้องทำทันที - ค3 (LLO 6.1)
docker stop devops-appใช้เวลาประมาณ 10 วินาทีทุกครั้งและ container จบด้วย exit code 137 วิเคราะห์สาเหตุและเสนอวิธีแก้ที่ไม่ต้องแก้โค้ดแอป
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
การศึกษาค้นคว้าเพิ่มเติม
- อ่าน Docker Docs: What is Docker? และ Storage drivers เพื่อเขียนใบงาน image, container และ layer (งานข้อ 2)
- อ่าน Docker Docs: Multi-stage builds แล้วเทียบกับ Dockerfile ของทีม บันทึกส่วนที่ทีมใช้และไม่ได้ใช้ใน Learning Log
- เลือกหัวข้องานต่อยอดจากรายการในภาคผนวก ก (Jenkins, Kubernetes ด้วย kind, Terraform, Ansible, Helm และ Argo CD, Platform Engineering) หรือหัวข้ออื่นที่ผู้สอนอนุมัติ แล้วส่งงานข้อ 3
- ทำแบบฝึกหัดท้ายบทให้ครบ และเขียน Learning Log ครั้งที่ 6
- (ไม่บังคับ สำหรับผู้สอนและผู้สนใจ) อ่าน OCI Image Format Specification ส่วน manifest และ layer เพื่อเข้าใจว่า image จาก Docker ใช้กับ registry อื่นได้อย่างไร
เอกสารอ้างอิง
แหล่งข้อมูลเพิ่มเติมสำหรับตัวอย่างและกรณีอธิบาย
ตรวจเอกสารออนไลน์เมื่อ 23 ตุลาคม 2569 ตัวเลขและเหตุการณ์ที่ระบุว่าสมมติใช้เพื่ออธิบายกลไก ไม่ใช่ผลทดลองหรือสถิติขององค์กรจริง
เอกสารอ้างอิงของเนื้อหาเดิม
- Docker Inc. (n.d.). Multi-stage builds. Docker Docs. https://docs.docker.com/build/building/multi-stage/
- Docker Inc. (n.d.). Storage drivers. Docker Docs. https://docs.docker.com/engine/storage/drivers/
- Docker Inc. (n.d.). What is Docker? Docker Docs. https://docs.docker.com/get-started/docker-overview/
- Open Container Initiative. (n.d.). OCI image format specification. https://github.com/opencontainers/image-spec/blob/main/spec.md