M1N

ชุดสื่อการสอน Docker (Docker Teaching Kit)

กำลังโหลดเนื้อหา

Updates & Additions (2)

09/07/2026 17:32
# Docker ฉบับสมบูรณ์: จากศูนย์ถึงใช้งานจริง > เอกสารประกอบการสอน — ครอบคลุมตั้งแต่แนวคิดพื้นฐาน สถาปัตยกรรม คำสั่งใช้งาน Dockerfile, Volumes, Networking, Docker Compose ไปจนถึง Security และการใช้งานใน Production --- ## สารบัญ 1. [ทำไมต้อง Docker](#1-ทำไมต้อง-docker) 2. [Container คืออะไร — เทียบกับ Virtual Machine](#2-container-คืออะไร--เทียบกับ-virtual-machine) 3. [เทคโนโลยีเบื้องหลัง Container](#3-เทคโนโลยีเบื้องหลัง-container) 4. [สถาปัตยกรรมของ Docker](#4-สถาปัตยกรรมของ-docker) 5. [Image, Container และ Registry](#5-image-container-และ-registry) 6. [คำสั่งพื้นฐานที่ต้องใช้ให้เป็น](#6-คำสั่งพื้นฐานที่ต้องใช้ให้เป็น) 7. [Dockerfile ฉบับละเอียด](#7-dockerfile-ฉบับละเอียด) 8. [Layer Caching และ .dockerignore](#8-layer-caching-และ-dockerignore) 9. [Multi-stage Build](#9-multi-stage-build) 10. [การจัดการข้อมูล: Volumes และ Bind Mounts](#10-การจัดการข้อมูล-volumes-และ-bind-mounts) 11. [Networking](#11-networking) 12. [Docker Compose](#12-docker-compose) 13. [Registry และการจัดการ Tag](#13-registry-และการจัดการ-tag) 14. [Security](#14-security) 15. [การใช้งานใน Production](#15-การใช้งานใน-production) 16. [Troubleshooting](#16-troubleshooting) 17. [สรุป Best Practices](#17-สรุป-best-practices) 18. [ก้าวต่อไป](#18-ก้าวต่อไป) --- ## 1. ทำไมต้อง Docker ปัญหาคลาสสิกของทีมพัฒนาซอฟต์แวร์คือประโยค **"มันรันได้บนเครื่องผมนะ"** (It works on my machine) — แอปทำงานได้ดีบนเครื่อง dev แต่พอย้ายไป test หรือ production กลับพัง สาเหตุมักมาจาก: - เวอร์ชันของ runtime ไม่ตรงกัน (Node 18 กับ Node 22, Python 3.9 กับ 3.12) - Library หรือ system dependency บนเครื่องไม่เหมือนกัน - Config, environment variable, path ของไฟล์ต่างกัน - OS คนละตัว คนละเวอร์ชัน (dev ใช้ macOS, prod ใช้ RHEL) Docker แก้ปัญหานี้ด้วยแนวคิดเดียว: **แพ็กแอปพลิเคชัน + dependencies ทั้งหมด + config ลงใน "กล่อง" เดียว** เรียกว่า **container** แล้วรันกล่องนี้ที่ไหนก็ได้ที่มี Docker — ผลลัพธ์เหมือนกันทุกที่ เปรียบเทียบกับตู้คอนเทนเนอร์ขนส่งสินค้า: ก่อนมีตู้คอนเทนเนอร์ การขนของขึ้นเรือแต่ละชิ้นต้องจัดการต่างกันหมด แต่พอทุกอย่างอยู่ในตู้ขนาดมาตรฐาน เรือ รถบรรทุก รถไฟ เครนก็จัดการได้เหมือนกันหมด — Docker คือ "ตู้คอนเทนเนอร์มาตรฐาน" ของซอฟต์แวร์ ประโยชน์หลักที่ได้: | ประโยชน์ | คำอธิบาย | |---|---| | **Consistency** | Dev, Test, Prod ใช้ image เดียวกัน ตัดปัญหา environment drift | | **Isolation** | แต่ละแอปมี dependencies ของตัวเอง ไม่ตีกัน (Python 2 กับ 3 รันคู่กันได้) | | **Speed** | Container start ในระดับวินาที ไม่ต้องรอ OS boot | | **Density** | เครื่องเดียวรันได้หลายสิบ container เพราะไม่ต้องแบก Guest OS | | **Portability** | Build ครั้งเดียว รันได้ทั้ง laptop, on-premise server, cloud | | **Version control ของ environment** | Dockerfile คือ "โค้ด" ที่บรรยาย environment เก็บใน Git ได้ review ได้ | --- ## 2. Container คืออะไร — เทียบกับ Virtual Machine **Container** คือ process (หรือกลุ่ม process) ที่ถูก "แยกส่วน" (isolate) ออกจากระบบส่วนอื่น โดยมี filesystem, network, process tree ของตัวเอง แต่ยังคง **ใช้ kernel ร่วมกับ Host OS** **Virtual Machine (VM)** คือการจำลองเครื่องคอมพิวเตอร์ทั้งเครื่อง — มี virtual hardware และต้องติดตั้ง Guest OS เต็มตัวในแต่ละ VM ``` Virtual Machine Container ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ App A │ │ App B │ │ App C │ │ App A │ │ App B │ │ App C │ ├─────────┤ ├─────────┤ ├─────────┤ ├───────┤ ├───────┤ ├───────┤ │Bins/Libs│ │Bins/Libs│ │Bins/Libs│ │ Bins/ │ │ Bins/ │ │ Bins/ │ ├─────────┤ ├─────────┤ ├─────────┤ │ Libs │ │ Libs │ │ Libs │ │Guest OS │ │Guest OS │ │Guest OS │ └───┬───┘ └───┬───┘ └───┬───┘ └────┬────┘ └────┬────┘ └────┬────┘ ┌───┴─────────┴─────────┴───┐ ┌────┴────────────┴───────────┴────┐ │ Docker Engine │ │ Hypervisor │ ├───────────────────────────┤ ├──────────────────────────────────┤ │ Host OS (kernel) │ │ Host OS │ ├───────────────────────────┤ ├──────────────────────────────────┤ │ Infrastructure │ │ Infrastructure │ └───────────────────────────┘ └──────────────────────────────────┘ ``` | หัวข้อ | Virtual Machine | Container | |---|---|---| | ระดับการจำลอง | จำลอง hardware ทั้งเครื่อง | แยก process ในระดับ OS | | Guest OS | ต้องมี (แต่ละ VM) | ไม่ต้องมี — ใช้ kernel ของ host | | ขนาด | หลาย GB | หลักสิบ–ร้อย MB (บางทีไม่กี่ MB) | | เวลา start | นาที | วินาที / มิลลิวินาที | | Overhead | สูง (CPU, RAM ต่อ VM) | ต่ำมาก (ใกล้เคียง native process) | | Isolation | แข็งแรงมาก (hardware-level) | ดี แต่ต่ำกว่า VM (แชร์ kernel) | | Density ต่อเครื่อง | หลักหน่วย–สิบ | หลักสิบ–ร้อย | > **จุดที่คนเข้าใจผิดบ่อย:** Container **ไม่ใช่** VM ขนาดเล็ก — มันคือ process ธรรมดาบน host ที่ถูกจำกัดมุมมองและทรัพยากร ลอง `ps aux` บน host จะเห็น process ของ container ปนอยู่ด้วย > > **ข้อควรจำ:** Container ใช้ kernel ของ host ดังนั้น Linux container ต้องรันบน Linux kernel — บน Windows/macOS นั้น Docker Desktop จะรัน Linux VM เล็กๆ ให้เบื้องหลังโดยอัตโนมัติ --- ## 3. เทคโนโลยีเบื้องหลัง Container Docker ไม่ได้ประดิษฐ์เทคโนโลยีใหม่ทั้งหมด แต่นำความสามารถที่มีอยู่ใน Linux kernel มาประกอบกันให้ใช้ง่าย: ### 3.1 Namespaces — "มองเห็นแค่ของตัวเอง" Namespace ทำให้ process เห็นทรัพยากรเป็นชุดของตัวเอง แยกจาก process อื่น: | Namespace | แยกอะไร | |---|---| | `pid` | Process tree — ใน container จะเห็น process ของตัวเองเป็น PID 1 | | `net` | Network stack — มี interface, IP, routing table, port ของตัวเอง | | `mnt` | Mount points — เห็น filesystem ของตัวเอง | | `uts` | Hostname และ domain name | | `ipc` | Inter-process communication (shared memory ฯลฯ) | | `user` | User/Group ID — root ใน container ≠ root บน host ได้ | ### 3.2 Control Groups (cgroups) — "ใช้ได้แค่เท่าที่กำหนด" cgroups จำกัดและวัดการใช้ทรัพยากรของกลุ่ม process เช่น CPU, memory, disk I/O, network — เป็นกลไกเบื้องหลัง `--memory` และ `--cpus` ของ Docker ### 3.3 Union Filesystem (OverlayFS) — "ซ้อน layer แบบประหยัดพื้นที่" Filesystem ของ container เกิดจากการนำ layer แบบ read-only หลายชั้นมาซ้อนกัน แล้ววาง writable layer บางๆ ไว้ชั้นบนสุด (รายละเอียดในหัวข้อถัดไป) > **สรุปหนึ่งประโยค:** Container = process ปกติ + namespaces (จำกัดการมองเห็น) + cgroups (จำกัดทรัพยากร) + union filesystem (จำกัด/จัดการดิสก์) --- ## 4. สถาปัตยกรรมของ Docker Docker เป็นระบบแบบ client–server: ``` ┌──────────────┐ REST API ┌──────────────────────────────┐ ┌──────────────┐ │ Docker CLI │──────────────▶│ Docker Daemon (dockerd) │◀──────▶│ Registry │ │ (docker ...) │ unix socket │ ├─ containerd (จัดการ │ pull/ │ (Docker Hub, │ └──────────────┘ หรือ TCP │ │ lifecycle ของ container) │ push │ Harbor ฯลฯ) │ │ └─ runc (สร้าง container │ └──────────────┘ │ จริงๆ ด้วย namespaces) │ └──────────────────────────────┘ ``` - **Docker Client (`docker`)** — CLI ที่เราพิมพ์คำสั่ง แปลงเป็น REST API call ส่งให้ daemon - **Docker Daemon (`dockerd`)** — ตัวจัดการหลัก: build image, จัดการ container/volume/network - **containerd** — runtime ระดับกลาง จัดการ lifecycle ของ container (Kubernetes ก็ใช้ตัวนี้) - **runc** — ตัว spawn container จริงๆ โดยเรียกใช้ namespaces/cgroups ของ kernel - **Registry** — คลังเก็บ image เช่น Docker Hub (สาธารณะ) หรือ private registry ขององค์กร เมื่อสั่ง `docker run nginx` สิ่งที่เกิดขึ้นตามลำดับ: 1. CLI ส่งคำสั่งให้ daemon 2. Daemon เช็คว่ามี image `nginx` ในเครื่องหรือไม่ — ถ้าไม่มี จะ `pull` จาก registry 3. สร้าง container จาก image (เพิ่ม writable layer, สร้าง network namespace, ตั้ง IP) 4. Start process ตามที่ image กำหนด (`CMD`/`ENTRYPOINT`) --- ## 5. Image, Container และ Registry สามคำนี้ต้องแยกให้ชัดตั้งแต่วันแรก: | คำ | ความหมาย | เปรียบเทียบ | |---|---|---| | **Image** | แม่แบบ read-only ที่บรรจุ filesystem + metadata (คำสั่งที่จะรัน, port, env) | Class / พิมพ์เขียว / แผ่นเกม | | **Container** | instance ที่รันจาก image — image + writable layer + process ที่กำลังทำงาน | Object / บ้านที่สร้างจากพิมพ์เขียว / เกมที่กำลังเล่น | | **Registry** | ที่เก็บและแจกจ่าย image | Git remote / App Store | จาก image เดียว สร้าง container ได้กี่ตัวก็ได้ แต่ละตัวแยกขาดจากกัน ### 5.1 Image ประกอบด้วย Layers Image ไม่ใช่ไฟล์ก้อนเดียว แต่คือ **กองของ layer แบบ read-only** ที่ซ้อนกัน — แต่ละ layer เกิดจากคำสั่งใน Dockerfile ที่เปลี่ยนแปลง filesystem (`FROM`, `RUN`, `COPY`, `ADD`) ``` ┌─────────────────────────────┐ │ Writable layer (container) │ ← เกิดตอน docker run — เขียนได้ หายเมื่อลบ container ├─────────────────────────────┤ │ Layer 3: COPY . . │ ← โค้ดแอป ┐ ├─────────────────────────────┤ │ │ Layer 2: RUN npm ci │ ← dependencies ├ Image (read-only) ├─────────────────────────────┤ │ │ Layer 1: FROM node:22 │ ← base image ┘ └─────────────────────────────┘ ``` กลไกที่ทำให้ระบบนี้ประหยัดและเร็วคือ **Copy-on-Write (CoW):** - Container อ่านไฟล์จาก layer ด้านล่างได้โดยตรง (ไม่ก๊อป) - เมื่อ container จะ "แก้" ไฟล์ที่อยู่ใน layer read-only ระบบจะก๊อปไฟล์นั้นขึ้นมาที่ writable layer ก่อนแล้วค่อยแก้ - Layer ถูก **แชร์ระหว่าง image และ container** — ถ้ามี 10 container จาก image เดียวกัน ดิสก์ถูกใช้เพิ่มแค่ writable layer ของแต่ละตัว ผลที่ตามมาที่สำคัญมาก: **ข้อมูลที่เขียนใน writable layer จะหายไปเมื่อ container ถูกลบ** — นี่คือเหตุผลที่ต้องมี Volumes (หัวข้อ 10) ### 5.2 การตั้งชื่อ Image รูปแบบเต็ม: `[registry-host[:port]/][namespace/]repository[:tag][@digest]` ```bash nginx # = docker.io/library/nginx:latest nginx:1.27-alpine # ระบุ tag ghcr.io/myorg/api:2.4.1 # registry อื่น (GitHub Container Registry) registry.mycompany.co.th/esb/gateway:1.0.3 # private registry ภายในองค์กร nginx@sha256:abc123... # อ้างด้วย digest — ชี้ image เดิมเป๊ะๆ เสมอ ``` > **`latest` ไม่ได้แปลว่า "ใหม่ล่าสุด"** — มันเป็นแค่ tag ปริยาย (default) เมื่อไม่ระบุ tag และชี้ไปที่ไหนก็ได้แล้วแต่คน push ล่าสุด ใน production ควรระบุเวอร์ชันชัดเจนเสมอ --- ## 6. คำสั่งพื้นฐานที่ต้องใช้ให้เป็น ### 6.1 รัน Container ```bash # Run in foreground (see output directly, Ctrl+C to stop) docker run nginx # Common options combined docker run -d \ --name web \ -p 8080:80 \ -e TZ=Asia/Bangkok \ --restart unless-stopped \ nginx:1.27-alpine ``` | Option | ความหมาย | |---|---| | `-d` | Detached — รันเบื้องหลัง คืน container ID มาให้ | | `--name web` | ตั้งชื่อ container (ถ้าไม่ตั้ง Docker จะสุ่มชื่อให้) | | `-p 8080:80` | Publish port — map **host:container** (เข้าที่ host:8080 → ส่งต่อไป container:80) | | `-e KEY=VALUE` | ตั้ง environment variable | | `-it` | Interactive + TTY — ใช้ตอนต้องพิมพ์โต้ตอบ เช่นเปิด shell | | `--rm` | ลบ container อัตโนมัติเมื่อหยุด (เหมาะกับงานชั่วคราว) | | `-v` / `--mount` | Mount volume หรือ bind mount (หัวข้อ 10) | | `--restart unless-stopped` | ให้ Docker restart ให้อัตโนมัติเมื่อ crash หรือเครื่อง reboot | ```bash # Interactive shell in a throwaway container docker run -it --rm alpine:3.21 sh ``` ### 6.2 ดูสถานะ ```bash docker ps # containers that are running docker ps -a # all containers, including stopped ones docker images # local images docker logs web # container logs docker logs -f --tail 100 web # follow logs, last 100 lines docker stats # live CPU/RAM usage per container docker inspect web # full details in JSON (IP, mounts, env, ...) docker top web # processes inside the container ``` ### 6.3 เข้าไปใน Container ที่รันอยู่ ```bash # Open a shell inside a running container docker exec -it web sh # alpine-based images have sh docker exec -it web bash # debian/ubuntu-based images have bash # Run a single command without opening a shell docker exec web cat /etc/nginx/nginx.conf ``` > `docker exec` คือเครื่องมือ debug อันดับหนึ่ง — เข้าไปดูไฟล์ ทดสอบ network เช็ค process ได้เหมือน ssh เข้าเครื่อง ### 6.4 หยุด / ลบ / เก็บกวาด ```bash docker stop web # graceful stop (SIGTERM, then SIGKILL after 10s) docker start web # start a stopped container again docker restart web docker kill web # force kill (SIGKILL) immediately docker rm web # remove a stopped container docker rm -f web # force remove even if running docker rmi nginx:1.27-alpine # remove an image # Housekeeping docker container prune # remove all stopped containers docker image prune # remove dangling images (no tag) docker image prune -a # remove ALL unused images docker system prune # containers + networks + dangling images docker system df # how much disk Docker is using ``` ### 6.5 คัดลอกไฟล์เข้า/ออก ```bash docker cp ./config.json web:/app/config.json # host -> container docker cp web:/var/log/app.log ./app.log # container -> host ``` --- ## 7. Dockerfile ฉบับละเอียด **Dockerfile** คือไฟล์ text ที่บรรยายวิธีสร้าง image ทีละขั้น — เป็น "สูตรอาหาร" ที่ทำซ้ำได้เหมือนเดิมทุกครั้ง และเก็บใน Git ได้ ตัวอย่าง Dockerfile สำหรับแอป Node.js: ```dockerfile # Base image: pin the version, use slim variant FROM node:22-alpine # Set working directory (created automatically if missing) WORKDIR /app # Copy dependency manifests first to leverage layer caching COPY package*.json ./ # Install production dependencies only RUN npm ci --omit=dev # Copy application source code COPY . . # Document the port the app listens on (does NOT actually open it) EXPOSE 3000 # Drop privileges: run as the built-in non-root user USER node # Default command (exec form) CMD ["node", "server.js"] ``` Build และรัน: ```bash docker build -t myapp:1.0 . # "." = build context (โฟลเดอร์ปัจจุบัน) docker run -d -p 3000:3000 myapp:1.0 ``` ### 7.1 Instruction ทีละตัว #### `FROM` — จุดเริ่มต้นของทุก image ```dockerfile FROM node:22-alpine FROM golang:1.24-alpine AS builder # ตั้งชื่อ stage (ใช้ใน multi-stage) FROM scratch # image เปล่าสนิท (สำหรับ static binary) ``` หลักการเลือก base image: **เล็กที่สุดที่ยังทำงานได้** — `alpine` (~5 MB) < `slim` (~80 MB) < ตัวเต็ม (หลายร้อย MB) และ **pin เวอร์ชันเสมอ** อย่าใช้ `latest` #### `WORKDIR` — กำหนด directory ทำงาน ```dockerfile WORKDIR /app # ทุก RUN/COPY/CMD หลังจากนี้ทำงานใน /app ``` ใช้ `WORKDIR` แทนการ `RUN cd /app` (ซึ่งไม่มีผลข้าม layer) #### `COPY` vs `ADD` ```dockerfile COPY src/ /app/src/ # copy from build context into image COPY --chown=node:node . . # copy and set owner in one step ADD https://example.com/file.tgz /tmp/ # ADD can fetch URLs ADD archive.tar.gz /opt/ # ADD auto-extracts local tar files ``` > **กฎง่ายๆ:** ใช้ `COPY` เป็นหลักเสมอ — พฤติกรรมตรงไปตรงมา ใช้ `ADD` เฉพาะเมื่อต้องการความสามารถแตก tar อัตโนมัติจริงๆ (การดึงไฟล์จาก URL ควรใช้ `RUN curl/wget` เพื่อควบคุม checksum ได้) #### `RUN` — รันคำสั่งตอน build (สร้าง layer ใหม่) ```dockerfile # Each RUN creates one layer -> chain related commands with && RUN apt-get update && \ apt-get install -y --no-install-recommends curl ca-certificates && \ rm -rf /var/lib/apt/lists/* ``` เหตุที่ต้องล้าง cache (`rm -rf /var/lib/apt/lists/*`) **ในคำสั่ง RUN เดียวกัน**: ถ้าแยกไปอีก `RUN` หนึ่ง ไฟล์จะยังอยู่ใน layer ก่อนหน้า — ขนาด image ไม่ลด #### `ENV` vs `ARG` ```dockerfile ARG APP_VERSION=dev # build-time only: docker build --build-arg APP_VERSION=1.2.3 ENV TZ=Asia/Bangkok # available at build time AND runtime ENV APP_VERSION=$ARG_... # persist an ARG into runtime if needed ``` | | `ARG` | `ENV` | |---|---|---| | มีผลตอน build | มี | มี | | มีผลตอน runtime (ใน container) | **ไม่มี** | มี | | Override ได้ด้วย | `--build-arg` | `docker run -e` | > **ห้ามใส่ secret ใน ENV/ARG** — ค่าจะติดอยู่ใน image metadata ดูได้ด้วย `docker history` / `docker inspect` #### `EXPOSE` — เอกสาร ไม่ใช่การเปิด port ```dockerfile EXPOSE 8080 ``` `EXPOSE` เป็นเพียง metadata บอกว่าแอปฟัง port ไหน — **ไม่ได้เปิด port ให้ภายนอกเข้าถึง** การเปิดจริงต้องใช้ `-p` ตอน `docker run` #### `CMD` vs `ENTRYPOINT` — คู่ที่สับสนที่สุด | | `CMD` | `ENTRYPOINT` | |---|---|---| | บทบาท | คำสั่ง/argument **ปริยาย** | คำสั่ง **หลักตายตัว** ของ container | | ถูกแทนที่เมื่อ | ผู้ใช้ใส่ argument ท้าย `docker run` | ต้องใช้ `--entrypoint` ถึงจะเปลี่ยน | ใช้คู่กัน: `ENTRYPOINT` เป็นตัวโปรแกรม, `CMD` เป็น argument ปริยาย ```dockerfile ENTRYPOINT ["ping"] CMD ["localhost"] ``` ```bash docker run myping # runs: ping localhost docker run myping google.com # runs: ping google.com (CMD ถูกแทนที่) ``` **Exec form vs Shell form:** ```dockerfile CMD ["node", "server.js"] # exec form (recommended): node becomes PID 1 CMD node server.js # shell form: runs as "/bin/sh -c ..." -> sh is PID 1 ``` ใช้ **exec form** เสมอ — ถ้าใช้ shell form ตัว shell จะเป็น PID 1 และมักไม่ส่งต่อ SIGTERM ให้แอป ทำให้ `docker stop` ต้องรอ timeout แล้วโดน SIGKILL (graceful shutdown ไม่ทำงาน) #### `USER` — อย่ารันเป็น root ```dockerfile # Debian/Ubuntu base RUN groupadd -r app && useradd -r -g app app # Alpine base RUN addgroup -S app && adduser -S app -G app USER app ``` ค่าปริยายของ container คือ root — ถ้าแอปโดนเจาะ ผู้โจมตีได้ root ใน container ทันที (และเป็นบันไดต่อไปยัง host ได้ในบางกรณี) #### `HEALTHCHECK` — ให้ Docker รู้ว่าแอป "ใช้งานได้จริง" ไม่ใช่แค่ process ยังอยู่ ```dockerfile HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:8080/health || exit 1 ``` สถานะจะแสดงใน `docker ps` เป็น `healthy` / `unhealthy` และใช้ร่วมกับ `depends_on: condition: service_healthy` ใน Compose ได้ #### อื่นๆ ที่ควรรู้ ```dockerfile LABEL org.opencontainers.image.source="https://github.com/myorg/myapp" VOLUME /data # declare a mount point (anonymous volume if not overridden) SHELL ["/bin/bash", "-c"] STOPSIGNAL SIGQUIT # e.g. nginx prefers SIGQUIT for graceful stop ``` --- ## 8. Layer Caching และ .dockerignore ### 8.1 กลไก Cache ตอน Build Docker build ทีละ instruction และ **cache ผลลัพธ์ของแต่ละ layer** — ถ้า instruction และ input ไม่เปลี่ยน จะใช้ cache เดิม (เห็นเป็น `CACHED` ใน log) แต่ **ถ้า layer ใดแตก cache — ทุก layer ถัดจากนั้นต้อง build ใหม่ทั้งหมด** จึงเกิดหลักการทอง: **เรียงจากสิ่งที่เปลี่ยนน้อย → เปลี่ยนบ่อย** ```dockerfile # BAD: any code change invalidates the npm install layer COPY . . RUN npm ci # GOOD: npm install layer is reused as long as package files are unchanged COPY package*.json ./ RUN npm ci --omit=dev COPY . . ``` ผลลัพธ์จริง: แก้โค้ด 1 บรรทัดแล้ว build ใหม่ — แบบแรกรอ `npm ci` ทุกครั้ง (อาจเป็นนาที) แบบหลังเสร็จในไม่กี่วินาที ### 8.2 `.dockerignore` Build context (ทุกไฟล์ในโฟลเดอร์ที่ระบุตอน `docker build`) จะถูกส่งให้ daemon ทั้งก้อน — ไฟล์ที่ไม่เกี่ยวทำให้ build ช้าและเสี่ยงหลุดเข้า image ``` # .dockerignore .git node_modules dist *.log .env .env.* Dockerfile docker-compose*.yml README.md ``` > `.env` ต้องอยู่ใน `.dockerignore` **เสมอ** — มิฉะนั้น `COPY . .` จะพา secret เข้า image ไปตลอดกาล (อยู่ใน layer แม้ภายหลังจะ `RUN rm` ทิ้ง) --- ## 9. Multi-stage Build ปัญหา: การ build แอปต้องใช้เครื่องมือเยอะ (compiler, SDK, dev dependencies) แต่ตอน **รัน** ต้องการแค่ผลลัพธ์สุดท้าย — ถ้า build ใน image เดียว ทุกอย่างจะติดไปด้วย ได้ image อ้วน 800 MB – 1 GB+ พร้อมพื้นที่โจมตี (attack surface) มหาศาล **Multi-stage build** แก้ด้วยการใช้หลาย `FROM` ในไฟล์เดียว — stage แรกไว้ build, stage สุดท้ายก๊อปเฉพาะผลลัพธ์มา: ```dockerfile # ---------- Build stage ---------- FROM golang:1.24-alpine AS builder WORKDIR /src # Cache dependencies first COPY go.mod go.sum ./ RUN go mod download # Build a fully static binary (no CGO) so it runs on minimal images COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app . # ---------- Runtime stage ---------- FROM alpine:3.21 RUN addgroup -S app && adduser -S app -G app && \ apk add --no-cache ca-certificates tzdata COPY --from=builder /out/app /usr/local/bin/app USER app EXPOSE 8080 ENTRYPOINT ["app"] ``` ผลลัพธ์ (แอป Go ตัวเดียวกัน): | วิธี | ขนาด image โดยประมาณ | |---|---| | Single-stage บน `golang:1.24` | **~900 MB** | | Multi-stage → `alpine` | **~15–25 MB** | | Multi-stage → `scratch` / distroless | **~8–15 MB** | ประโยชน์นอกจากขนาด: ใน image สุดท้าย **ไม่มี compiler, ไม่มี source code, ไม่มี dev tools** — ปลอดภัยขึ้นอย่างมีนัยสำคัญ ตัวเลือก base สำหรับ stage สุดท้าย: - **`alpine`** — เล็ก มี shell/apk ให้ debug ได้ (ระวังเรื่อง musl libc กับบาง binary) - **`gcr.io/distroless/static`** — ไม่มี shell ไม่มี package manager เหมาะ production ที่เน้น security - **`scratch`** — ว่างเปล่า 100% ใช้ได้กับ static binary เท่านั้น (ต้องก๊อป ca-certificates เองถ้าเรียก HTTPS) --- ## 10. การจัดการข้อมูล: Volumes และ Bind Mounts หลักที่ต้องจำ: **container เป็นของชั่วคราว (ephemeral)** — ข้อมูลใน writable layer หายทันทีที่ลบ container ถ้าข้อมูลต้องอยู่รอด ต้องเก็บนอก container | แบบ | คืออะไร | เหมาะกับ | |---|---|---| | **Volume** | พื้นที่ที่ Docker จัดการเอง (อยู่ใต้ `/var/lib/docker/volumes/`) | ข้อมูล production: database, ไฟล์อัปโหลด | | **Bind mount** | ผูก directory จริงบน host เข้า container | Development (mount โค้ดเข้าไปแก้สดๆ), ไฟล์ config | | **tmpfs** | เก็บใน RAM หายเมื่อ container หยุด | ข้อมูลชั่วคราว/ความลับที่ไม่อยากลงดิสก์ | ### 10.1 Volumes ```bash docker volume create pgdata docker volume ls docker volume inspect pgdata # Attach the volume to a container (-v name:path-in-container) docker run -d --name db \ -e POSTGRES_PASSWORD=secret \ -v pgdata:/var/lib/postgresql/data \ postgres:17-alpine # Delete the container -> data survives in the volume docker rm -f db docker run -d --name db2 -e POSTGRES_PASSWORD=secret \ -v pgdata:/var/lib/postgresql/data postgres:17-alpine # data is back docker volume rm pgdata # deleting the volume is what destroys data docker volume prune # remove unused volumes (careful!) ``` ### 10.2 Bind Mounts ```bash # Mount current directory into the container (great for development) docker run -d -p 8080:80 \ -v "$(pwd)/site:/usr/share/nginx/html:ro" \ nginx:1.27-alpine ``` `:ro` = read-only — แนะนำสำหรับ config/ไฟล์ที่ container ไม่ควรแก้ > **ข้อควรระวัง bind mount:** ขึ้นกับ path บนเครื่องนั้นๆ (พังเมื่อย้ายเครื่อง), เรื่อง permission/UID ระหว่าง host กับ container, และประสิทธิภาพบน Docker Desktop (macOS/Windows) ช้ากว่า volume --- ## 11. Networking ### 11.1 Network Drivers หลัก | Driver | พฤติกรรม | |---|---| | `bridge` (default) | Container ต่อกับ virtual switch บน host — โลกภายนอกเข้าถึงได้ผ่าน `-p` เท่านั้น | | `host` | ใช้ network ของ host ตรงๆ ไม่มี isolation, ไม่ต้อง `-p` (Linux เท่านั้น) | | `none` | ไม่มี network เลย | | `overlay` | เชื่อม container ข้ามหลายเครื่อง (Swarm/cluster) | ### 11.2 User-defined Bridge — สิ่งที่ควรใช้จริง ความต่างสำคัญ: **default bridge ไม่มี DNS ระหว่าง container** แต่ **network ที่เราสร้างเองมี built-in DNS** — container เรียกหากันด้วย "ชื่อ" ได้เลย ```bash docker network create appnet docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:17-alpine docker run -d --name api --network appnet -p 8080:8080 myapp:1.0 # Inside "api", the hostname "db" resolves to the db container's IP docker exec -it api sh / # nc -zv db 5432 # connect to Postgres by container name ``` จุดที่พลาดบ่อยที่สุดของมือใหม่: ตั้งค่าแอปให้ต่อ database ที่ `localhost:5432` — **ผิด** เพราะ `localhost` ใน container คือตัว container เอง ต้องใช้ **ชื่อ service/container** (`db:5432`) ```bash docker network ls docker network inspect appnet docker network connect appnet existing-container docker network disconnect appnet existing-container ``` ### 11.3 Port Publishing ```bash -p 8080:80 # host 8080 -> container 80 (ทุก interface) -p 127.0.0.1:8080:80 # bind เฉพาะ localhost ของ host (ปลอดภัยขึ้นสำหรับ internal service) -p 8080:80/udp # ระบุโปรโตคอล -P # publish ทุก port ที่ EXPOSE ไว้ ไปยัง random port ``` > **ข้อควรระวังด้าน security:** บน Linux การ `-p 3306:3306` เปิดสู่ทุก interface และ Docker เขียน iptables ให้เอง ซึ่ง **อาจลัดผ่านกฎ firewall อย่าง ufw** — database/บริการภายในควร bind เป็น `127.0.0.1:port:port` หรือไม่ publish เลยแล้วให้คุยกันใน network ภายใน --- ## 12. Docker Compose พอระบบมีหลาย container (api + db + cache) การพิมพ์ `docker run` ยาวๆ ทีละตัวไม่ไหว — **Docker Compose** ให้เราประกาศทั้งระบบในไฟล์ YAML เดียว แล้วคุมด้วยคำสั่งเดียว > Compose รุ่นปัจจุบันเป็น plugin ของ Docker CLI — ใช้ `docker compose` (มีเว้นวรรค) ไม่ใช่ `docker-compose` แบบเก่า และชื่อไฟล์มาตรฐานคือ `compose.yaml` (ยังอ่าน `docker-compose.yml` ได้) โดยไม่ต้องใส่ key `version:` แล้ว ### 12.1 ตัวอย่างระบบจริง: API + PostgreSQL + Adminer ```yaml # compose.yaml services: api: build: . # build from Dockerfile in this directory image: myapp:1.0 ports: - "8080:8080" environment: DB_HOST: db # talk to Postgres by service name DB_USER: app DB_PASSWORD: ${DB_PASSWORD} # read from .env file or shell env DB_NAME: appdb depends_on: db: condition: service_healthy # wait until db is actually ready restart: unless-stopped db: image: postgres:17-alpine environment: POSTGRES_USER: app POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: appdb volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U app -d appdb"] interval: 5s timeout: 3s retries: 10 restart: unless-stopped adminer: # web UI for inspecting the database image: adminer:4 ports: - "8081:8080" depends_on: - db volumes: db_data: # named volume managed by Docker ``` ไฟล์ `.env` วางข้างๆ `compose.yaml`: ``` DB_PASSWORD=changeme-in-real-life ``` ### 12.2 คำสั่ง Compose ที่ใช้ประจำ ```bash docker compose up -d # create & start everything in background docker compose ps # status of services in this project docker compose logs -f api # follow logs of one service docker compose exec api sh # shell into a running service docker compose build # rebuild images docker compose up -d --build # rebuild + restart in one go docker compose restart api docker compose down # stop & remove containers + network (volumes survive) docker compose down -v # ...AND delete volumes (data is gone!) docker compose config # render the final merged/validated config ``` ### 12.3 แนวคิดสำคัญใน Compose - **Service name = DNS name** — Compose สร้าง network ให้โปรเจกต์อัตโนมัติ ทุก service เรียกหากันด้วยชื่อ - **`depends_on` เปล่าๆ คุมแค่ "ลำดับ start"** ไม่รอให้พร้อมใช้งาน — ต้องผูกกับ `condition: service_healthy` + `healthcheck` ถึงจะรอจริง - **`down` ไม่ลบ volume** (ข้อมูลอยู่) แต่ **`down -v` ลบ** — จำให้ขึ้นใจก่อนใช้กับข้อมูลจริง - แยก config ตาม environment ได้ด้วยหลายไฟล์: `docker compose -f compose.yaml -f compose.prod.yaml up -d` --- ## 13. Registry และการจัดการ Tag ### 13.1 Push ขึ้น Registry ```bash docker login # Docker Hub docker login registry.mycompany.co.th # private registry # Tag = pointer: same image can have many tags docker tag myapp:1.0 registry.mycompany.co.th/team/myapp:1.0 docker push registry.mycompany.co.th/team/myapp:1.0 docker pull registry.mycompany.co.th/team/myapp:1.0 ``` ### 13.2 กลยุทธ์การตั้ง Tag ``` myapp:1.4.2 # semantic version — immutable, ใช้ deploy production myapp:1.4 # ตามสายเวอร์ชัน myapp:git-3fa9c21 # ผูกกับ commit — traceability สูงสุด (เหมาะกับ CI/CD) myapp:latest # ใช้เพื่อความสะดวกใน dev เท่านั้น ``` หลักการ: **production ต้อง deploy ด้วย tag ที่ระบุตัว image ได้เป๊ะ** (เวอร์ชันเต็มหรือ digest) — `latest` ทำให้ "deploy เดิมซ้ำ" อาจได้คนละ image และ rollback ลำบาก ### 13.3 Private Registry ตัวเลือกยอดนิยมในองค์กร: **Harbor** (มี UI, RBAC, vulnerability scan), **GitLab/GitHub Container Registry**, cloud registry (ECR/ACR/GAR) หรือรันแบบ minimal: ```bash docker run -d -p 5000:5000 --name registry -v regdata:/var/lib/registry registry:2 ``` --- ## 14. Security หลักคิด: image ที่ดีคือ image ที่ **เล็ก, ไม่รันเป็น root, ไม่มี secret ฝังอยู่, และรู้ว่าข้างในมีอะไรบ้าง** ### 14.1 Checklist ความปลอดภัยของ Image 1. **รันด้วย non-root user** — ใส่ `USER` เสมอ (หัวข้อ 7) 2. **Base image เล็กและ pin เวอร์ชัน** — alpine/slim/distroless ลด CVE ที่ติดมา 3. **Multi-stage** — ไม่พก compiler/dev tools ไป production 4. **ห้ามฝัง secret ใน image** — ไม่ใส่ใน `ENV`, `ARG`, ไม่ `COPY .env` (ตรวจย้อนหลังด้วย `docker history <image>`) 5. **สแกนช่องโหว่ก่อนใช้/ก่อน push:** ```bash # Trivy (open source, นิยมใน CI) trivy image myapp:1.0 # Docker Scout (มากับ Docker) docker scout quickview myapp:1.0 docker scout cves myapp:1.0 ``` ### 14.2 ทำให้ Runtime แข็งแรงขึ้น ```bash docker run -d \ --read-only \ # root filesystem อ่านอย่างเดียว --tmpfs /tmp \ # เปิดพื้นที่เขียนเฉพาะจุดที่จำเป็น --cap-drop ALL \ # ถอด Linux capabilities ทั้งหมด --cap-add NET_BIND_SERVICE \ # เพิ่มคืนเฉพาะที่ต้องใช้ --security-opt no-new-privileges \ # กัน privilege escalation --memory 512m --cpus 1.5 \ # จำกัดทรัพยากร myapp:1.0 ``` ### 14.3 ส่ง Secret อย่างถูกวิธี - **Runtime:** ใช้ environment variable ที่ inject ตอน run (จากไฟล์ `.env` ที่ไม่เข้า Git / secret manager เช่น Vault) หรือ mount secret file เข้า container - **Build time:** ใช้ BuildKit secret mount — ค่าไม่ติดเข้า layer: ```dockerfile # syntax=docker/dockerfile:1 RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci ``` ```bash docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp . ``` ### 14.4 ข้อห้ามที่เจอบ่อย - อย่า `docker run --privileged` โดยไม่จำเป็นจริงๆ — เท่ากับยก host ให้ container - อย่า mount `/var/run/docker.sock` เข้า container ทั่วไป — ใครคุม socket = คุม host - อย่าเปิด Docker daemon TCP (`2375`) โดยไม่มี TLS — เท่ากับเปิด root shell สู่สาธารณะ --- ## 15. การใช้งานใน Production ### 15.1 Restart Policy ```bash --restart no # ค่าปริยาย --restart on-failure[:N] # restart เมื่อ exit code != 0 (จำกัดจำนวนครั้งได้) --restart unless-stopped # restart เสมอ ยกเว้นเราสั่ง stop เอง (แนะนำสำหรับ service) --restart always # เหมือนบน แต่ boot เครื่องแล้วขึ้นด้วยแม้เคยถูก stop ``` ### 15.2 จำกัดทรัพยากร ```bash docker run -d --memory 512m --memory-swap 512m --cpus 1.5 myapp:1.0 docker stats # ตรวจการใช้จริง ``` ถ้าไม่จำกัด — container เดียวที่ memory leak สามารถลากทั้งเครื่องล่มได้ เมื่อ container ใช้ RAM เกิน limit จะถูก kernel ฆ่า (**OOMKilled**, exit code **137**) ### 15.3 Logging Docker default ใช้ `json-file` driver และ **ไม่จำกัดขนาด** — production ต้องตั้ง rotation เสมอ ไม่งั้นดิสก์เต็มแน่นอน: ```json // /etc/docker/daemon.json { "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } } ``` หลักการฝั่งแอป: **log ออก stdout/stderr** อย่าเขียนลงไฟล์ใน container — ให้ Docker/ระบบรวบรวม log (ELK, Loki, Fluentd) เป็นคนจัดการ ### 15.4 หลักออกแบบแอปให้เข้ากับ Container - **1 container = 1 หน้าที่หลัก** — แยก web / worker / db ไม่ยัดทุกอย่างใส่กล่องเดียว - **Stateless ให้มากที่สุด** — state ไปอยู่ใน database/volume/object storage - **Config ผ่าน environment variables** — image เดียว รันได้ทุก environment - **จัดการ SIGTERM** — ปิด connection ให้เรียบร้อยภายใน grace period (default 10s) ก่อนโดน SIGKILL - **มี `/health` endpoint** — ต่อกับ HEALTHCHECK/orchestrator --- ## 16. Troubleshooting ### 16.1 ลำดับการไล่ปัญหา ```bash docker ps -a # 1) container สถานะอะไร? exit code เท่าไร? docker logs --tail 200 <name> # 2) แอปพูดว่าอะไรก่อนตาย? docker inspect <name> # 3) config ถูกไหม? (env, mounts, network, OOMKilled?) docker exec -it <name> sh # 4) เข้าไปดูข้างใน (ถ้ายังรันอยู่) docker events # 5) ดูเหตุการณ์ระดับ daemon แบบ real-time docker stats # 6) ทรัพยากรตันหรือเปล่า? ``` ### 16.2 Exit Codes ที่ควรจำ | Code | ความหมาย | ทางไปต่อ | |---|---|---| | `0` | จบปกติ | ปกติสำหรับ job/one-shot; ถ้าเป็น service = แอปจบเองโดยไม่ตั้งใจ | | `1` | Error ทั่วไปในแอป | ดู `docker logs` | | `125` | Docker รันคำสั่งไม่ได้ | option ของ `docker run` ผิด | | `126` | Execute ไม่ได้ | ไฟล์ไม่มีสิทธิ์ execute / ไม่ใช่ executable | | `127` | หา command ไม่เจอ | path ผิด / binary ไม่มีใน image (เช่นใช้ bash บน alpine) | | `137` | SIGKILL | มักคือ **OOMKilled** — เช็ค `docker inspect` ฟิลด์ `OOMKilled` | | `139` | Segmentation fault | ปัญหาระดับ binary/library | | `143` | SIGTERM | ถูกสั่ง stop ตามปกติ | ### 16.3 อาการยอดฮิตและวิธีแก้ | อาการ | สาเหตุที่พบบ่อย | วิธีแก้ | |---|---|---| | `port is already allocated` | port บน host ถูกใช้แล้ว | เปลี่ยน port ฝั่ง host หรือหาตัวจอง: `ss -tlnp \| grep 8080` | | Container ขึ้นแล้วดับทันที | process หลักจบ (เช่น รัน daemon แบบ background แล้ว PID 1 จบ) | รันแอปแบบ foreground; ดู logs | | ต่อ DB ไม่ได้ | ใช้ `localhost` แทนชื่อ service / อยู่คนละ network | ใช้ชื่อ service + network เดียวกัน | | `permission denied` ตอนเขียน volume | UID ใน container ไม่ตรง owner ของโฟลเดอร์ | `COPY --chown` / ปรับ owner / กำหนด `user:` | | ข้อมูลหายหลัง restart | เขียนลง writable layer ไม่ได้ใช้ volume | ผูก volume ให้ path ข้อมูล | | Build ช้าทุกครั้ง | ลำดับ COPY ทำ cache แตก / context ใหญ่ | จัดลำดับ Dockerfile + `.dockerignore` | | `Cannot connect to the Docker daemon` | daemon ไม่รัน / user ไม่อยู่ group docker | `systemctl status docker`; เพิ่ม user เข้า group `docker` | | ดิสก์เต็ม | image/log สะสม | `docker system df` → `docker system prune`; ตั้ง log rotation | --- ## 17. สรุป Best Practices **Dockerfile** 1. Pin เวอร์ชัน base image — ห้าม `latest` ใน production 2. เลือก base เล็กที่สุดที่พอ (alpine/slim/distroless) 3. เรียง instruction ให้ cache ทำงาน: dependencies ก่อน source code 4. ใช้ multi-stage แยก build ออกจาก runtime 5. `USER` non-root เสมอ, ใช้ exec form สำหรับ `CMD`/`ENTRYPOINT` 6. มี `.dockerignore` และห้าม secret เข้า image 7. ใส่ `HEALTHCHECK` ให้ service **Runtime / Operations** 8. Container = ephemeral — ข้อมูลถาวรอยู่ใน volume เท่านั้น 9. ใช้ user-defined network, เรียกกันด้วยชื่อ service 10. จำกัด memory/CPU และตั้ง restart policy 11. ตั้ง log rotation ตั้งแต่วันแรก 12. สแกน image (trivy/scout) ใน CI ก่อน push 13. Deploy ด้วย tag เวอร์ชันชัดเจน เพื่อ rollback ได้เสมอ 14. ระบบหลาย service → ใช้ Compose ประกาศไว้ในไฟล์ ไม่พิมพ์มือ --- ## 18. ก้าวต่อไป Docker + Compose เพียงพอสำหรับเครื่องเดียว แต่เมื่อระบบต้องการ **หลายเครื่อง, auto-scaling, self-healing, rolling update** — นั่นคือหน้าที่ของ **Container Orchestrator**: - **Kubernetes (K8s)** — มาตรฐานอุตสาหกรรม: Pod, Deployment, Service, Ingress ล้วนต่อยอดจากแนวคิด container/image/registry ที่เรียนมาทั้งหมด - **Docker Swarm** — ง่ายกว่า ใช้ syntax ใกล้ Compose เหมาะระบบเล็ก - แนวเรียนต่อ: Docker → Compose → Kubernetes พื้นฐาน → Helm / GitOps → Observability แหล่งอ้างอิงหลัก: [docs.docker.com](https://docs.docker.com) · [Dockerfile reference](https://docs.docker.com/reference/dockerfile/) · [Compose reference](https://docs.docker.com/compose/) · [Play with Docker](https://labs.play-with-docker.com) (สนามซ้อมฟรีในเบราว์เซอร์)
09/07/2026 17:33
# Docker Labs และเคสตัวอย่างประกอบการสอน > คู่มือปฏิบัติ 7 แล็บ (เรียงจากง่ายไปยาก) + 5 เคสสถานการณ์จริงสำหรับชวนผู้เรียนวิเคราะห์ — แต่ละแล็บมีเป้าหมาย ขั้นตอน จุดสังเกต และคำถามท้ายแล็บพร้อมแนวเฉลย **เตรียมเครื่องผู้เรียน:** Docker Engine หรือ Docker Desktop ที่รัน `docker run hello-world` ผ่าน · ถ้าเครื่องไม่พร้อม ใช้ [labs.play-with-docker.com](https://labs.play-with-docker.com) ได้ (ฟรี มีอายุ session 4 ชม.) --- ## Lab 0: ตรวจความพร้อม (10 นาที) ```bash docker version # client + server version docker info # daemon configuration summary docker run hello-world # end-to-end test: pull -> create -> run ``` **ชวนสังเกต:** ตอนรัน `hello-world` ครั้งแรกจะเห็นบรรทัด `Unable to find image ... locally` ตามด้วย `Pulling from library/hello-world` — นี่คือขั้นตอน pull อัตโนมัติ ครั้งที่สองรันซ้ำจะไม่ pull แล้ว --- ## Lab 1: รัน Container แรก — nginx (25 นาที) **เป้าหมาย:** เข้าใจ lifecycle ของ container, port mapping, logs, exec ```bash # 1) Run nginx in the background, map host 8080 -> container 80 docker run -d --name web -p 8080:80 nginx:1.27-alpine # 2) Open http://localhost:8080 in a browser -> nginx welcome page # 3) Inspect what's running docker ps docker logs web docker logs -f web # refresh the browser and watch access logs live (Ctrl+C to exit) # 4) Go inside the container docker exec -it web sh ls /usr/share/nginx/html # Change the homepage from inside the container echo '<h1>Hello from container!</h1>' > /usr/share/nginx/html/index.html exit # Refresh browser -> new page # 5) Prove that containers are ephemeral docker rm -f web docker run -d --name web -p 8080:80 nginx:1.27-alpine # Refresh browser -> back to the default page. Our edit is GONE. # 6) Clean up docker rm -f web ``` **คำถามท้ายแล็บ** 1. ทำไมหน้าเว็บที่แก้ไว้ถึงหายไปในข้อ 5? → *แก้ใน writable layer ของ container ตัวเก่า ซึ่งถูกลบไปพร้อม container — image ต้นทางไม่เคยถูกแก้* 2. `-p 8080:80` เลขไหนคือฝั่ง host เลขไหนคือฝั่ง container? → *ซ้าย host : ขวา container* 3. ลองรัน container ที่สองด้วย `-p 8080:80` เหมือนกัน — เกิดอะไรขึ้น เพราะอะไร? → *Error `port is already allocated` เพราะ port ฝั่ง host ซ้ำกันไม่ได้ (เปลี่ยนเป็น 8081:80 ได้)* --- ## Lab 2: Build Image แรกด้วย Dockerfile (40 นาที) **เป้าหมาย:** เขียน Dockerfile, เข้าใจ build context และเห็น layer caching ด้วยตาตัวเอง สร้างโฟลเดอร์ `lab2/` แล้วสร้าง 3 ไฟล์นี้: ```javascript // server.js — tiny Express app for the lab const express = require("express"); const os = require("os"); const app = express(); const PORT = process.env.PORT || 3000; app.get("/", (req, res) => { res.json({ message: "Hello from Docker!", hostname: os.hostname(), // inside a container this is the container ID version: process.env.APP_VERSION || "dev", }); }); app.listen(PORT, () => console.log(`Listening on :${PORT}`)); ``` ```json { "name": "lab2-app", "version": "1.0.0", "main": "server.js", "dependencies": { "express": "^4.19.0" } } ``` ```dockerfile # Dockerfile FROM node:22-alpine WORKDIR /app # Copy dependency manifests first -> this layer is cached COPY package*.json ./ RUN npm install --omit=dev # Copy application source (changes often -> keep it late) COPY . . EXPOSE 3000 USER node CMD ["node", "server.js"] ``` และไฟล์ `.dockerignore`: ``` node_modules *.log .git ``` **ขั้นตอน:** ```bash cd lab2 docker build -t lab2-app:1.0 . docker run -d --name lab2 -p 3000:3000 -e APP_VERSION=1.0 lab2-app:1.0 curl http://localhost:3000 ``` **การทดลอง layer cache (หัวใจของแล็บนี้):** ```bash # Round 1: build again without changing anything docker build -t lab2-app:1.0 . # -> every step shows CACHED, finishes in about a second # Round 2: edit only the message text in server.js, then build docker build -t lab2-app:1.1 . # -> COPY package*.json and npm install are still CACHED # only "COPY . ." and later layers rebuild # Round 3: add any dependency in package.json, then build docker build -t lab2-app:1.2 . # -> cache breaks at COPY package*.json; npm install runs again ``` **คำถามท้ายแล็บ** 1. ทำไม `COPY package*.json ./` ต้องมาก่อน `COPY . .`? → *เพื่อให้ layer `npm install` ใช้ cache ได้ตราบใดที่ dependencies ไม่เปลี่ยน — แก้โค้ดแล้ว build เร็วขึ้นมาก* 2. `EXPOSE 3000` เปิด port ให้เข้าถึงจากภายนอกหรือไม่? → *ไม่ — เป็นเอกสาร ต้อง `-p` ตอน run* 3. ค่า `hostname` ใน response คืออะไร? → *Container ID สั้น — พิสูจน์ว่าแอปรันใน environment ของตัวเอง* --- ## Lab 3: Multi-stage Build ด้วย Go (35 นาที) **เป้าหมาย:** เห็นขนาด image ต่างกันเป็นสิบเท่า และเข้าใจว่าทำไม production image ไม่ควรมีเครื่องมือ build สร้างโฟลเดอร์ `lab3/` และไฟล์: ```go // main.go — minimal HTTP server in Go package main import ( "fmt" "net/http" "os" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { host, _ := os.Hostname() fmt.Fprintf(w, "Hello from Go in Docker! host=%s\n", host) }) fmt.Println("Listening on :8080") http.ListenAndServe(":8080", nil) } ``` ```go // go.mod module lab3 go 1.24 ``` **Dockerfile แบบที่ 1 — single-stage (แบบที่ไม่ควรทำ):** ```dockerfile # Dockerfile.single FROM golang:1.24 WORKDIR /src COPY . . RUN go build -o /app . EXPOSE 8080 CMD ["/app"] ``` **Dockerfile แบบที่ 2 — multi-stage:** ```dockerfile # Dockerfile # ---------- Build stage ---------- FROM golang:1.24-alpine AS builder WORKDIR /src COPY go.mod ./ COPY . . # Static binary: runs on any minimal base image RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app . # ---------- Runtime stage ---------- FROM alpine:3.21 RUN addgroup -S app && adduser -S app -G app COPY --from=builder /out/app /usr/local/bin/app USER app EXPOSE 8080 ENTRYPOINT ["app"] ``` **ขั้นตอน:** ```bash cd lab3 docker build -f Dockerfile.single -t lab3:fat . docker build -t lab3:slim . docker images | grep lab3 # lab3 fat ... ~900MB+ # lab3 slim ... ~15MB <- ต่างกันประมาณ 50-60 เท่า docker run -d --name go-slim -p 8082:8080 lab3:slim curl http://localhost:8082 ``` **ชวนสังเกตเพิ่ม:** ```bash # The slim image has no Go toolchain and no source code inside docker run --rm -it lab3:slim sh / $ which go # -> not found / $ ls /src # -> not found ``` **คำถามท้ายแล็บ** 1. ทำไม image `slim` ถึงปลอดภัยกว่านอกจากเรื่องขนาด? → *ไม่มี compiler/source/tooling ให้ผู้โจมตีใช้ต่อยอด — attack surface เล็กลงมาก* 2. `CGO_ENABLED=0` ใส่ไปทำไม? → *ให้ได้ static binary ที่ไม่พึ่ง libc ของระบบ จึงรันบน alpine/scratch ได้* 3. ถ้าเปลี่ยน stage สุดท้ายเป็น `FROM scratch` ต้องระวังอะไร? → *ไม่มี shell ให้ exec, ไม่มี CA certificates (ต้องก๊อปมาเองถ้าเรียก HTTPS), debug ยากขึ้น* --- ## Lab 4: Volume และความอยู่รอดของข้อมูล (30 นาที) **เป้าหมาย:** พิสูจน์ว่า volume ทำให้ข้อมูลรอดข้ามชีวิตของ container ```bash # 1) Postgres WITHOUT a volume docker run -d --name db1 -e POSTGRES_PASSWORD=secret postgres:17-alpine sleep 5 docker exec -it db1 psql -U postgres -c "CREATE TABLE t(msg TEXT); INSERT INTO t VALUES ('important data');" docker exec -it db1 psql -U postgres -c "SELECT * FROM t;" # Destroy and recreate docker rm -f db1 docker run -d --name db1 -e POSTGRES_PASSWORD=secret postgres:17-alpine sleep 5 docker exec -it db1 psql -U postgres -c "SELECT * FROM t;" # -> ERROR: relation "t" does not exist (data is GONE) # 2) Postgres WITH a named volume docker volume create pgdata docker rm -f db1 docker run -d --name db2 -e POSTGRES_PASSWORD=secret \ -v pgdata:/var/lib/postgresql/data postgres:17-alpine sleep 5 docker exec -it db2 psql -U postgres -c "CREATE TABLE t(msg TEXT); INSERT INTO t VALUES ('survives!');" # Destroy and recreate with the SAME volume docker rm -f db2 docker run -d --name db3 -e POSTGRES_PASSWORD=secret \ -v pgdata:/var/lib/postgresql/data postgres:17-alpine sleep 5 docker exec -it db3 psql -U postgres -c "SELECT * FROM t;" # -> survives! (data persisted in the volume) # 3) Inspect and clean up docker volume inspect pgdata docker rm -f db3 docker volume rm pgdata ``` **คำถามท้ายแล็บ** 1. ตอนไหนกันแน่ที่ข้อมูล "หายจริง"? → *เมื่อลบ volume (`docker volume rm`) — การลบ container ไม่กระทบข้อมูลใน volume* 2. path `/var/lib/postgresql/data` รู้มาจากไหน? → *เอกสารของ image บน Docker Hub — ทุก image ระบุ path ข้อมูลของตัวเอง* 3. ถ้าใช้ bind mount `-v $(pwd)/pgdata:/var/lib/postgresql/data` ต่างจาก named volume อย่างไร? → *ข้อมูลไปอยู่ในโฟลเดอร์ที่เรากำหนดบน host เห็น/แก้ตรงได้ แต่ผูกกับ path เครื่องนั้นและมีประเด็น permission* --- ## Lab 5: Networking — คุยกันด้วยชื่อ (25 นาที) **เป้าหมาย:** เข้าใจว่า user-defined network ให้ DNS และ `localhost` ใน container คือตัวมันเอง ```bash # 1) Create a network and two containers on it docker network create labnet docker run -d --name red --network labnet nginx:1.27-alpine docker run -d --name blue --network labnet alpine:3.21 sleep 3600 # 2) From "blue", call "red" BY NAME docker exec -it blue sh / # wget -qO- http://red # nginx welcome page -> DNS by container name works / # ping -c 2 red / # exit # 3) Show that the DEFAULT bridge cannot do this docker run -d --name green nginx:1.27-alpine # no --network -> default bridge docker exec -it blue wget -qO- --timeout=3 http://green # -> fails: name "green" does not resolve on the default bridge # 4) Show the classic localhost mistake docker exec -it blue wget -qO- --timeout=3 http://localhost # -> fails: localhost inside "blue" is "blue" itself, and blue runs no web server # Clean up docker rm -f red blue green docker network rm labnet ``` **คำถามท้ายแล็บ** 1. อะไรคือความต่างสำคัญระหว่าง default bridge กับ network ที่สร้างเอง? → *Network ที่สร้างเองมี embedded DNS — เรียกกันด้วยชื่อ container/service ได้* 2. แอปใน container จะต่อ database ที่รันอีก container ควรตั้ง host เป็นอะไร? → *ชื่อ container/service (เช่น `db`) ไม่ใช่ `localhost`* --- ## Lab 6: Docker Compose — ยกทั้งระบบด้วยคำสั่งเดียว (45 นาที) **เป้าหมาย:** ประกอบ API + Database + Web UI, ใช้ healthcheck/depends_on, เห็นผลของ `down` vs `down -v` สร้างโฟลเดอร์ `lab6/` — ใช้แอปจาก Lab 2 (`server.js`, `package.json`, `Dockerfile`, `.dockerignore`) แล้วเพิ่ม 2 ไฟล์: ```yaml # compose.yaml services: api: build: . ports: - "3000:3000" environment: APP_VERSION: compose-1.0 DB_HOST: db # service name = DNS name DB_PASSWORD: ${DB_PASSWORD} depends_on: db: condition: service_healthy # wait for Postgres to be truly ready restart: unless-stopped db: image: postgres:17-alpine environment: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 3s retries: 10 adminer: image: adminer:4 ports: - "8081:8080" depends_on: - db volumes: db_data: ``` ``` # .env DB_PASSWORD=lab6-secret ``` **ขั้นตอน:** ```bash cd lab6 docker compose up -d docker compose ps # observe: db becomes (healthy), then api starts docker compose logs -f api # Ctrl+C to exit curl http://localhost:3000 # Open http://localhost:8081 -> Adminer: System=PostgreSQL, Server=db, User=postgres # Create data in the db (via Adminer or psql), then: docker compose down # containers & network removed, volume SURVIVES docker compose up -d # data is still there docker compose down -v # now the volume is deleted too docker compose up -d # fresh empty database docker compose down -v ``` **คำถามท้ายแล็บ** 1. ใน Adminer ทำไมช่อง Server ใส่ `db` ไม่ใช่ `localhost`? → *Adminer รันใน container ของตัวเอง — ต้องเรียก Postgres ผ่านชื่อ service บน network ของ Compose* 2. ถ้าเอา `healthcheck` ออกแต่คง `condition: service_healthy` ไว้ จะเกิดอะไร? → *Compose ฟ้อง error เพราะไม่มีข้อมูลสุขภาพให้รอ — สองอย่างนี้ต้องมาคู่กัน* 3. ทีมบอกว่า "สั่ง down แล้วข้อมูลลูกค้าหาย" — น่าจะเกิดจากคำสั่งไหน? → *`docker compose down -v` (หรือไม่ได้ประกาศ volume ตั้งแต่แรก)* --- ## Lab 7: ห้องซ่อม — วินิจฉัย Container ที่พัง (40 นาที) **เป้าหมาย:** ฝึกไล่ปัญหาอย่างเป็นระบบด้วย `ps -a` → `logs` → `inspect` → `exec` ให้ผู้เรียนรันทีละเคสแล้ววินิจฉัยเองก่อนดูเฉลย ```bash # Case A docker run -d --name caseA -p 80:80 nginx:1.27-alpine docker run -d --name caseA2 -p 80:80 nginx:1.27-alpine # Case B docker run -d --name caseB alpine:3.21 # Case C docker run -d --name caseC alpine:3.21 bash -c "echo hi" # Case D docker run -d --name caseD --memory 10m --memory-swap 10m postgres:17-alpine \ || true; sleep 8; docker ps -a | grep caseD # Case E docker run -d --name caseE -e POSTGRES_PASSWORD= postgres:17-alpine ``` **แนวเฉลย** | เคส | อาการ | การวินิจฉัย | สาเหตุ/บทเรียน | |---|---|---|---| | A | container ที่สองรันไม่ขึ้น | error ตอน run: `port is already allocated` | host port ซ้ำ — เปลี่ยน port ฝั่ง host | | B | `Exited (0)` ทันที | `docker ps -a` เห็น exit 0; ไม่มี error ใน logs | image `alpine` คำสั่งปริยายคือ `sh` ซึ่งจบทันทีเมื่อไม่มี TTY — container อยู่ได้เท่าที่ PID 1 อยู่ | | C | `Exited (127)` | `docker logs caseC` → `bash: not found` | alpine ไม่มี bash (มี `sh`) — exit 127 = command not found | | D | `Exited (137)` | `docker inspect caseD --format '{{.State.OOMKilled}}'` → `true` | RAM limit 10m ต่ำเกิน — 137 = SIGKILL/OOMKilled | | E | restart วน / Exited (1) | `docker logs caseE` → ข้อความบอกว่าต้องกำหนด password/ENV ให้ถูก | environment variable จำเป็นไม่ครบ — อ่าน error ของ image เป็นทางลัดที่สุด | ```bash # Clean up docker rm -f caseA caseA2 caseB caseC caseD caseE 2>/dev/null ``` --- --- # เคสตัวอย่างสำหรับชวนวิเคราะห์ (Case Discussions) > ใช้ท้ายคาบหรือแทรกระหว่างหัวข้อ — โยนโจทย์ ให้คิดเป็นกลุ่ม 5–10 นาที แล้วเฉลยร่วมกัน ## Case 1: "มันรันได้บนเครื่องผม" **สถานการณ์:** ทีมพัฒนาแอปด้วย Node 22 บนเครื่อง dev แต่ server test ติดตั้ง Node 18 ไว้ — deploy แล้วพังเพราะ syntax ใหม่ใช้ไม่ได้ ทีม ops ไม่กล้าอัปเกรด Node บน server เพราะมีแอปเก่าอีก 3 ตัวใช้อยู่ **คำถามชวนคิด:** Docker ช่วยแก้สถานการณ์นี้ตรงไหนบ้าง? แอปเก่า 3 ตัวต้องแก้อะไรไหม? **แนวเฉลย:** แต่ละแอป pin base image ของตัวเอง (`node:22-alpine`, `node:18-alpine`) รันคู่กันบนเครื่องเดียวโดยไม่ตีกัน — server ไม่ต้องติดตั้ง Node เลย มีแค่ Docker · แอปเก่ายังไม่ต้องแตะทันที ค่อยๆ containerize ทีละตัวได้ · จุดสำคัญ: environment กลายเป็นสิ่งที่ระบุไว้ใน Dockerfile (reviewable, version-controlled) แทนที่จะเป็นสภาพของเครื่อง ## Case 2: Image อ้วน 1.2 GB **สถานการณ์:** ทีมมี image แอป Python ขนาด 1.2 GB — pull ช้า, deploy ช้า, ค่า storage บาน ตรวจ Dockerfile พบ: `FROM python:3.12` (ตัวเต็ม), `COPY . .` ก่อน `pip install`, ไม่มี `.dockerignore` (มี `.git` 300 MB กับโฟลเดอร์ `data/` เข้าไปด้วย), ติดตั้ง `gcc` ไว้ compile library แล้วไม่ได้เอาออก **คำถามชวนคิด:** ระบุปัญหาให้ครบและเรียงลำดับสิ่งที่จะแก้ก่อน-หลัง **แนวเฉลย:** (1) เพิ่ม `.dockerignore` ตัด `.git`, `data/`, cache — ได้ผลทันทีและง่ายสุด (2) เปลี่ยน base เป็น `python:3.12-slim` (3) จัดลำดับ `COPY requirements.txt` → `pip install --no-cache-dir` → `COPY . .` เพื่อ cache (4) multi-stage: stage แรกมี gcc ไว้ build wheel, stage สุดท้าย copy เฉพาะ site-packages/venv — คาดหวังผลรวม: จาก 1.2 GB เหลือหลักร้อย MB ต้นๆ หรือต่ำกว่า ## Case 3: Secret หลุดเข้า Image **สถานการณ์:** Dev ใช้ `COPY . .` โดยไม่มี `.dockerignore` ไฟล์ `.env` (มี DB password และ API key ของจริง) เข้าไปอยู่ใน image และถูก push ขึ้น registry ที่คนทั้งบริษัทเข้าถึงได้ — ภายหลัง dev แก้ด้วยการเพิ่ม `RUN rm /app/.env` ใน Dockerfile แล้ว push ทับ **คำถามชวนคิด:** วิธี "แก้" ของ dev คนนี้ปิดรูรั่วได้จริงไหม? ขั้นตอนตอบสนองที่ถูกต้องคืออะไร? **แนวเฉลย:** ไม่ได้ — `rm` สร้าง layer ใหม่ทับ แต่ไฟล์ยังอยู่ใน layer เดิม ดึงออกมาดูได้ (`docker history`, save แล้วแตก layer) · ขั้นตอนที่ถูก: **(1) rotate secret ทุกตัวทันที** — ถือว่ารั่วแล้ว (2) ลบ tag/image ออกจาก registry (3) แก้ต้นเหตุ: `.env` เข้า `.dockerignore`, inject secret ตอน runtime หรือใช้ BuildKit secret mount (4) เพิ่ม secret scanning ใน CI · บทเรียนหลัก: **layer คือประวัติศาสตร์ที่ลบไม่ได้ — กันไว้ก่อนเข้าเท่านั้น** ## Case 4: ข้อมูลหายหลังอัปเดตระบบ **สถานการณ์:** ระบบภายในตัวหนึ่งรัน MariaDB ด้วย `docker run` เปล่าๆ ไม่มี `-v` ใช้งานมา 3 เดือน — วันหนึ่ง admin อัปเดต image ด้วยการ `docker rm -f` แล้ว run ใหม่ ข้อมูลทั้งหมดหายเกลี้ยง ไม่มี backup **คำถามชวนคิด:** ข้อมูลไปไหน? กู้ได้ไหม? ออกแบบใหม่อย่างไรไม่ให้เกิดซ้ำ? **แนวเฉลย:** ข้อมูลอยู่ใน writable layer ของ container ตัวเก่า — ถูกลบไปพร้อม `docker rm` โอกาสกู้แทบเป็นศูนย์ · ออกแบบใหม่: named volume สำหรับ data directory เสมอ, มี backup job (`docker exec ... mysqldump` เข้าที่เก็บภายนอก + ทดสอบ restore), เขียน runbook การอัปเดตที่ถูกต้อง (stop → backup → run ใหม่ด้วย volume เดิม) · ประเด็นเสริมที่ดี: การ "อัปเดต image" ที่ถูกคือสร้าง container ใหม่จาก image ใหม่ + volume เดิม — ไม่ใช่การพยายามอัปเดตในตัว container ## Case 5: เปิด Port Database สู่โลก **สถานการณ์:** ทีมรัน MariaDB ด้วย `-p 3306:3306` บน VPS ที่มี public IP โดยคิดว่า "ตั้ง ufw ปิดไว้แล้ว" — ต่อมาได้รับแจ้งเตือนจากผู้ให้บริการว่า port 3306 เปิดสู่อินเทอร์เน็ต **คำถามชวนคิด:** ทำไม ufw ถึงไม่ช่วย? ทางเลือกในการออกแบบมีอะไรบ้าง? **แนวเฉลย:** Docker จัดการ iptables เอง (chain DOCKER) ซึ่งรับ traffic ก่อนกฎของ ufw จะทำงาน — ผลคือ `-p` เปิดจริงแม้ ufw บอกว่าปิด · ทางเลือกเรียงตามความเหมาะสม: (1) ไม่ publish เลย — ให้แอปคุยกับ DB ผ่าน user-defined network ภายใน (2) ถ้าจำเป็นต้องเข้าจากเครื่อง host เอง ใช้ `-p 127.0.0.1:3306:3306` (3) ถ้าต้องเข้าจากภายนอกจริง ใช้ VPN/SSH tunnel + จำกัด source IP + แข็งแรงเรื่อง auth/TLS · บทเรียน: **การ publish port ของ Docker คือการเปิดประตูจริง — ต้องออกแบบอย่างตั้งใจ ไม่ใช่ค่า default** --- ## ตารางเทียบแล็บกับหัวข้อในสไลด์ | แล็บ | ใช้หลังหัวข้อ | เวลา | แนวคิดหลัก | |---|---|---|---| | Lab 0–1 | คำสั่งพื้นฐาน | 35 นาที | lifecycle, port, logs, exec, ephemeral | | Lab 2 | Dockerfile | 40 นาที | build, layer caching, .dockerignore | | Lab 3 | Multi-stage | 35 นาที | ขนาด image, security ของ runtime image | | Lab 4 | Volumes | 30 นาที | data persistence | | Lab 5 | Networking | 25 นาที | user-defined network, DNS, localhost trap | | Lab 6 | Compose | 45 นาที | multi-service, healthcheck, down vs down -v | | Lab 7 | Troubleshooting | 40 นาที | exit codes, กระบวนการวินิจฉัย |

CONFIRM_DELETE

Are you sure you want to delete this article?
"ชุดสื่อการสอน Docker (Docker Teaching Kit)"
This action cannot be undone.