
Containerization คือเทคโนโลยีที่ปฏิวัติวิธีการพัฒนา ส่งมอบ และรันซอฟต์แวร์ โดยห่อหุ้มแอปพลิเคชันและ dependency ทั้งหมดไว้ในหน่วยที่แยกขาดและพกพาได้ บทนี้จะกล่าวถึงแนวคิดของ Container, สถาปัตยกรรมและการใช้งาน Docker, รวมถึง Docker Compose สำหรับจัดการแอปพลิเคชันแบบหลายบริการ พร้อมตัวอย่างที่นำไปใช้งานจริงได้ทันที
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
ข้อแตกต่างพื้นฐานระหว่าง 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 |
ในเชิงคณิตศาสตร์ เราสามารถเปรียบเทียบประสิทธิภาพการใช้ทรัพยากรได้ดังนี้:
โดยกำหนดให้:
จากสมการนี้จะเห็นได้ว่า เมื่อ ถูกตัดออกในกรณี container ทำให้ความหนาแน่นในการรันแอปพลิเคชันสูงกว่า VM อย่างมีนัยสำคัญ
เทคโนโลยี 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 ทำงานร่วมกันได้
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
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
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 # เห็นค่าใหม่
นอกจาก namespace และ cgroups แล้ว Linux ยังมีกลไกความปลอดภัยเพิ่มเติมที่ Container ใช้เพื่อจำกัดสิทธิ์:
CAP_NET_ADMIN, CAP_SYS_TIME เพื่อให้ container มีเฉพาะที่จำเป็น Docker จะ drop capabilities ส่วนใหญ่ทิ้งโดย defaultkexec_load, clock_settimeตัวอย่างการรัน container แบบจำกัด capability:
# Drop ทุก capability แล้วเพิ่มเฉพาะที่จำเป็น
docker run --rm \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
nginx:alpine
Open Container Initiative (OCI) เป็นองค์กรกลางที่กำหนดมาตรฐานเพื่อให้ ecosystem ของ container ทำงานข้ามเครื่องมือกันได้ มี 3 specification หลัก:
runc, crun, youkiจากมาตรฐานนี้ทำให้ทุกวันนี้เราสามารถสร้าง image ด้วย buildah แล้วรันด้วย podman หรือ push ไป Harbor และ pull ด้วย 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)"]
dockerd เป็น long-running daemon ที่รับคำสั่งจาก client ผ่าน REST API (โดย default คือ UNIX socket /var/run/docker.sock) มีหน้าที่:
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"
ใต้ Docker Engine ยังมีอีกหลายชั้น:
Registry คือบริการเก็บ image แบบ HTTP (ตามมาตรฐาน OCI Distribution) เมื่อรัน docker pull nginx Docker จะติดต่อ registry (default: Docker Hub) เพื่อดาวน์โหลด manifest และ layer ทั้งหมด แต่ละ layer สามารถถูก reuse โดย image อื่นเพื่อประหยัดพื้นที่
เนื่องจาก container ต้องการ Linux kernel ดังนั้นบน macOS และ Windows Docker Desktop จะ:
ผลคือ filesystem ของ container อยู่ใน VM ทำให้ bind mount จาก host ช้ากว่าบน Linux native — สำคัญที่ต้องเข้าใจเมื่อพัฒนาบน macOS/Windows
บน 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
Docker Desktop เป็นแอปพลิเคชันแบบ GUI ที่รวม Docker Engine + Compose + Kubernetes + Dashboard เหมาะกับการพัฒนาบนเครื่องส่วนตัว
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
วิธีให้ 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 แทน
/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
วงจรชีวิตพื้นฐานของการใช้ 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
คำสั่งสำหรับดูข้อมูลและจัดการ:
# ดู 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
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
-d (detached) — รันใน background ทันที เหมาะกับ service เช่น web server-it (interactive + tty) — รันแบบมี shell โต้ตอบ เหมาะกับการ debug หรือใช้เป็น sandbox# 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 อัตโนมัติเมื่อออก
-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
ส่ง 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
จำกัดทรัพยากรเพื่อป้องกัน 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
| 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
Dockerfile คือไฟล์ข้อความที่บอกขั้นตอนการสร้าง image ทุก instruction จะกลายเป็น layer หนึ่ง
| 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"] |
หนึ่งในจุดสับสนที่สุดของ 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 form — cmd arg1 arg2 — รันผ่าน /bin/sh -c ทำให้ process ที่เห็น PID 1 คือ shell ไม่ใช่แอปจริง อาจมีปัญหาเรื่องการ stop graceful
แนวทาง: ใช้ COPY เป็น default ใช้ ADD เฉพาะกรณีต้องการแตก tar (URL ควรใช้ RUN curl แทนเพราะควบคุมได้ดีกว่า)
ทุก 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 . .
.dockerignoreเหมือน .gitignore ใช้ระบุไฟล์ที่ไม่ควรส่งเข้า build context — ลดขนาดและป้องกันข้อมูลลับรั่ว:
.git
.gitignore
node_modules
__pycache__
*.pyc
*.log
.env
.env.*
!.env.example
.vscode
.idea
*.md
Dockerfile*
docker-compose*.yml
เทคนิคที่ใช้หลาย 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
docker buildxBuildKit เป็น 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
สร้าง 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 .
ตัวอย่าง 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"]
หลักสำคัญ:
FROM python@sha256:...), pin version ของ dependencyDocker Hub (hub.docker.com) เป็น registry default ของ Docker มีทั้ง:
nginx, python, postgresbitnami/*| 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 |
สำหรับองค์กรที่ต้องการ 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
# 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
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 และความปลอดภัย
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
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 ชั่วคราว, ไฟล์ที่ไม่ต้องการให้คงอยู่ |
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
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
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
Storage driver คือกลไกที่ Docker ใช้รวม layer ของ image ปัจจุบัน overlay2 เป็น default และเร็วที่สุดบน Linux modern
# ดู storage driver ปัจจุบัน
docker info | grep "Storage Driver"
# เปลี่ยนใน /etc/docker/daemon.json
{
"storage-driver": "overlay2"
}
| 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 |
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
ใน 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 เอง
--expose — เป็นแค่ metadata บอกว่า image ใช้ port อะไร ไม่เปิด port จริง-p / --publish — เปิด port จริงให้ host เข้าถึงได้# ดูการเชื่อมต่อของ 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
Docker Compose ใช้สำหรับนิยามและรัน multi-container application ผ่านไฟล์ YAML แทนที่จะพิมพ์คำสั่ง docker run หลายบรรทัด
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
# เริ่มทุก 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
depends_on ระบุลำดับการ start แต่ default แค่รอให้ container "เริ่มต้น" ไม่ใช่ "พร้อม" ใช้ condition: service_healthy คู่กับ healthcheck เพื่อให้รอจริง:
depends_on:
db:
condition: service_healthy # รอ healthcheck pass
migrate:
condition: service_completed_successfully # รอ exit 0
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s # ตรวจทุก 30s
timeout: 5s # timeout ของแต่ละครั้ง
retries: 3 # fail กี่ครั้งจึงเป็น unhealthy
start_period: 30s # ระยะเริ่มต้น (fail ไม่นับ)
ใน Compose ใช้ key restart:
restart: unless-stopped
# หรือ "no", "always", "on-failure[:max]"
.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}
ใช้ 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 # ครบ
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
docker-compose (มี dash) เป็น Python script แยกdocker compose (เว้นวรรค) เขียนด้วย Go เป็น plugin ของ Docker CLICompose v2 เร็วกว่า, รองรับ profile/include/secret ดีกว่า, และเป็นมาตรฐานปัจจุบัน Compose v1 หยุดพัฒนาแล้ว
# 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;
}
}
# 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:
ตัวอย่างสมจริงสำหรับ 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
}
ใช้ 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:
# 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
ตัวอย่างการ 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:
ความเชื่อผิดที่พบบ่อย: "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
หลัก #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"
--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 # เฉพาะที่ต้องเขียน
# 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
# 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
ห้าม ใส่ 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
ใช้ 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
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
Docker ไม่ใช่ทางเลือกเดียว — ecosystem ของ container มีเครื่องมือมากมายตามวัตถุประสงค์ที่ต่างกัน
Podman เป็น drop-in replacement สำหรับ Docker จาก Red Hat ที่ออกแบบให้:
alias docker=podman ก็ใช้แทนได้# ใช้งานเหมือน 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 เต็ม)
หากต้องการคุย containerd ตรง ๆ (ไม่ผ่าน Docker) ใช้ nerdctl ที่มี syntax เหมือน Docker:
nerdctl run -d --name web -p 8080:80 nginx
nerdctl compose up -d
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
เมื่อ 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
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
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
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