/static/codemoomoo2.png

11. Containerization (Docker, Docker Compose)

Containerization คือเทคโนโลยีที่ปฏิวัติวิธีการพัฒนา ส่งมอบ และรันซอฟต์แวร์ โดยห่อหุ้มแอปพลิเคชันและ dependency ทั้งหมดไว้ในหน่วยที่แยกขาดและพกพาได้ บทนี้จะกล่าวถึงแนวคิดของ Container, สถาปัตยกรรมและการใช้งาน Docker, รวมถึง Docker Compose สำหรับจัดการแอปพลิเคชันแบบหลายบริการ พร้อมตัวอย่างที่นำไปใช้งานจริงได้ทันที


11.1 แนวคิดของ Container

Container คือหน่วยซอฟต์แวร์ที่ห่อหุ้ม (encapsulate) โค้ด, runtime, system tools, libraries และ settings ทั้งหมดที่จำเป็นต่อการรันแอปพลิเคชันให้ได้ผลลัพธ์เหมือนกันในทุกสภาพแวดล้อม โดยอาศัย kernel ของ host แทนการจำลองทั้งระบบเหมือน Virtual Machine

flowchart TB
    subgraph VM["Virtual Machine (เครื่องเสมือน)"]
        direction TB
        VApp1["App A"] --- VApp2["App B"]
        VApp1 --> VOS1["Guest OS 1
(เคอร์เนลเต็ม)"] VApp2 --> VOS2["Guest OS 2
(เคอร์เนลเต็ม)"] VOS1 --> Hyp["Hypervisor"] VOS2 --> Hyp Hyp --> VHW["Host OS / Hardware"] end subgraph CT["Container"] direction TB CApp1["App A"] --- CApp2["App B"] CApp1 --> CLib1["Libs/Bin"] CApp2 --> CLib2["Libs/Bin"] CLib1 --> CRT["Container Runtime
(Docker / containerd)"] CLib2 --> CRT CRT --> CHW["Host OS Kernel / Hardware
(แบ่งใช้ kernel ร่วมกัน)"] end

11.1.1 Container vs VM (overhead, boot time, isolation, portability)

ข้อแตกต่างพื้นฐานระหว่าง Container และ Virtual Machine (VM) อยู่ที่ระดับของการจำลอง โดย VM จำลองทั้งฮาร์ดแวร์เสมือนและรัน Guest OS แบบสมบูรณ์ ขณะที่ Container แบ่งใช้ kernel ของ host แต่แยก userspace ออกจากกัน

ประเด็น Virtual Machine (VM) Container
ขนาด (size) หลาย GB (รวม OS) สิบ MB ถึงไม่กี่ร้อย MB
เวลา boot (boot time) หลายสิบวินาที – นาที มิลลิวินาที – ไม่กี่วินาที
Overhead สูง (รัน kernel เต็ม) ต่ำมาก (แบ่งใช้ kernel)
การแยก (isolation) แข็งแรงระดับ hardware ระดับ kernel namespace
ความปลอดภัย สูงกว่า (attack surface เล็ก) ขึ้นกับ kernel ของ host
Portability ขึ้นกับ hypervisor สูงมาก (image มาตรฐาน OCI)
Density ต่อ host สิบ ๆ ตัว หลายร้อยถึงหลายพันตัว
OS ของ guest ใดก็ได้ที่ hypervisor รองรับ ต้องใช้ kernel เดียวกับ host

ในเชิงคณิตศาสตร์ เราสามารถเปรียบเทียบประสิทธิภาพการใช้ทรัพยากรได้ดังนี้:

Dcontainer = Rhost Rapp + Oruntime Dvm = Rhost Rapp + Ros + Ohyp

โดยกำหนดให้:

จากสมการนี้จะเห็นได้ว่า เมื่อ Ros ถูกตัดออกในกรณี container ทำให้ความหนาแน่นในการรันแอปพลิเคชันสูงกว่า VM อย่างมีนัยสำคัญ

11.1.2 ประวัติ: chroot → FreeBSD Jail → Solaris Zone → LXC → Docker → OCI

เทคโนโลยี Container ไม่ได้เกิดขึ้นพร้อมกับ Docker แต่มีวิวัฒนาการมายาวนานหลายทศวรรษ

flowchart LR
    subgraph Era1["ยุคบุกเบิก (1979-2004)"]
        A["1979
chroot
(Unix V7)"] --> B["2000
FreeBSD Jail"] B --> C["2004
Solaris Zones
(Containers)"] end subgraph Era2["ยุค Linux Container (2006-2013)"] D["2006
cgroups
(Google)"] --> E["2008
LXC
(Linux Containers)"] E --> F["2013
Docker 0.1
(dotCloud)"] end subgraph Era3["ยุคมาตรฐาน (2015-ปัจจุบัน)"] G["2015
OCI
(Open Container
Initiative)"] --> H["2017
containerd
+ runc"] H --> I["2018+
Podman, Buildah,
Kubernetes"] end Era1 --> Era2 Era2 --> Era3

จุดเปลี่ยนสำคัญคือ chroot ในปี 1979 ที่เริ่มแยก filesystem ของ process ออกจากระบบหลัก จากนั้น FreeBSD Jail (2000) และ Solaris Zones (2004) ขยายแนวคิดให้แยก process, network และ user ได้ Linux ตามมาด้วย cgroups จาก Google (2006) เพื่อจำกัดทรัพยากร และ LXC (2008) ที่รวม namespaces + cgroups เข้าด้วยกันเป็น container ที่สมบูรณ์ Docker (2013) ทำให้ container ใช้งานได้ง่ายผ่าน image และ CLI ที่เป็นมิตร และในปี 2015 OCI ก็ได้กำหนดมาตรฐานกลางเพื่อให้ ecosystem ทำงานร่วมกันได้

11.1.3 Linux Kernel Primitives: Namespace (PID, NET, MNT, UTS, IPC, USER, CGROUP, TIME)

Namespace คือกลไกของ Linux kernel ที่แยกมุมมองของ process จากกัน ทำให้ process ภายใน namespace หนึ่งมองไม่เห็นทรัพยากรใน namespace อื่น

Namespace สิ่งที่แยก ตัวอย่างผล
PID Process ID Container เห็น PID 1 เป็นของตัวเอง
NET Network stack มี interface, route, port ของตัวเอง
MNT Mount point filesystem ไม่ปะปนกัน
UTS Hostname, domain ตั้งชื่อเครื่องอิสระ
IPC System V IPC, message queue shared memory แยกกัน
USER UID/GID mapping root ใน container ≠ root ของ host
CGROUP cgroup hierarchy ดูสถิติทรัพยากรเฉพาะของตน
TIME System clock (Linux 5.6+) ตั้งเวลาใน container ต่างจาก host ได้

ตัวอย่างการใช้คำสั่ง unshare เพื่อสร้าง namespace ด้วยตัวเอง (โดยไม่ใช้ Docker):

# สร้าง shell ใหม่ที่มี PID, MNT, UTS namespace แยกจาก host
# -p = PID namespace, -m = MNT namespace, -u = UTS namespace
# -f = fork ก่อน exec, -r = map root user
sudo unshare -pmuf -r /bin/bash

# ตั้งชื่อเครื่องใน namespace ใหม่
hostname mycontainer

# ตรวจสอบ – จะเห็นเฉพาะ process ใน namespace นี้
ps -ef
# UID PID ...
#   0   1 /bin/bash
#   0   2 ps -ef

11.1.4 Control Groups (cgroups v1 vs v2): CPU, memory, I/O limit

cgroups (Control Groups) ใช้จำกัดและตรวจวัดการใช้ทรัพยากรของกลุ่ม process ในขณะที่ namespace แยก "การมองเห็น" cgroups แยก "การใช้ทรัพยากร" — ทั้งสองอย่างทำงานร่วมกันจึงเกิดเป็น container ที่สมบูรณ์

ความแตกต่างระหว่าง cgroups v1 และ v2:

คุณสมบัติ cgroups v1 cgroups v2
ลำดับชั้น (hierarchy) หลายลำดับชั้นแยกตาม controller ลำดับชั้นเดียวรวมศูนย์
ตำแหน่ง mount /sys/fs/cgroup/<controller>/ /sys/fs/cgroup/
Rootless support จำกัด รองรับเต็ม
PSI (Pressure Stall Info) ไม่มี มี
การใช้งานกับ Docker รองรับ (legacy) รองรับ (default ใน Docker 20.10+)

ตัวอย่างการจำกัด memory ด้วย cgroups v2 โดยตรง:

# สร้าง cgroup ใหม่ในชื่อ "demo"
sudo mkdir /sys/fs/cgroup/demo

# จำกัด memory ที่ 100 MB
echo "100M" | sudo tee /sys/fs/cgroup/demo/memory.max

# ย้าย process ปัจจุบันเข้า cgroup
echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs

# ทดสอบ – พยายามใช้ memory เกินจะถูก OOM kill
stress --vm 1 --vm-bytes 200M --timeout 10

11.1.5 Union File System: OverlayFS, AUFS, Btrfs

Union File System (UnionFS) เป็นเทคนิคที่รวมหลาย directory (เรียกว่า layers) เข้าเป็น filesystem เดียว แบบ logically merged โดยที่ layer ด้านบนซ้อนทับ layer ด้านล่าง การเปลี่ยนแปลงไฟล์จะถูกเขียนลง layer บนสุดผ่านกลไก Copy-on-Write (CoW)

flowchart TB
    Top["Container Layer
(เขียนได้, RW)
เก็บการเปลี่ยนแปลง"] L3["Image Layer 3
(read-only)
เพิ่มไฟล์ของแอป"] L2["Image Layer 2
(read-only)
ติดตั้ง pip install"] L1["Image Layer 1
(read-only)
apt install python"] Base["Base Layer
(read-only)
ubuntu:22.04"] Top --> L3 --> L2 --> L1 --> Base

OverlayFS เป็นมาตรฐานปัจจุบันบน Linux modern (kernel 4.0+) มีโครงสร้างหลัก 4 directory:

ตัวอย่างการสร้าง OverlayFS ด้วยมือ:

# สร้าง directory ทดลอง
mkdir -p /tmp/overlay/{lower,upper,work,merged}
echo "ข้อมูลจาก lower" > /tmp/overlay/lower/file.txt

# mount แบบ overlay
sudo mount -t overlay overlay \
  -o lowerdir=/tmp/overlay/lower,\
upperdir=/tmp/overlay/upper,\
workdir=/tmp/overlay/work \
  /tmp/overlay/merged

# ทดสอบ – เขียนทับไฟล์
echo "ข้อมูลใหม่" > /tmp/overlay/merged/file.txt

# ตรวจสอบ
cat /tmp/overlay/lower/file.txt   # ไม่เปลี่ยน
cat /tmp/overlay/upper/file.txt   # มีไฟล์ใหม่ (CoW)
cat /tmp/overlay/merged/file.txt  # เห็นค่าใหม่

11.1.6 Capabilities, Seccomp, AppArmor, SELinux

นอกจาก namespace และ cgroups แล้ว Linux ยังมีกลไกความปลอดภัยเพิ่มเติมที่ Container ใช้เพื่อจำกัดสิทธิ์:

ตัวอย่างการรัน container แบบจำกัด capability:

# Drop ทุก capability แล้วเพิ่มเฉพาะที่จำเป็น
docker run --rm \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges \
  nginx:alpine

11.1.7 OCI Specification (Runtime, Image, Distribution)

Open Container Initiative (OCI) เป็นองค์กรกลางที่กำหนดมาตรฐานเพื่อให้ ecosystem ของ container ทำงานข้ามเครื่องมือกันได้ มี 3 specification หลัก:

  1. Runtime Specification — กำหนดว่า bundle (rootfs + config.json) ควรถูก execute อย่างไร เครื่องมือที่ใช้ตามมาตรฐานนี้คือ runc, crun, youki
  2. Image Specification — กำหนดโครงสร้าง image (manifest, layer, config) เพื่อให้ pull/push ระหว่าง registry กันได้
  3. Distribution Specification — กำหนด HTTP API ของ registry เพื่อให้ Docker Hub, Harbor, GHCR ใช้ร่วมกันได้

จากมาตรฐานนี้ทำให้ทุกวันนี้เราสามารถสร้าง image ด้วย buildah แล้วรันด้วย podman หรือ push ไป Harbor และ pull ด้วย Docker ได้โดยไม่มีปัญหา


11.2 สถาปัตยกรรม Docker

Docker ไม่ใช่โปรแกรมเดียว แต่เป็นระบบของหลาย component ที่ทำงานร่วมกัน

flowchart TB
    User["ผู้ใช้ (User)"] --> CLI["docker CLI
(client)"] CLI -->|REST API
over UNIX socket| Daemon["dockerd
(Docker Engine Daemon)"] Daemon --> Containerd["containerd
(container supervisor)"] Containerd --> Shim["containerd-shim
(per-container)"] Shim --> Runc["runc
(OCI runtime)"] Runc --> Kernel["Linux Kernel
(namespace + cgroups)"] Daemon -.->|pull/push| Registry["Docker Registry
(Hub, GHCR, Harbor)"] Daemon -.-> Storage["Storage Driver
(overlay2, btrfs)"] Daemon -.-> Network["Network Driver
(bridge, overlay)"]

11.2.1 Docker Engine (dockerd)

dockerd เป็น long-running daemon ที่รับคำสั่งจาก client ผ่าน REST API (โดย default คือ UNIX socket /var/run/docker.sock) มีหน้าที่:

11.2.2 Docker Client (docker CLI)

docker CLI คือคำสั่งที่ผู้ใช้พิมพ์ใน terminal ตัว client เป็นเพียง wrapper ที่แปลงคำสั่งเป็น HTTP request ส่งไป dockerd จุดสำคัญคือ client และ daemon ไม่จำเป็นต้องอยู่เครื่องเดียวกัน — สามารถตั้งค่า DOCKER_HOST ให้ชี้ไปเครื่องอื่นได้

# คุย Docker ที่เครื่อง remote ผ่าน SSH
export DOCKER_HOST="ssh://user@remote-server"
docker ps

# คุยผ่าน TCP (ต้องเปิด tls)
export DOCKER_HOST="tcp://192.168.1.100:2376"

11.2.3 containerd และ runc (low-level runtime)

ใต้ Docker Engine ยังมีอีกหลายชั้น:

11.2.4 Docker Registry

Registry คือบริการเก็บ image แบบ HTTP (ตามมาตรฐาน OCI Distribution) เมื่อรัน docker pull nginx Docker จะติดต่อ registry (default: Docker Hub) เพื่อดาวน์โหลด manifest และ layer ทั้งหมด แต่ละ layer สามารถถูก reuse โดย image อื่นเพื่อประหยัดพื้นที่

11.2.5 Docker Desktop Architecture (บน macOS/Windows มี Linux VM)

เนื่องจาก container ต้องการ Linux kernel ดังนั้นบน macOS และ Windows Docker Desktop จะ:

  1. รัน Linux VM ขนาดเล็ก (ใช้ HyperKit/Virtualization.framework บน macOS, WSL2/Hyper-V บน Windows)
  2. รัน dockerd ภายใน VM นั้น
  3. Docker CLI บนเครื่อง host ติดต่อ dockerd ผ่าน proxy

ผลคือ filesystem ของ container อยู่ใน VM ทำให้ bind mount จาก host ช้ากว่าบน Linux native — สำคัญที่ต้องเข้าใจเมื่อพัฒนาบน macOS/Windows


11.3 การติดตั้งและตั้งค่า Docker

11.3.1 การติดตั้งบน Ubuntu/Debian, Fedora/RHEL, Arch

บน Ubuntu/Debian (ใช้ official repository):

# 1. ติดตั้ง dependency
sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release

# 2. เพิ่ม Docker GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 3. เพิ่ม repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list

# 4. ติดตั้ง Docker Engine + Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# 5. ตรวจสอบ
sudo docker run hello-world

บน Fedora/RHEL:

sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker

บน Arch Linux:

# Docker อยู่ใน official repo
sudo pacman -S docker docker-compose docker-buildx
sudo systemctl enable --now docker.service

11.3.2 Docker Desktop (Windows, macOS, Linux)

Docker Desktop เป็นแอปพลิเคชันแบบ GUI ที่รวม Docker Engine + Compose + Kubernetes + Dashboard เหมาะกับการพัฒนาบนเครื่องส่วนตัว

11.3.3 Rootless Docker

Rootless Docker ให้ผู้ใช้ทั่วไป (ไม่ต้องเป็น root) สามารถรัน Docker daemon ในชื่อของตัวเองได้ เพิ่มความปลอดภัยอย่างมาก เพราะหากมี exploit ใน container ผู้โจมตีก็จะได้แค่สิทธิ์ของ user คนนั้น

# ติดตั้ง dependency
sudo apt install -y uidmap dbus-user-session

# รัน installer
curl -fsSL https://get.docker.com/rootless | sh

# เพิ่ม environment
echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc

# Enable systemd user service
systemctl --user enable --now docker

11.3.4 การเพิ่ม User เข้า docker group (security consideration)

วิธีให้ user ทั่วไปรัน docker ได้โดยไม่ใช้ sudo ทุกครั้งคือเพิ่มเข้า group docker:

sudo usermod -aG docker $USER
# logout และ login ใหม่ หรือใช้
newgrp docker

คำเตือน: การอยู่ใน group docker มีค่าเทียบเท่า root เพราะ user สามารถ mount / ของ host เข้า container แล้วทำอะไรก็ได้ ในระบบ production ที่ multi-user แนะนำให้ใช้ Rootless Docker หรือ Podman แทน

11.3.5 Configuration: /etc/docker/daemon.json

ไฟล์ตั้งค่าหลักของ daemon คือ /etc/docker/daemon.json (สร้างเองหากยังไม่มี) ตัวอย่างการกำหนด:

{
  "data-root": "/var/lib/docker",
  "storage-driver": "overlay2",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "registry-mirrors": ["https://mirror.gcr.io"],
  "insecure-registries": ["192.168.1.100:5000"],
  "default-address-pools": [
    {"base": "172.30.0.0/16", "size": 24}
  ],
  "live-restore": true
}

หลังแก้ไขต้องรัน sudo systemctl restart docker


11.4 Image และ Container

11.4.1 docker pull, run, stop, start, restart, rm, rmi

วงจรชีวิตพื้นฐานของการใช้ image และ container:

# ดึง image จาก registry
docker pull nginx:alpine

# สร้างและรัน container ใหม่
docker run -d --name web -p 8080:80 nginx:alpine
#   -d = detached (background)
#   --name = ตั้งชื่อ container
#   -p host:container = port mapping

# หยุด container (ส่ง SIGTERM, รอ 10 วินาที, แล้ว SIGKILL)
docker stop web

# เริ่มใหม่ (ใช้ container เดิม)
docker start web

# restart = stop + start
docker restart web

# ลบ container (ต้อง stop ก่อน หรือใช้ -f)
docker rm -f web

# ลบ image
docker rmi nginx:alpine

11.4.2 docker ps, images, logs, exec, inspect, stats, top

คำสั่งสำหรับดูข้อมูลและจัดการ:

# ดู container ที่กำลังรัน
docker ps
docker ps -a            # รวมที่หยุดแล้ว
docker ps --filter "status=exited"

# ดู image ทั้งหมด
docker images

# ดู log (เหมือน tail)
docker logs web
docker logs -f --tail 100 web   # follow + 100 บรรทัดล่าสุด

# เข้าไปใน container เพื่อ debug
docker exec -it web /bin/sh
#   -i = interactive, -t = TTY

# ดูข้อมูลทั้งหมดของ container/image (JSON)
docker inspect web

# ดูการใช้ทรัพยากรแบบ real-time
docker stats

# ดู process ภายใน container
docker top web

11.4.3 Container Lifecycle (Created, Running, Paused, Stopped, Exited)

stateDiagram-v2
    [*] --> Created: docker create
    Created --> Running: docker start
    Running --> Paused: docker pause


Paused --> Running: docker unpause
Running --> Stopped: docker stop Stopped --> Running: docker start Running --> Exited: process exits Exited --> Running: docker start Stopped --> [*]: docker rm Exited --> [*]: docker rm

11.4.4 Detached Mode (-d) และ Interactive Mode (-it)

# Service mode
docker run -d --name api -p 3000:3000 myapp:latest

# Sandbox mode
docker run -it --rm ubuntu:22.04 /bin/bash
#   --rm = ลบ container อัตโนมัติเมื่อออก

11.4.5 Port Mapping (-p host:container)

-p แมป port ของ host ไปยัง port ใน container:

docker run -d -p 8080:80 nginx           # host 8080 → container 80
docker run -d -p 127.0.0.1:8080:80 nginx # bind เฉพาะ localhost
docker run -d -p 8080:80/tcp -p 8080:80/udp myapp
docker run -d -P nginx                   # auto map ทุก EXPOSE port

11.4.6 Environment Variable (-e, --env-file)

ส่ง environment variable เข้า container:

# ทีละตัว
docker run -d \
  -e DB_HOST=db.example.com \
  -e DB_USER=admin \
  -e DB_PASS=secret123 \
  myapp

# จากไฟล์
cat > .env <<EOF
DB_HOST=db.example.com
DB_USER=admin
DB_PASS=secret123
EOF
docker run -d --env-file .env myapp

11.4.7 Resource Limit (--memory, --cpus)

จำกัดทรัพยากรเพื่อป้องกัน container เดี่ยวกินทรัพยากรหมด:

docker run -d \
  --memory="512m" \           # RAM สูงสุด 512 MB
  --memory-swap="1g" \         # รวม swap สูงสุด 1 GB
  --cpus="1.5" \               # CPU 1.5 cores
  --cpu-shares=512 \           # น้ำหนัก (default 1024)
  --pids-limit=100 \           # process สูงสุด 100
  myapp

11.4.8 Restart Policy (no, always, on-failure, unless-stopped)

Policy พฤติกรรม ใช้เมื่อ
no (default) ไม่ restart งาน batch/one-shot
on-failure[:max] restart ถ้า exit code ≠ 0 service ที่ crash บ้าง
always restart ทุกกรณี (รวมหลัง reboot) service สำคัญ
unless-stopped restart เหมือน always แต่ถ้า user สั่ง stop จะไม่กลับมาเอง service ทั่วไป
docker run -d --restart=unless-stopped --name web nginx

11.5 Dockerfile

Dockerfile คือไฟล์ข้อความที่บอกขั้นตอนการสร้าง image ทุก instruction จะกลายเป็น layer หนึ่ง

11.5.1 Instruction: FROM, RUN, COPY, ADD, WORKDIR, CMD, ENTRYPOINT, EXPOSE, ENV, ARG, VOLUME, USER, LABEL, HEALTHCHECK, STOPSIGNAL, SHELL

Instruction ความหมาย ตัวอย่าง
FROM image ต้นทาง FROM python:3.12-alpine
RUN สั่งรันคำสั่งตอน build RUN apt update && apt install -y curl
COPY คัดลอกไฟล์จาก context เข้า image COPY app.py /app/
ADD เหมือน COPY แต่รองรับ URL/tar ADD https://x.com/f.tar.gz /tmp/
WORKDIR ตั้ง working directory WORKDIR /app
CMD คำสั่ง default ตอน run CMD ["python", "app.py"]
ENTRYPOINT คำสั่งหลัก (override ยากกว่า CMD) ENTRYPOINT ["python"]
EXPOSE บันทึกว่า image ใช้ port อะไร (เป็น metadata) EXPOSE 8080
ENV ตั้ง environment variable ถาวร ENV NODE_ENV=production
ARG ตัวแปรเฉพาะตอน build ARG VERSION=1.0
VOLUME ประกาศจุดที่ควรเป็น volume VOLUME /data
USER สลับ user ที่จะรัน USER 1000:1000
LABEL metadata key=value LABEL maintainer="moo@rmutsv.ac.th"
HEALTHCHECK คำสั่งตรวจสุขภาพ HEALTHCHECK CMD curl -f http://localhost/
STOPSIGNAL signal ที่ใช้ stop STOPSIGNAL SIGTERM
SHELL shell ที่ใช้ใน RUN form แบบ shell SHELL ["/bin/bash", "-c"]

11.5.2 CMD vs ENTRYPOINT (exec form vs shell form)

หนึ่งในจุดสับสนที่สุดของ Dockerfile คือความแตกต่างระหว่าง CMD และ ENTRYPOINT

# ENTRYPOINT = "command หลัก" — argument จาก docker run จะถูก append
ENTRYPOINT ["python", "app.py"]

# CMD = "default argument" — ถูก override ได้ง่ายด้วย docker run <image> <args>
CMD ["--port", "8080"]

ถ้ารัน docker run myimage --port 9000 จะได้ผลคือ python app.py --port 9000

Exec form (แนะนำ)["cmd", "arg1", "arg2"] — เรียก binary โดยตรง ไม่ผ่าน shell ทำให้ signal (เช่น SIGTERM) ส่งถึง process ตรง ๆ

Shell formcmd arg1 arg2 — รันผ่าน /bin/sh -c ทำให้ process ที่เห็น PID 1 คือ shell ไม่ใช่แอปจริง อาจมีปัญหาเรื่องการ stop graceful

11.5.3 COPY vs ADD

แนวทาง: ใช้ COPY เป็น default ใช้ ADD เฉพาะกรณีต้องการแตก tar (URL ควรใช้ RUN curl แทนเพราะควบคุมได้ดีกว่า)

11.5.4 Layer และ Build Cache

ทุก instruction สร้าง layer ใหม่ Docker จะ cache layer ไว้ — ถ้า instruction และ context ไม่เปลี่ยน จะ reuse cache เพื่อให้ build เร็ว

กฎ: เรียง instruction ที่เปลี่ยนน้อย ขึ้นก่อน, ที่เปลี่ยนบ่อย อยู่ท้าย

# ❌ ไม่ดี – แก้ source 1 บรรทัดก็ต้อง pip install ใหม่
FROM python:3.12-alpine
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt

# ✅ ดี – cache pip install จะ reuse ได้
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

11.5.5 .dockerignore

เหมือน .gitignore ใช้ระบุไฟล์ที่ไม่ควรส่งเข้า build context — ลดขนาดและป้องกันข้อมูลลับรั่ว:

.git
.gitignore
node_modules
__pycache__
*.pyc
*.log
.env
.env.*
!.env.example
.vscode
.idea
*.md
Dockerfile*
docker-compose*.yml

11.5.6 Multi-stage Build

เทคนิคที่ใช้หลาย FROM เพื่อแยก stage ของ build ออกจาก stage ของ runtime ทำให้ image สุดท้ายเล็กลงมาก

# ===== Stage 1: Builder =====
FROM golang:1.22-alpine AS builder
WORKDIR /src
# ติดตั้ง dependency ก่อน เพื่อ cache
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# build แบบ static binary ไม่พึ่ง libc
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app ./cmd/server

# ===== Stage 2: Runtime =====
FROM alpine:3.19
RUN apk add --no-cache ca-certificates && \
    addgroup -S app && adduser -S app -G app
COPY --from=builder /out/app /usr/local/bin/app
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/app"]

ผล: image runtime อาจมีขนาดเพียง ~15 MB แทนที่จะเป็น ~800 MB ของ golang image

11.5.7 BuildKit และ docker buildx

BuildKit เป็น engine การ build ใหม่ที่เร็วขึ้น มี feature เช่น parallel build, cache mount, secret mount, multi-platform docker buildx คือ CLI ที่ใช้ BuildKit

# ตัวอย่าง: ใช้ cache mount เพื่อให้ pip cache คงอยู่ระหว่าง build
# syntax=docker/dockerfile:1.7
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY . .
# build ปกติ (BuildKit เป็น default ใน Docker 23.0+)
docker buildx build -t myapp:1.0 .

# ดู cache ที่มี
docker buildx du

11.5.8 Cross-platform Build (multi-arch)

สร้าง image ที่ทำงานได้ทั้ง amd64 และ arm64 (เช่น Apple Silicon, Raspberry Pi):

# สร้าง builder ใหม่ที่รองรับหลาย platform
docker buildx create --name multi --use --bootstrap

# build และ push พร้อมกัน 2 architecture
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t username/myapp:1.0 \
  --push .

11.5.9 Best Practice: ขนาดเล็ก (distroless, alpine, scratch), security, reproducibility, non-root

ตัวอย่าง Dockerfile ที่ใช้ best practice ครบถ้วนสำหรับ Python web app:

# syntax=docker/dockerfile:1.7

# ===== Stage 1: builder =====
FROM python:3.12-slim-bookworm AS builder
WORKDIR /app

# ติดตั้ง build dependency
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential gcc \
 && rm -rf /var/lib/apt/lists/*

# ใช้ virtualenv เพื่อแยก site-packages
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-cache-dir -r requirements.txt

# ===== Stage 2: runtime =====
FROM python:3.12-slim-bookworm AS runtime

# สร้าง user ที่ไม่ใช่ root
RUN groupadd -r app && useradd -r -g app -u 1000 app

WORKDIR /app

# ลอก venv จาก builder (ไม่ต้องลง compiler ใน runtime)
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# ลอก source ในชื่อ user app
COPY --chown=app:app . .

USER app
EXPOSE 8000

# Healthcheck เพื่อให้ orchestrator รู้สถานะ
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

# Exec form เพื่อให้ signal forward ถูกต้อง
ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app:app"]

หลักสำคัญ:

  1. เล็ก — ใช้ alpine, slim, distroless, หรือ scratch
  2. ปลอดภัย — non-root user, drop capabilities, ตรึง version ของ base image
  3. ทำซ้ำได้ (reproducible) — ใช้ digest แทน tag (FROM python@sha256:...), pin version ของ dependency
  4. Cache friendly — เรียง layer ตามความถี่ในการเปลี่ยน
  5. Healthcheck — ให้ container แจ้งสถานะตัวเองได้

11.6 Docker Registry

11.6.1 Docker Hub (public, private repository)

Docker Hub (hub.docker.com) เป็น registry default ของ Docker มีทั้ง:

11.6.2 Container Registry อื่น: GHCR, GitLab Registry, AWS ECR, Google GCR, Azure ACR

Registry URL Format จุดเด่น
GitHub Container Registry (GHCR) ghcr.io/USER/IMAGE ฟรี, ผูกกับ GitHub Actions
GitLab Registry registry.gitlab.com/GROUP/PROJECT ฝัง GitLab CE/EE
AWS ECR ACCOUNT.dkr.ecr.REGION.amazonaws.com ผูก IAM, scale
Google GCR / Artifact Registry gcr.io/PROJECT GCP integration
Azure ACR REGISTRY.azurecr.io AKS integration

11.6.3 Self-hosted: Harbor, Nexus, Distribution (registry:2)

สำหรับองค์กรที่ต้องการ host registry เอง:

ตั้งค่า registry แบบ minimum ใน 1 บรรทัด:

docker run -d -p 5000:5000 --restart=always --name registry registry:2
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0

11.6.4 docker login, push, pull, tag

# Login (จะถาม username/password)
docker login                                # Docker Hub
docker login ghcr.io                         # GHCR
docker login registry.gitlab.com             # GitLab

# ใช้ token แทน password (ปลอดภัยกว่า)
echo "$GITHUB_TOKEN" | docker login ghcr.io -u myuser --password-stdin

# tag image ให้ตรง path ของ registry
docker tag myapp:1.0 ghcr.io/moo/myapp:1.0

# push
docker push ghcr.io/moo/myapp:1.0

# pull จากที่อื่น
docker pull ghcr.io/moo/myapp:1.0

11.6.5 Image Tag, Digest และ Immutable Tag

Tag เช่น 1.0, latest เป็นแค่ "ชื่อเล่น" ที่ชี้ไปยัง digest จริง — ผู้ดูแล image สามารถเปลี่ยนให้ tag เดิมชี้ไป digest ใหม่ได้

Digest เช่น sha256:abc123... คือ hash ของ manifest — ไม่เปลี่ยน เป็น immutable identity ที่แท้จริง

# pull แบบระบุ tag (อาจเปลี่ยนได้ในอนาคต)
docker pull nginx:1.25

# pull แบบระบุ digest (ทำซ้ำได้ 100%)
docker pull nginx@sha256:9d6b58feebd2dbd3c56ab5853333d627cc6e281011cfd6050fa4bcf2072c9496

# ดู digest ของ image ที่มี
docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25

ใน production ควร pin ด้วย digest เพื่อ reproducibility และความปลอดภัย

11.6.6 Vulnerability Scan ใน Registry

Registry สมัยใหม่มักมี scanner ในตัว เช่น Harbor + Trivy หรือใช้เครื่องมือภายนอก:

# scan image local ด้วย Trivy
trivy image nginx:1.25

# scan แบบ severity สูงเท่านั้น
trivy image --severity HIGH,CRITICAL nginx:1.25

# scan แล้ว fail ถ้าพบ critical
trivy image --exit-code 1 --severity CRITICAL myapp:1.0

11.7 Docker Storage

11.7.1 Volume vs Bind Mount vs tmpfs Mount

flowchart LR
    subgraph Host["Host Filesystem"]
        A["/var/lib/docker/
volumes/myvol/_data
(Docker จัดการเอง)"] B["/home/moo/project
(path ที่ user เลือก)"] C["RAM
(ไม่อยู่ใน disk)"] end subgraph Container["Container"] V["Volume Mount
/data"] BM["Bind Mount
/app"] T["tmpfs Mount
/tmp"] end A === V B === BM C === T
ประเภท จัดการโดย ใช้เมื่อ
Volume Docker (ใน /var/lib/docker/volumes/) ข้อมูล persistent ของ DB, cache, อะไรก็ตามที่อยู่ใน production
Bind Mount User (ใส่ path ของ host เอง) development (mount source code), config file
tmpfs Kernel (อยู่ใน RAM) secret ชั่วคราว, ไฟล์ที่ไม่ต้องการให้คงอยู่

11.7.2 การสร้างและจัดการ Volume: docker volume create/ls/inspect/rm

# สร้าง volume แบบมีชื่อ
docker volume create pgdata

# ดูทั้งหมด
docker volume ls

# ดูรายละเอียด
docker volume inspect pgdata

# mount เข้า container
docker run -d --name db \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16

# mount แบบ bind (ใช้ syntax ใหม่ --mount ชัดเจนกว่า -v)
docker run -d --name app \
  --mount type=bind,source="$(pwd)",target=/app \
  --mount type=tmpfs,target=/tmp,tmpfs-size=64m \
  myapp

# ลบ volume (ต้องไม่มี container ใช้อยู่)
docker volume rm pgdata

# ลบ volume ที่ไม่ถูกใช้แล้วทั้งหมด
docker volume prune

11.7.3 Volume Driver (local, NFS, cloud)

Docker รองรับ volume driver หลายแบบ — local เป็น default แต่สามารถใช้ NFS, cloud ได้

# Volume แบบ NFS
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw \
  --opt device=:/exports/data \
  nfs-data

11.7.4 การสำรองและกู้คืน Volume

Docker ไม่มีคำสั่ง volume backup ในตัว แต่ใช้ container ชั่วคราวสำรองได้:

# Backup volume "pgdata" เป็น tar
docker run --rm \
  -v pgdata:/source:ro \
  -v "$(pwd)":/backup \
  alpine \
  tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /source .

# Restore กลับเข้า volume
docker volume create pgdata-restored
docker run --rm \
  -v pgdata-restored:/target \
  -v "$(pwd)":/backup \
  alpine \
  tar xzf /backup/pgdata-20260101.tar.gz -C /target

11.7.5 Storage Driver: overlay2, btrfs, zfs

Storage driver คือกลไกที่ Docker ใช้รวม layer ของ image ปัจจุบัน overlay2 เป็น default และเร็วที่สุดบน Linux modern

# ดู storage driver ปัจจุบัน
docker info | grep "Storage Driver"

# เปลี่ยนใน /etc/docker/daemon.json
{
  "storage-driver": "overlay2"
}

11.8 Docker Networking

11.8.1 Network Driver: bridge (default), host, none, overlay, macvlan, ipvlan

Driver Use Case คุณสมบัติ
bridge default, single host NAT, แยก network แต่ละกลุ่ม
host ไม่มี isolation container ใช้ network ของ host ตรง ๆ
none ไม่ต้องการ network ไม่มี interface
overlay multi-host (Swarm/K8s) network ข้ามเครื่องผ่าน VXLAN
macvlan container ดูเหมือน physical ได้ MAC ของตัวเอง
ipvlan คล้าย macvlan แต่ใช้ MAC ร่วม เร็ว, รองรับ L2/L3

11.8.2 การสร้าง Custom Network: docker network create

# สร้าง bridge network ของตัวเอง
docker network create \
  --driver bridge \
  --subnet 172.30.0.0/16 \
  --gateway 172.30.0.1 \
  mynet

# ดูรายการ network
docker network ls

# ดูรายละเอียด
docker network inspect mynet

# รัน container ใน network ของเรา
docker run -d --name web --network mynet nginx
docker run -d --name db --network mynet postgres

11.8.3 Container-to-Container Communication และ DNS ภายใน

ใน custom bridge network (ไม่ใช่ default bridge) Docker จะมี embedded DNS ทำให้ container เรียกหากันด้วยชื่อได้:

# จาก container "web" ติดต่อ "db" ด้วยชื่อ
docker exec web ping -c 2 db
# PING db (172.30.0.3): 56 data bytes
# 64 bytes from 172.30.0.3: ...

ข้อสำคัญ: default bridge (bridge) ไม่มี DNS ระหว่าง container ใหม่ — ต้องสร้าง custom network เอง

11.8.4 Port Publishing vs Exposing

11.8.5 Network Inspection และ Troubleshooting

# ดูการเชื่อมต่อของ container
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web

# ดู port mapping
docker port web

# ทดสอบ DNS จากใน container
docker exec web nslookup db

# ตรวจ firewall/iptables ของ host (Linux)
sudo iptables -t nat -L DOCKER -n

11.9 Docker Compose

Docker Compose ใช้สำหรับนิยามและรัน multi-container application ผ่านไฟล์ YAML แทนที่จะพิมพ์คำสั่ง docker run หลายบรรทัด

11.9.1 ไฟล์ compose.yaml / docker-compose.yml: services, networks, volumes, configs, secrets

ตัวอย่างไฟล์ครบเครื่องสำหรับ web app + database + cache:

# compose.yaml
name: myapp

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        APP_VERSION: "1.0"
    image: myapp:1.0
    container_name: myapp-web
    restart: unless-stopped
    ports:
      - "8080:8000"
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/myapp
      REDIS_URL: redis://cache:6379/0
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - frontend
      - backend
    volumes:
      - ./logs:/app/logs

  db:
    image: postgres:16-alpine
    container_name: myapp-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: myapp
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    container_name: myapp-cache
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true   # ไม่ออก internet ได้

volumes:
  pgdata:
  redisdata:

secrets:
  db_password:
    file: ./secrets/db_password.txt

11.9.2 คำสั่ง: up, down, ps, logs, exec, build, pull, restart, stop

# เริ่มทุก service (build ถ้ายังไม่มี)
docker compose up -d

# build ใหม่ก่อนเริ่ม
docker compose up -d --build

# ดูสถานะ
docker compose ps

# ดู log ของทุก service (หรือเฉพาะ service)
docker compose logs -f
docker compose logs -f web

# exec เข้า service
docker compose exec web bash
docker compose exec db psql -U app -d myapp

# rebuild service เดียว
docker compose build web

# stop ทุก service (container ยังอยู่)
docker compose stop

# down = stop + rm container + rm network (volume คงเดิม)
docker compose down

# down พร้อมลบ volume ด้วย (ระวัง!)
docker compose down -v

11.9.3 depends_on (service_started, service_healthy)

depends_on ระบุลำดับการ start แต่ default แค่รอให้ container "เริ่มต้น" ไม่ใช่ "พร้อม" ใช้ condition: service_healthy คู่กับ healthcheck เพื่อให้รอจริง:

depends_on:
  db:
    condition: service_healthy   # รอ healthcheck pass
  migrate:
    condition: service_completed_successfully  # รอ exit 0

11.9.4 healthcheck

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 30s        # ตรวจทุก 30s
  timeout: 5s          # timeout ของแต่ละครั้ง
  retries: 3           # fail กี่ครั้งจึงเป็น unhealthy
  start_period: 30s    # ระยะเริ่มต้น (fail ไม่นับ)

11.9.5 restart policy

ใน Compose ใช้ key restart:

restart: unless-stopped
# หรือ "no", "always", "on-failure[:max]"

11.9.6 Environment File (.env)

ไฟล์ .env ที่อยู่ข้าง compose.yaml จะถูกอ่านเป็น default ของตัวแปรใน YAML:

# .env
APP_VERSION=1.2.3
DB_PASSWORD=secret123
EXTERNAL_PORT=8080
# compose.yaml
services:
  web:
    image: myapp:${APP_VERSION}
    ports:
      - "${EXTERNAL_PORT}:8000"
    environment:
      DB_PASSWORD: ${DB_PASSWORD}

11.9.7 Profile (dev, prod)

ใช้ profiles เพื่อให้บาง service รันเฉพาะตอนต้องการ:

services:
  web:
    image: myapp
    # ไม่มี profile = รันเสมอ

  debug-tools:
    image: nicolaka/netshoot
    profiles: ["debug"]
    network_mode: container:web

  pgadmin:
    image: dpage/pgadmin4
    profiles: ["dev"]
    ports: ["5050:80"]
docker compose up -d                              # รันแค่ web
docker compose --profile dev up -d                # web + pgadmin
docker compose --profile dev --profile debug up   # ครบ

11.9.8 extends, include, override file

Compose โหลดไฟล์เริ่มต้นเรียงเป็น compose.yaml + compose.override.yaml อัตโนมัติ ใช้แยก dev/prod ได้ง่าย:

# compose.yaml (base – production-like)
services:
  web:
    image: myapp:${TAG:-latest}
    restart: unless-stopped
# compose.override.yaml (dev เพิ่มเข้าอัตโนมัติ)
services:
  web:
    build: .
    volumes:
      - .:/app           # bind source code
    environment:
      DEBUG: "true"
    ports:
      - "8080:8000"
# compose.prod.yaml (เรียกใช้แบบเลือก)
services:
  web:
    deploy:
      replicas: 3
    logging:
      driver: json-file
      options:
        max-size: "10m"
# Dev (auto-merge)
docker compose up -d

# Production
docker compose -f compose.yaml -f compose.prod.yaml up -d

11.9.9 Compose v1 vs v2 (docker compose CLI plugin)

Compose v2 เร็วกว่า, รองรับ profile/include/secret ดีกว่า, และเป็นมาตรฐานปัจจุบัน Compose v1 หยุดพัฒนาแล้ว


11.10 ตัวอย่างการประยุกต์ใช้งาน

11.10.1 LAMP/LEMP Stack (Nginx + PHP-FPM + MariaDB)

# compose.yaml – LEMP Stack
name: lemp-demo

services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
    volumes:
      - ./public:/var/www/html:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - php

  php:
    image: php:8.3-fpm-alpine
    volumes:
      - ./public:/var/www/html
    environment:
      DB_HOST: db
      DB_USER: app
      DB_PASS: secret

  db:
    image: mariadb:11
    environment:
      MARIADB_ROOT_PASSWORD: rootsecret
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: secret
    volumes:
      - mariadb_data:/var/lib/mysql

volumes:
  mariadb_data:

ไฟล์ nginx.conf ที่ต้องสร้างคู่กัน:

server {
    listen 80;
    server_name _;
    root /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
        include fastcgi_params;
    }
}

11.10.2 MEAN/MERN Stack (MongoDB + Express + React/Angular + Node)

# compose.yaml – MERN Stack
name: mern-demo

services:
  mongo:
    image: mongo:7
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: rootsecret
    volumes:
      - mongodata:/data/db

  api:
    build: ./backend
    environment:
      MONGO_URI: mongodb://root:rootsecret@mongo:27017/app?authSource=admin
      PORT: 4000
    ports:
      - "4000:4000"
    depends_on:
      - mongo

  web:
    build: ./frontend
    environment:
      VITE_API_URL: http://localhost:4000
    ports:
      - "3000:3000"
    depends_on:
      - api

volumes:
  mongodata:

11.10.3 Application + Database + Reverse Proxy + Cache

ตัวอย่างสมจริงสำหรับ production: Caddy เป็น reverse proxy พร้อม HTTPS อัตโนมัติ + app + Postgres + Redis

# compose.yaml – Production-like stack
name: prod-stack

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app

  app:
    build: .
    restart: unless-stopped
    expose:
      - "8000"
    environment:
      DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/app
      REDIS_URL: redis://cache:6379/0
    depends_on:
      db: { condition: service_healthy }
      cache: { condition: service_started }

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      retries: 5

  cache:
    image: redis:7-alpine
    restart: unless-stopped
    command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
    volumes:
      - redisdata:/data

volumes:
  caddy_data:
  caddy_config:
  pgdata:
  redisdata:

ไฟล์ Caddyfile ที่ต้องสร้าง:

example.com {
    encode gzip zstd
    reverse_proxy app:8000
}

11.10.4 Development Environment (dev container)

ใช้ Compose สร้าง environment เหมือนทีมทุกคน ลด "works on my machine":

# compose.dev.yaml
services:
  dev:
    image: mcr.microsoft.com/devcontainers/python:3.12
    volumes:
      - .:/workspaces/project:cached
      - vscode-extensions:/root/.vscode-server/extensions
    command: sleep infinity
    network_mode: service:db
    environment:
      DATABASE_URL: postgresql://dev:dev@localhost:5432/dev

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: dev
    ports:
      - "5432:5432"

volumes:
  vscode-extensions:

11.10.5 CI/CD Build Agent และ Testing Environment

# compose.test.yaml – ใช้ใน CI
services:
  test:
    build:
      context: .
      target: builder    # multi-stage: stop ที่ stage builder
    command: pytest -xvs --cov=app tests/
    environment:
      DATABASE_URL: postgresql://test:test@db:5432/test
    depends_on:
      db: { condition: service_healthy }

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test"]
      interval: 5s
      retries: 10
    tmpfs:
      - /var/lib/postgresql/data   # ใน RAM = test เร็ว ไม่ต้อง persist
# รันใน CI
docker compose -f compose.test.yaml up --abort-on-container-exit --exit-code-from test

11.10.6 Self-hosted Services (Nextcloud, Gitea, Jellyfin)

ตัวอย่างการ self-host บริการสามตัวด้วย Compose ตัวเดียว:

# compose.yaml – Self-hosted stack
name: home-services

services:
  # ---------- Nextcloud ----------
  nextcloud:
    image: nextcloud:28-apache
    restart: unless-stopped
    ports:
      - "8081:80"
    environment:
      MYSQL_HOST: nextcloud-db
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_PASSWORD: nc_secret
    volumes:
      - nextcloud_data:/var/www/html
    depends_on:
      - nextcloud-db

  nextcloud-db:
    image: mariadb:11
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    environment:
      MYSQL_ROOT_PASSWORD: rootsecret
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_PASSWORD: nc_secret
    volumes:
      - nextcloud_db:/var/lib/mysql

  # ---------- Gitea ----------
  gitea:
    image: gitea/gitea:1.21
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "2222:22"
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
    volumes:
      - gitea_data:/data

  # ---------- Jellyfin ----------
  jellyfin:
    image: jellyfin/jellyfin:latest
    restart: unless-stopped
    ports:
      - "8096:8096"
    volumes:
      - jellyfin_config:/config
      - jellyfin_cache:/cache
      - /home/moo/Media:/media:ro

volumes:
  nextcloud_data:
  nextcloud_db:
  gitea_data:
  jellyfin_config:
  jellyfin_cache:

11.11 Security ของ Container

ความเชื่อผิดที่พบบ่อย: "container แยกกันโดย default ก็ปลอดภัยแล้ว" ความจริงคือ container ทุกตัวแบ่ง kernel เดียวกัน ดังนั้นต้องมีการตั้งค่าเสริมหลายชั้น

flowchart TB
    subgraph SC["Container Security Layers"]
        direction TB
        L1["1. Image Security
(scan, sign, minimal base)"] L2["2. Build Security
(non-root, no secret)"] L3["3. Runtime Security
(read-only, drop caps,
seccomp/AppArmor)"] L4["4. Network Security
(internal network, firewall)"] L5["5. Supply Chain
(SBOM, provenance)"] end L1 --> L2 --> L3 --> L4 --> L5

11.11.1 รันแบบ non-root user

หลัก #1 ที่ควรทำเสมอ — แม้ container แยกจาก host แต่ถ้ารันด้วย root ใน container ก็เพิ่ม attack surface

# Dockerfile
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --chown=app:app . .
RUN npm ci --omit=dev
USER app
CMD ["node", "server.js"]

หรือบังคับจาก compose:

services:
  web:
    image: myapp
    user: "1000:1000"

11.11.2 Read-only Root Filesystem (--read-only)

ถ้า application ไม่ต้องเขียน root filesystem (ส่วนใหญ่ไม่ต้อง) ก็ทำให้ read-only เลย:

services:
  web:
    image: myapp
    read_only: true
    tmpfs:
      - /tmp:size=64m
      - /var/run:size=8m
    volumes:
      - logs:/app/logs   # เฉพาะที่ต้องเขียน

11.11.3 Image Scanning: Trivy, Grype, Docker Scout, Snyk

# Trivy
trivy image --severity HIGH,CRITICAL nginx:1.25

# Grype
grype nginx:1.25

# Docker Scout (built-in)
docker scout cves nginx:1.25

# ใส่ใน CI: fail ถ้าพบ critical
trivy image --exit-code 1 --severity CRITICAL myapp:1.0

11.11.4 Linux Security Module: Seccomp, AppArmor, SELinux profile

# Custom seccomp profile (JSON)
docker run --security-opt seccomp=./my-seccomp.json myapp

# AppArmor (Ubuntu/Debian)
docker run --security-opt apparmor=docker-default myapp

# disable seccomp (ไม่แนะนำ – เฉพาะ debug)
docker run --security-opt seccomp=unconfined myapp

11.11.5 Secret Management: Docker Secret, External Secret Manager

ห้าม ใส่ secret ใน Dockerfile หรือ environment variable ที่ leak ผ่าน docker inspect

services:
  app:
    image: myapp
    secrets:
      - source: db_password
        target: /run/secrets/db_password
        mode: 0400

secrets:
  db_password:
    file: ./secrets/db_password.txt
  # หรือดึงจาก external manager เช่น HashiCorp Vault, AWS Secrets Manager, SOPS

ใน application อ่านจาก /run/secrets/db_password แทน env var

11.11.6 Signed Image: Cosign, Docker Content Trust (Notary)

ใช้ Cosign (จาก Sigstore) เพื่อเซ็นและตรวจสอบ image:

# สร้าง keypair
cosign generate-key-pair

# เซ็น image
cosign sign --key cosign.key ghcr.io/moo/myapp:1.0

# ตรวจสอบก่อน pull
cosign verify --key cosign.pub ghcr.io/moo/myapp:1.0

11.11.7 Supply Chain Security: SBOM, provenance attestation

SBOM (Software Bill of Materials) = รายการ dependency ทั้งหมดของ image Provenance = หลักฐานว่าใคร/ที่ไหน/จากอะไร build image นี้

# สร้าง SBOM ด้วย syft
syft ghcr.io/moo/myapp:1.0 -o spdx-json > sbom.json

# Build พร้อม attestation ด้วย buildx
docker buildx build \
  --sbom=true \
  --provenance=true \
  -t ghcr.io/moo/myapp:1.0 \
  --push .

# ดู attestation
docker buildx imagetools inspect ghcr.io/moo/myapp:1.0

11.12 ทางเลือกอื่นและ Ecosystem

Docker ไม่ใช่ทางเลือกเดียว — ecosystem ของ container มีเครื่องมือมากมายตามวัตถุประสงค์ที่ต่างกัน

11.12.1 Podman, Buildah, Skopeo (Daemonless, rootless)

Podman เป็น drop-in replacement สำหรับ Docker จาก Red Hat ที่ออกแบบให้:

# ใช้งานเหมือน Docker เลย
podman run -d --name web -p 8080:80 nginx
podman pull alpine
podman build -t myapp .

# สร้าง pod (กลุ่ม container ที่แบ่ง network namespace ร่วมกัน)
podman pod create --name app -p 8080:80
podman run -d --pod app --name web nginx
podman run -d --pod app --name cache redis

ทีมเดียวกันนี้ยังมี Buildah (สร้าง image โดยไม่ต้อง daemon) และ Skopeo (copy image ระหว่าง registry โดยไม่ต้อง pull/push เต็ม)

11.12.2 nerdctl + containerd (โดยตรง)

หากต้องการคุย containerd ตรง ๆ (ไม่ผ่าน Docker) ใช้ nerdctl ที่มี syntax เหมือน Docker:

nerdctl run -d --name web -p 8080:80 nginx
nerdctl compose up -d

11.12.3 LXC / LXD / Incus (System Container)

LXC/LXD/Incus เป็น "system container" — ห่อหุ้มทั้ง init + service ให้รู้สึกเหมือน VM ขนาดเล็ก ต่างจาก Docker ที่เน้น "application container" (1 process / 1 container)

# Incus (fork ของ LXD ที่ active)
incus launch images:ubuntu/22.04 c1
incus exec c1 -- bash    # เข้า container เหมือน ssh เข้า VM

เหมาะกับการสร้าง dev environment, CI runner, หรือแทน VM ในงาน infrastructure

11.12.4 Kubernetes เบื้องต้น (Pod, Deployment, Service) – เส้นทางจาก Compose → Kompose → K8s

เมื่อ scale เกินเครื่องเดียว Kubernetes (K8s) เป็นมาตรฐาน:

แนวคิด Compose แนวคิด Kubernetes
service Deployment + Pod
port mapping Service (ClusterIP/NodePort/LoadBalancer)
environment ConfigMap, Secret
volume PersistentVolumeClaim
depends_on InitContainer, readinessProbe
network NetworkPolicy

Kompose แปลง compose.yaml เป็น Kubernetes manifest ได้ทันที:

kompose convert -f compose.yaml -o k8s/
kubectl apply -f k8s/

ตัวอย่าง K8s manifest ที่เทียบเท่า service หนึ่งใน Compose:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  selector:
    matchLabels: { app: web }
  template:
    metadata:
      labels: { app: web }
    spec:
      containers:
      - name: web
        image: ghcr.io/moo/myapp:1.0
        ports:
        - containerPort: 8000
        envFrom:
        - configMapRef: { name: web-config }
        - secretRef: { name: web-secret }
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector: { app: web }
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP

11.12.5 Docker Swarm เบื้องต้น (Service, Stack, Node)

Docker Swarm เป็น orchestrator ที่ฝังใน Docker engine ใช้ syntax เดียวกับ Compose แต่กระจายไปหลายเครื่องได้ — ติดตั้งง่ายกว่า K8s มาก เหมาะกับ small/medium cluster

# เริ่ม swarm บนเครื่องแรก (manager)
docker swarm init --advertise-addr 192.168.1.10

# เครื่องอื่น join เป็น worker
docker swarm join --token SWMTKN-... 192.168.1.10:2377

# deploy stack จาก compose.yaml ที่มี deploy: section
docker stack deploy -c compose.yaml myapp

# scale service
docker service scale myapp_web=5

# ดูสถานะ
docker service ls
docker service ps myapp_web
docker node ls

11.12.6 Nomad + Consul เป็นทางเลือกแทน K8s

HashiCorp Nomad เป็น orchestrator ที่ออกแบบให้ง่ายกว่า K8s และไม่ผูกอยู่แค่ container — รัน VM, raw binary, Java, Docker, Podman ได้ในตัวเดียวกัน เหมาะกับองค์กรขนาดกลางที่ต้องการ orchestration แต่ไม่ต้องการความซับซ้อนของ K8s

# example.nomad
job "web" {
  datacenters = ["dc1"]
  group "app" {
    count = 3
    network {
      port "http" { to = 8000 }
    }
    task "server" {
      driver = "docker"
      config {
        image = "ghcr.io/moo/myapp:1.0"
        ports = ["http"]
      }
      resources {
        cpu    = 500
        memory = 256
      }
    }
  }
}
nomad job run example.nomad

11.12.7 Devcontainer / Dev Environment

Devcontainer (มาตรฐานจาก Microsoft, ใช้ใน VS Code, JetBrains) คือการบรรยาย environment การพัฒนาในไฟล์ JSON เพื่อให้ทุกคนในทีมมี toolchain เหมือนกัน:

// .devcontainer/devcontainer.json
{
  "name": "Python + PostgreSQL",
  "dockerComposeFile": "../compose.dev.yaml",
  "service": "dev",
  "workspaceFolder": "/workspaces/project",
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance",
        "charliermarsh.ruff"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python"
      }
    }
  },
  "postCreateCommand": "pip install -r requirements-dev.txt",
  "remoteUser": "vscode"
}

ผลลัพธ์: clone repo → เปิดใน VS Code → เลือก "Reopen in Container" → ทุก dependency, extension, database พร้อมใช้งาน ภายในไม่กี่นาที


สรุปท้ายบท

Containerization ด้วย Docker และ Docker Compose เป็นเทคโนโลยีฐานที่นักพัฒนาและผู้ดูแลระบบยุคใหม่ต้องเข้าใจ จุดเริ่มต้นคือเข้าใจว่า container = namespace + cgroups + UnionFS ของ Linux kernel จากนั้นจึงเรียนรู้ Docker เป็นเครื่องมือทำให้ทุกอย่างนั้นใช้ง่าย เมื่อระบบเริ่มมีหลาย service ก็ใช้ Compose ในการรวมศูนย์การตั้งค่า และเมื่อระบบโตข้ามเครื่อง ก็ขยายไปยัง Swarm, Nomad หรือ Kubernetes ตามความเหมาะสม จุดสำคัญที่ห้ามลืมคือ ความปลอดภัย — รัน non-root, scan image, จัดการ secret ให้ถูกต้อง และตระหนักเสมอว่า container ใช้ kernel เดียวกับ host