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

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 คือแพ็กเกจแบบอ่านอย่างเดียวที่บรรจุทุกอย่างที่จำเป็นต่อการรันโปรแกรม — 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
image คือ stack ของ layer แบบอ่านอย่างเดียว

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 เพิ่ม writable layer ไว้บน image

container สองตัวจาก image เดียวกัน ตัวหนึ่งเขียนไฟล์ 4 ไบต์: docker diff แสดงการเปลี่ยนแปลงเฉพาะ container นั้น และ docker ps -s แสดง writable layer เป็น 4B บน image ที่แชร์ virtual 8.66MB:

$ docker diff c1 # wrote /only-in-c1
A /only-in-c1
$ docker diff c2 # nothing
$ docker ps -s --format 'table {{.Names}}\t{{.Size}}'
NAMES SIZE
c2 0B (virtual 8.66MB)
c1 4B (virtual 8.66MB)

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:1
FROM alpine:3.24
CMD ["echo", "Hello, Docker!"]

Build และรัน:

Terminal window
docker build -t hello .
docker run --rm hello

output ที่คาดหวัง:

Hello, Docker!

ตัว build เอง (plain progress, image cache ในเครื่องอยู่แล้ว):

3.24
#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 ล่าสุด

ดู image ที่คุณเพิ่ง build จากสามมุม

$ docker history hello
IMAGE CREATED CREATED BY SIZE COMMENT
216be2c70742 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=8660054

docker 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 t
blobs/sha256/1b349a3334531b575b01d409540dd8aca14ef29abddacd50f36643d7c949db4b # the layer
blobs/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 & tagbuild context, -t name:tag, docker images, .dockerignore
Layers & cachebuild cache ทำงานอย่างไร การเรียงคำสั่งเพื่อความเร็ว
CMD vs ENTRYPOINTexec form, shell form, argument เริ่มต้น, การ override

วาง 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 hello

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=50
RUN rm /tmp/big.bin
CMD ["ls", "-la", "/tmp"]
เฉลย

ไฟล์หายไปจาก container ที่รัน แต่ image ไม่เล็กลง: layer เป็นแบบสะสม rm เพิ่มแค่ layer “whiteout” ที่ซ่อนไฟล์ ส่วน layer ที่สร้างไฟล์ยังถูกส่งไปด้วย build และวัดแล้ว:

$ docker image ls
docker-b1-bloat 61.1MB
docker-b1-lean 8.66MB
$ docker history bloat
RUN /bin/sh -c rm /tmp/big.bin # buildkit 0B
RUN /bin/sh -c dd if=/dev/zero of=/tmp/big.b… 52.4MB # still there
CMD ["/bin/sh"] 0B
$ docker image inspect bloat --format 'layers={{len .RootFS.Layers}}'
layers=3

61.1 MB เทียบกับ 8.66 MB สำหรับผลลัพธ์ที่มองเห็นเหมือนกัน ทุก docker pull ปลายทางต้องจ่าย 52.4 MB นั้น ให้สร้างและลบใน RUN เดียวกัน เพื่อให้ layer ถูกจับหลังการล้าง:

Dockerfile.lean
# @build
FROM alpine:3.24
RUN dd if=/dev/zero of=/tmp/big.bin bs=1M count=50 2>/dev/null && rm /tmp/big.bin
CMD ["ls", "-la", "/tmp"]

กฎเดียวกันนี้คือเหตุผลที่การล้างของ package manager (apt-get install … && rm -rf /var/lib/apt/lists/*) ต้องอยู่ในคำสั่งที่ติดตั้ง ส่วนความลับแย่กว่า: ความลับที่เขียนใน layer หนึ่งแล้วลบใน layer ถัดไปยังกู้คืนได้จาก layer แรก

image คือ stack ของ filesystem diff บวก config (คำสั่งเริ่มต้น, environment, port) มันโตขึ้นด้านบนอย่างเดียว: layer ที่ตามมาซ่อนไฟล์ได้แต่ลบไบต์จาก layer ล่างไม่ได้ container คือ stack นั้นที่ทำให้เขียนได้ผ่าน copy-on-write layer ด้านบน บวก process ที่กำลังรัน layer ถูกแชร์ตาม content hash จึงทำให้ image สิบตัวบน base เดียวกันเสียค่า base แค่หนึ่งชุด

ทางเลือกข้อดีข้อเสีย
layer เล็กหลายชั้นreuse cache และแชร์ได้ละเอียดเนื้อหาที่ลบแล้วยังถูกส่งไปด้วย history รกขึ้น
layer ใหญ่ไม่กี่ชั้น (เชื่อมด้วย &&)การล้างทำให้ image เล็กลงจริงการเปลี่ยนอะไรก็ตาม rebuild ทั้ง layer
# syntax=docker/dockerfile:1ฟีเจอร์ Dockerfile ล่าสุด พฤติกรรมเหมือนกันทุกที่ต้องดึง frontend ผ่านเครือข่ายเพิ่มเมื่อ cache ว่าง
  • “ทุกคำสั่งสร้าง 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
Docker image คืออะไร?
เกิดอะไรขึ้นกับ writable layer ของ container เมื่อ container ถูกลบ?
คำสั่งใดเปลี่ยน Dockerfile เป็น image?
docker history ของ Dockerfile 2 บรรทัด (FROM alpine:3.24, CMD [...]) แสดงสามรายการ แต่ RootFS.Layers มี 1 element ทำไม?
Dockerfile มี RUN dd ... /tmp/big.bin (50 MB) แล้ว RUN rm /tmp/big.bin แยกกัน ขนาด image เป็นอย่างไรเทียบกับทำทั้งสองใน RUN เดียว?