Images และ Dockerfile — ภาพรวมโมดูล
วัดบน 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
Docker image คืออะไร?
หัวข้อที่มีชื่อว่า “Docker image คืออะไร?”Docker image คือแพ็กเกจแบบอ่านอย่างเดียวที่บรรจุทุกอย่างที่จำเป็นต่อการรันโปรแกรม — filesystem ของ OS, runtime, library, โค้ดแอป และ configuration เริ่มต้น ลองมองเป็นสูตรอาหาร: สูตรไม่เคยเปลี่ยน แต่คุณอบเค้ก (container) จากสูตรนั้นได้กี่ชิ้นก็ได้
image ถูกสร้างจาก layer แบบอ่านอย่างเดียวที่ซ้อนกัน แต่ละ layer แทนการเปลี่ยนแปลงหนึ่งครั้ง — เพิ่มไฟล์ ติดตั้งแพ็กเกจ เขียน configuration เมื่อซ้อน layer ขึ้นไปคุณจะได้ filesystem ที่สมบูรณ์ แต่ Docker เก็บ (และส่งต่อ) เฉพาะ layer ที่เปลี่ยน ทำให้ image เล็ก
flowchart TB img["Image (read-only)"] l3["Layer 3: COPY app/ /app/ (your code)"] l2["Layer 2: RUN npm install (dependencies)"] l1["Layer 1: FROM node:24-alpine (base OS + runtime)"] img --> l3 --> l2 --> l1
container ที่กำลังรันคือ image บวก writable layer พิเศษหนึ่งชั้นด้านบน image ไม่ถูกแตะ — container เขียนลง layer ชั่วคราวของตัวเอง ลบ container (docker rm) แล้ว layer นั้นหายไป (เว้นแต่คุณบันทึกไว้); การหยุด container เฉย ๆ ยังเก็บมันไว้ (พิสูจน์ในบทเรียน lifecycle)
flowchart TB con["Container (running)"] w["Writable layer (container-specific changes, deleted with docker rm)"] l3["Layer 3 (read-only)"] l2["Layer 2 (read-only)"] l1["Layer 1 (read-only)"] con --> w --> l3 --> l2 --> l1
container สองตัวจาก image เดียวกัน ตัวหนึ่งเขียนไฟล์ 4 ไบต์: docker diff แสดงการเปลี่ยนแปลงเฉพาะ container นั้น และ docker ps -s แสดง writable layer เป็น 4B บน image ที่แชร์ virtual 8.66MB:
$ docker diff c1 # wrote /only-in-c1A /only-in-c1$ docker diff c2 # nothing$ docker ps -s --format 'table {{.Names}}\t{{.Size}}'NAMES SIZEc2 0B (virtual 8.66MB)c1 4B (virtual 8.66MB)Dockerfile คืออะไร?
หัวข้อที่มีชื่อว่า “Dockerfile คืออะไร?”Dockerfile คือไฟล์ข้อความธรรมดาที่บรรจุคำสั่งบอก Docker ว่าจะ build image อย่างไร บางคำสั่งสร้าง layer (RUN, COPY, ADD) และบางคำสั่งบันทึกแค่ metadata (CMD, ENV, EXPOSE, WORKDIR เมื่อไดเรกทอรีมีอยู่แล้ว) คุณรัน docker build เพื่อเปลี่ยน Dockerfile เป็น image และ docker run เพื่อ start container จาก image นั้น
นี่คือ Dockerfile ที่ง่ายที่สุด:
# Dockerfile# @build# syntax=docker/dockerfile:1FROM alpine:3.24CMD ["echo", "Hello, Docker!"]Build และรัน:
docker build -t hello .docker run --rm hellooutput ที่คาดหวัง:
Hello, Docker!ตัว build เอง (plain progress, image cache ในเครื่องอยู่แล้ว):
#1 [internal] load build definition from Dockerfile#2 resolve image config for docker-image://docker.io/docker/dockerfile:1 # from the "# syntax" line#7 [1/1] FROM docker.io/library/alpine:3.24#7 CACHED#8 exporting to image#8 naming to docker.io/library/hello doneสังเกต #2: บรรทัด # syntax=docker/dockerfile:1 ทำให้ BuildKit ดึง image frontend ของ Dockerfile จาก Docker Hub (เรียกเครือข่ายครั้งแรก แล้ว cache) ลบบรรทัดนี้เพื่อ build แบบ offline โดยใช้ frontend ที่รวมมาใน BuildKit; คงไว้เมื่อต้องการฟีเจอร์ Dockerfile ล่าสุด
พิสูจน์ layer
หัวข้อที่มีชื่อว่า “พิสูจน์ layer”ดู image ที่คุณเพิ่ง build จากสามมุม
$ docker history helloIMAGE CREATED CREATED BY SIZE COMMENT216be2c70742 12 days ago CMD ["echo" "Hello, Docker!"] 0B buildkit.dockerfile.v0<missing> 12 days ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0<missing> 12 days ago ADD alpine-minirootfs-3.24.2-aarch64.tar.gz … 8.66MB buildkit.dockerfile.v0$ docker image inspect hello --format 'layers={{len .RootFS.Layers}} size={{.Size}}'layers=1 size=8660054docker history แสดง history entry สามรายการ แต่ image มี filesystem layer เพียง หนึ่งชั้น (RootFS.Layers): entry CMD สองรายการเป็น metadata ขนาด 0B history entry กับ layer ไม่ใช่สิ่งเดียวกัน <missing> หมายถึง entry นั้นเป็นของ parent image ที่คุณไม่ได้ build ในเครื่อง
docker save แสดง layer เป็นไฟล์: archive ของ image มีหนึ่ง blob ต่อ layer (tar 1b349a33… คือ root filesystem ขนาด 8.66 MB เหมือนกับที่อยู่ใน alpine:3.24) พร้อม config และ manifest JSON:
$ docker save hello | tar tblobs/sha256/1b349a3334531b575b01d409540dd8aca14ef29abddacd50f36643d7c949db4b # the layerblobs/sha256/216be2c707428d3874e3ad6a2b12828c9aef44f61fa5f4730f80b1c5cbcf3ac5 # image config...manifest.json$ docker save hello | tar xO manifest.json # "Layers": ["blobs/sha256/1b349a33…"]layer ถูกระบุด้วยเนื้อหา (content-addressed): docker image inspect alpine:3.24 --format '{{index .RootFS.Layers 0}}' พิมพ์ sha256:1b349a33… ตัวเดียวกัน ทุก image ที่ build FROM alpine:3.24 ใช้ layer นั้นซ้ำบนดิสก์แทนการคัดลอก
โมดูลนี้ครอบคลุมอะไร
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไร”| บทเรียน | สิ่งที่คุณจะได้เรียนรู้ |
|---|---|
| หน้านี้ | image กับ container, layer, การ build ครั้งแรก |
| Dockerfile basics | คำสั่งหลัก: FROM, WORKDIR, COPY, RUN, ENV, EXPOSE |
| Build & tag | build context, -t name:tag, docker images, .dockerignore |
| Layers & cache | build cache ทำงานอย่างไร การเรียงคำสั่งเพื่อความเร็ว |
| CMD vs ENTRYPOINT | exec form, shell form, argument เริ่มต้น, การ override |
ลงมือทำ: build ครั้งแรกของคุณ
หัวข้อที่มีชื่อว่า “ลงมือทำ: build ครั้งแรกของคุณ”วาง snippet ด้านล่างลงใน session ของ Play with Docker รันสองคำสั่ง และดู Docker build image ตัวเล็กตั้งแต่ต้น
# syntax=docker/dockerfile:1
FROM alpine:3.24
CMD ["echo", "Hello, Docker!"]
# --- build & run ---
# docker build -t hello .
# docker run --rm helloReading AI-generated code
หัวข้อที่มีชื่อว่า “Reading AI-generated code”Dockerfile ที่ AI เขียนและอ้างว่า “ล้างของตัวเองเพื่อให้ image เล็ก”:
🐛 หา bug ในโค้ดนี้
FROM alpine:3.24# AI: "generate the data, then delete it so the final image stays small"RUN dd if=/dev/zero of=/tmp/big.bin bs=1M count=50RUN rm /tmp/big.binCMD ["ls", "-la", "/tmp"]เฉลย
ไฟล์หายไปจาก container ที่รัน แต่ image ไม่เล็กลง: layer เป็นแบบสะสม rm เพิ่มแค่ layer “whiteout” ที่ซ่อนไฟล์ ส่วน layer ที่สร้างไฟล์ยังถูกส่งไปด้วย build และวัดแล้ว:
$ docker image lsdocker-b1-bloat 61.1MBdocker-b1-lean 8.66MB$ docker history bloatRUN /bin/sh -c rm /tmp/big.bin # buildkit 0BRUN /bin/sh -c dd if=/dev/zero of=/tmp/big.b… 52.4MB # still thereCMD ["/bin/sh"] 0B$ docker image inspect bloat --format 'layers={{len .RootFS.Layers}}'layers=361.1 MB เทียบกับ 8.66 MB สำหรับผลลัพธ์ที่มองเห็นเหมือนกัน ทุก docker pull ปลายทางต้องจ่าย 52.4 MB นั้น ให้สร้างและลบใน RUN เดียวกัน เพื่อให้ layer ถูกจับหลังการล้าง:
# @buildFROM alpine:3.24RUN dd if=/dev/zero of=/tmp/big.bin bs=1M count=50 2>/dev/null && rm /tmp/big.binCMD ["ls", "-la", "/tmp"]กฎเดียวกันนี้คือเหตุผลที่การล้างของ package manager (apt-get install … && rm -rf /var/lib/apt/lists/*) ต้องอยู่ในคำสั่งที่ติดตั้ง ส่วนความลับแย่กว่า: ความลับที่เขียนใน layer หนึ่งแล้วลบใน layer ถัดไปยังกู้คืนได้จาก layer แรก
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”image คือ stack ของ filesystem diff บวก config (คำสั่งเริ่มต้น, environment, port) มันโตขึ้นด้านบนอย่างเดียว: layer ที่ตามมาซ่อนไฟล์ได้แต่ลบไบต์จาก layer ล่างไม่ได้ container คือ stack นั้นที่ทำให้เขียนได้ผ่าน copy-on-write layer ด้านบน บวก process ที่กำลังรัน layer ถูกแชร์ตาม content hash จึงทำให้ image สิบตัวบน base เดียวกันเสียค่า base แค่หนึ่งชุด
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ข้อดี | ข้อเสีย |
|---|---|---|
| layer เล็กหลายชั้น | reuse cache และแชร์ได้ละเอียด | เนื้อหาที่ลบแล้วยังถูกส่งไปด้วย history รกขึ้น |
layer ใหญ่ไม่กี่ชั้น (เชื่อมด้วย &&) | การล้างทำให้ image เล็กลงจริง | การเปลี่ยนอะไรก็ตาม rebuild ทั้ง layer |
# syntax=docker/dockerfile:1 | ฟีเจอร์ Dockerfile ล่าสุด พฤติกรรมเหมือนกันทุกที่ | ต้องดึง frontend ผ่านเครือข่ายเพิ่มเมื่อ cache ว่าง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ทุกคำสั่งสร้าง layer” เฉพาะคำสั่งที่เปลี่ยน filesystem image hello มี history 3 รายการและ 1 layer
- “
rmในRUNถัดไปทำให้ image เล็กลง” มันซ่อนไฟล์ image ยังคง 61.1 MB - “การหยุด container ลบการเปลี่ยนแปลงของมัน”
docker rmลบ; การหยุดเก็บ writable layer ไว้ - “image คือดิสก์ของ VM” มันไม่มี kernel หรือ init; เป็น filesystem บวก metadata ที่ container runtime แตกออกและรันบน kernel ของ host