Docker ตั้งแต่ศูนย์ถึงระดับมืออาชีพ: 100 ตัวอย่างด้วย Dockerfile, Compose และ CI/CD
Docker บรรจุแอปพลิเคชันพร้อมทุกอย่างที่มันต้องใช้ไว้ในคอนเทนเนอร์ ทำให้ซอฟต์แวร์เดียวกันทำงานเหมือนกันทั้งบนแล็ปท็อป เซิร์ฟเวอร์ และคลาวด์ บทความนี้พาคุณจากคอนเทนเนอร์แรกไปจนถึงการตั้งค่าที่คำนึงถึงโปรดักชันใน 100 ตัวอย่าง คุณจะได้เรียนคำสั่ง Docker CLI อิมเมจและ registry การเขียน Dockerfile พร้อม multi-stage build วอลุ่มและเครือข่าย Docker Compose สำหรับแอปหลายคอนเทนเนอร์ การตั้งค่าและความลับ ขีดจำกัดทรัพยากรและการตรวจสุขภาพ ความปลอดภัยของคอนเทนเนอร์ การดีบัก ไปป์ไลน์ CI/CD และมองเห็น Kubernetes กับ Swarm เป็นครั้งแรก หมายเหตุตามจริง: เครื่องที่ใช้เตรียมบทความนี้ไม่ได้ติดตั้ง Docker จึงไม่ได้รันตัวอย่างทั้ง 100 ตัวอย่างที่นี่ ทุกคำสั่งและไฟล์เขียนตามไวยากรณ์ทางการของ Docker และตรวจด้วยการอ่าน คุณควรรันเองก่อนนำไปใช้จริง
สิ่งที่ต้องเตรียม
Docker Desktop บน Windows หรือ macOS หรือ Docker Engine บน Linux พร้อมปลั๊กอิน Docker Compose (ควรใช้คำสั่ง docker compose version ได้) โปรแกรมแก้ไขข้อความอย่าง Visual Studio Code พร้อมส่วนขยาย Docker จะช่วยได้ ความรู้พื้นฐานเรื่องบรรทัดคำสั่งเพียงพอสำหรับการเริ่มต้น บางตัวอย่างใช้โปรเจกต์ Python หรือ Node.js ตัวอย่าง และบางตัวอย่างใช้ registry ส่วนตัว จึงควรใช้ตัวอย่างชื่อเช่น registry.example.com และไม่ควรใส่รหัสผ่านจริงในคำสั่ง
วิธีใช้ตัวอย่าง
ตัวอย่าง bash ใช้กับ Docker CLI และ Docker Compose ตัวอย่าง Dockerfile ให้บันทึกเป็นไฟล์ชื่อ Dockerfile ในโฟลเดอร์โปรเจกต์ ตัวอย่าง YAML ให้บันทึกเป็น compose.yaml หรือไฟล์ Kubernetes ตามที่ระบุ
หัวข้อที่ 1: เริ่มต้นกับ Docker
คอนเทนเนอร์คือโปรเซสที่แยกตัวออกมา มีระบบไฟล์และเครือข่ายของตัวเอง สร้างจากอิมเมจ เรียนรู้แนวคิดหลัก ตรวจการติดตั้ง และรันคอนเทนเนอร์แรกของคุณ
อิมเมจ คอนเทนเนอร์ และ registry
อิมเมจคือแม่แบบแบบอ่านอย่างเดียวที่ประกอบด้วยเลเยอร์ คอนเทนเนอร์คืออินสแตนซ์ที่กำลังทำงานของอิมเมจ ส่วน registry เช่น Docker Hub หรือ Azure Container Registry เก็บอิมเมจเพื่อให้เครื่องอื่นดาวน์โหลดได้
ทำไมคอนเทนเนอร์จึงช่วยได้
คอนเทนเนอร์รวมแอปกับไลบรารีที่ต้องใช้ จึงทำงานเหมือนกันทุกเครื่อง เริ่มทำงานได้ในไม่กี่วินาที และใช้เคอร์เนลของโฮสต์ร่วมกัน จึงกินหน่วยความจำน้อยกว่าเครื่องเสมือนเต็มรูปแบบมาก
ตรวจการติดตั้ง
รัน docker version เพื่อดูทั้งฝั่ง client และ server หากไม่มีบรรทัด server แปลว่า Docker Desktop หรือบริการ Docker ยังไม่ทำงาน ให้เริ่มก่อนรันคำสั่งคอนเทนเนอร์ใดๆ
ภาพประกอบ
ตัวอย่างที่ 1: ตรวจสอบว่าติดตั้ง Docker แล้ว
ยืนยันว่าโปรแกรม client เชื่อมกับ Docker engine ได้
docker --version
docker info --format "server: {{.ServerVersion}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 2: รัน hello-world
คอนเทนเนอร์แรกของคุณ ดาวน์โหลดและรันด้วยคำสั่งเดียว
docker run hello-world
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 3: ดาวน์โหลดอิมเมจ
โหลดอิมเมจโดยยังไม่เริ่มคอนเทนเนอร์
docker pull nginx:1.27
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 4: ดูรายการอิมเมจในเครื่อง
ดูว่ามีอิมเมจอะไรเก็บอยู่บนเครื่องนี้
docker images
docker image ls --filter "reference=nginx"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 5: ตรวจสอบรายละเอียดอิมเมจ
อ่านสถาปัตยกรรมและระบบปฏิบัติการของอิมเมจ
docker image inspect nginx:1.27 --format "arch={{.Architecture}} os={{.Os}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 6: ดูการใช้พื้นที่ดิสก์
ดูว่าอิมเมจ คอนเทนเนอร์ และวอลุ่มกินพื้นที่เท่าไร
docker system df
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 2: รันและจัดการคอนเทนเนอร์
คำสั่ง docker run สร้างคอนเทนเนอร์จากอิมเมจ คุณจะเปิดพอร์ต อ่าน log รันคำสั่งภายใน และล้างคอนเทนเนอร์ที่ไม่ต้องใช้แล้ว
โหมด detached และ interactive
-d รันคอนเทนเนอร์เบื้องหลัง ส่วน -it เชื่อมเทอร์มินัล --rm จะลบคอนเทนเนอร์เมื่อหยุดทำงาน เหมาะกับการทดลองสั้นๆ
พอร์ตและชื่อ
-p host:container เปิดพอร์ต ดังนั้น 8080:80 จะส่งพอร์ต 8080 ของโฮสต์ไปยังพอร์ต 80 ในคอนเทนเนอร์ --name ตั้งชื่อที่อ่านง่ายให้คอนเทนเนอร์ เพื่อใช้ในคำสั่งถัดไป
วงจรชีวิตและการล้างข้อมูล
คอนเทนเนอร์ที่หยุดแล้วยังเก็บข้อมูลไว้จนกว่าจะลบ ใช้ docker ps -a เพื่อดูและ docker rm เพื่อลบ และควรล้างเป็นประจำบนเครื่องที่ build บ่อย
ตัวอย่างที่ 7: รันคอนเทนเนอร์เบื้องหลัง
เริ่มคอนเทนเนอร์แบบ detached และแสดงรายการ
docker run -d --name web nginx:1.27
docker ps
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 8: เปิดพอร์ตของคอนเทนเนอร์
เข้าเว็บเซิร์ฟเวอร์ในคอนเทนเนอร์จากเครื่องของคุณ
docker run -d --name web-8080 -p 8080:80 nginx:1.27
curl http://localhost:8080
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 9: เปิด shell ชั่วคราว
ใช้ --rm เพื่อลบคอนเทนเนอร์เมื่อออกจาก shell
docker run -it --rm alpine:3.20 sh
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 10: ดูรายการคอนเทนเนอร์
แสดงคอนเทนเนอร์ที่กำลังทำงานและที่หยุดแล้ว
docker ps
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Image}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 11: อ่าน log ของคอนเทนเนอร์
แสดงบรรทัดท้ายสุดหรือดูผลลัพธ์แบบสด
docker logs --tail 50 web
docker logs -f web
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 12: รันคำสั่งในคอนเทนเนอร์
เปิด shell หรือรันคำสั่งหนึ่งครั้งในคอนเทนเนอร์ที่ทำงานอยู่
docker exec -it web sh
docker exec web cat /etc/nginx/nginx.conf
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 13: หยุด เริ่ม และรีสตาร์ต
ควบคุมวงจรชีวิตของคอนเทนเนอร์
docker stop web
docker start web
docker restart web
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 14: ลบคอนเทนเนอร์
ลบคอนเทนเนอร์ที่ไม่ต้องใช้แล้ว
docker rm -f web web-8080
docker container prune -f
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 3: อิมเมจและ Registry
อิมเมจมีชื่อ แท็ก และ digest แท็กเปลี่ยนแปลงได้ แต่ digest เปลี่ยนไม่ได้ จึงต้องใช้ทั้งคู่เพื่อตั้งแท็ก แชร์ และทำซ้ำซอฟต์แวร์ได้อย่างแม่นยำ
แท็กและ digest
latest เป็นเพียงแท็กที่จะเปลี่ยนทุกครั้งที่ผู้เผยแพร่อัปเดต digest ที่ขึ้นต้นด้วย sha256 ตามด้วยแฮชยาวๆ ระบุอิมเมจหนึ่งเดียวที่แน่นอน ซึ่งเป็นวิธีทำให้การปรับใช้ทำซ้ำได้
การ push ไป registry
ล็อกอิน ตั้งแท็กให้อิมเมจด้วยที่อยู่ registry เต็ม แล้ว push เครื่องอื่นจะดึงอิมเมจเดียวกันด้วยชื่อได้ ห้าม push อิมเมจที่มีความลับอยู่ข้างใน
อ่านประวัติของอิมเมจ
docker history แสดงแต่ละเลเยอร์ คำสั่งที่สร้างเลเยอร์นั้น และขนาด ช่วยหาเลเยอร์ที่ใหญ่ผิดปกติ และตรวจว่ารหัสผ่านไม่เคยถูกเขียนลงในอิมเมจ
ตัวอย่างที่ 15: ตั้งชื่อและแท็กอิมเมจ
ตั้งชื่อและเวอร์ชันของอิมเมจที่คุณดูแลเอง
docker tag nginx:1.27 mycompany/nginx:1.0.0
docker images mycompany/nginx
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 16: ส่งอิมเมจไปยัง registry
อัปโหลดอิมเมจเพื่อให้เครื่องอื่นดึงไปใช้ได้
docker login registry.example.com
docker tag mycompany/nginx:1.0.0 registry.example.com/team/nginx:1.0.0
docker push registry.example.com/team/nginx:1.0.0
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 17: ส่งไปยัง Azure Container Registry
ล็อกอินด้วย Azure CLI แล้วส่งอิมเมจไป registry
az acr login --name <registry-name>
docker tag mycompany/nginx:1.0.0 <registry-name>.azurecr.io/nginx:1.0.0
docker push <registry-name>.azurecr.io/nginx:1.0.0
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 18: รัน registry ในเครื่อง
เริ่ม registry ส่วนตัวบนเครื่องของคุณเพื่อทดสอบ
docker run -d -p 5000:5000 --restart unless-stopped --name registry registry:2
docker tag nginx:1.27 localhost:5000/nginx:1.27
docker push localhost:5000/nginx:1.27
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 19: ระบุอิมเมจด้วย digest
digest เป็นลายนิ้วมือที่ไม่เปลี่ยน ต่างจากแท็กที่ขยับได้
docker pull nginx:1.27
docker image inspect nginx:1.27 --format "{{index .RepoDigests 0}}"
docker run -d nginx@sha256:<digest-from-the-line-above>
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 20: อ่านเลเยอร์ของอิมเมจ
ดูว่าแต่ละคำสั่งสร้างเลเยอร์ใดและมีขนาดเท่าไร
docker history nginx:1.27
docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" nginx:1.27
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 4: เขียน Dockerfile
Dockerfile คือสูตรแบบข้อความ คำสั่งแต่ละบรรทัดสร้างเลเยอร์หนึ่งชั้น ลำดับและเนื้อหาของคำสั่งจึงกำหนดขนาด ความเร็วในการ build ใหม่ และสิ่งที่ทำงานเมื่อคอนเทนเนอร์เริ่ม
คำสั่งหลัก
FROM เลือกอิมเมจฐาน COPY และ RUN เพิ่มไฟล์และคำสั่ง WORKDIR กำหนดโฟลเดอร์สำหรับคำสั่งถัดไป CMD และ ENTRYPOINT กำหนดสิ่งที่คอนเทนเนอร์รัน
ARG เทียบกับ ENV
ARG มีอยู่เฉพาะตอนสร้างอิมเมจ ส่วน ENV คงอยู่ในอิมเมจและโปรแกรมที่รันอยู่มองเห็นได้ ใช้ ARG สำหรับตัวเลือกตอน build และใช้ ENV สำหรับค่าที่โปรแกรมอ่าน
การ build และตั้งแท็ก
docker build -t name:tag . ส่งโฟลเดอร์ให้ builder ใช้เฉพาะไฟล์ที่ Dockerfile คัดลอก จึงควรเก็บ build context ให้เล็กด้วยไฟล์ .dockerignore
ตัวอย่างที่ 21: Dockerfile ขั้นต่ำ
สูตรที่เล็กที่สุดที่ใช้งานได้: อิมเมจฐานและหนึ่งคำสั่ง
FROM alpine:3.20
CMD ["echo", "Hello from a container"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 22: สร้างและรันอิมเมจ
แปลง Dockerfile เป็นอิมเมจแล้วเริ่มทำงาน
docker build -t hello-image:1.0 .
docker run --rm hello-image:1.0
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 23: Dockerfile สำหรับแอป Python
ติดตั้ง dependency และคัดลอกโค้ดลงอิมเมจขนาดเล็ก
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 24: Dockerfile สำหรับแอป Node.js
ติดตั้งแพ็กเกจสำหรับใช้งานจริงและเปิดพอร์ต
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 25: ARG และ ENV
ตั้งค่าตอนสร้างอิมเมจและตัวแปรสภาพแวดล้อมตอนรัน
FROM alpine:3.20
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
ENV APP_PORT=8080
CMD ["sh", "-c", "echo version=$APP_VERSION port=$APP_PORT"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 26: ส่ง build argument
เปลี่ยนเวอร์ชันตอนสร้างโดยไม่ต้องแก้ไฟล์
docker build --build-arg APP_VERSION=1.4.2 -t vars-image:1.4.2 .
docker run --rm -e APP_PORT=9000 vars-image:1.4.2
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 27: WORKDIR และ COPY
กำหนดโฟลเดอร์ทำงานและคัดลอกไฟล์เข้าอิมเมจ
FROM alpine:3.20
WORKDIR /srv/site
COPY ./site/ ./
# ADD can unpack archives, but COPY is clearer for plain files
RUN ls -la /srv/site
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 28: ENTRYPOINT และ CMD
เลือกโปรแกรมหลักและอาร์กิวเมนต์เริ่มต้น
FROM alpine:3.20
RUN apk add --no-cache curl
ENTRYPOINT ["curl", "-s"]
CMD ["https://example.com"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 5: แนวปฏิบัติที่ดีสำหรับอิมเมจ
อิมเมจที่ดีมีขนาดเล็ก ทำซ้ำได้ build เร็ว และรันอย่างปลอดภัย multi-stage build การเรียงเลเยอร์ เวอร์ชันที่ตรึงไว้ ผู้ใช้ที่ไม่ใช่ root และ health check ครอบคลุมแนวปฏิบัติส่วนใหญ่
Multi-stage build
สเตจสำหรับ build เก็บคอมไพเลอร์และเครื่องมือ ส่วนสเตจสุดท้ายคัดลอกเฉพาะผลลัพธ์ อิมเมจสุดท้ายจึงเล็ก และไม่มีซอร์สโค้ดหรือเครื่องมือ build ที่ผู้โจมตีอาจใช้ได้
การแคชของเลเยอร์
Docker ใช้เลเยอร์ซ้ำเมื่อคำสั่งและอินพุตไม่เปลี่ยน และจะ build ใหม่ทั้งหมดหลังจากมีการเปลี่ยนแปลงครั้งแรก ให้คัดลอกไฟล์ dependency และติดตั้งก่อนคัดลอกซอร์สโค้ด
ผู้ใช้ เวอร์ชัน และการตรวจสอบ
รันด้วยผู้ใช้เป็นตัวเลขที่ไม่ใช่ root ตรึงเวอร์ชันอิมเมจฐาน เพิ่ม label ของเจ้าของและเวอร์ชัน และใช้ HEALTHCHECK เพื่อให้ Docker รายงานได้ว่าแอปทำงานจริงหรือไม่
ตัวอย่างที่ 29: ยกเว้นไฟล์ด้วย .dockerignore
ไม่ให้ความลับ ข้อมูล Git และ dependency เข้าไปในการ build
cat > .dockerignore <<'EOF'
.git
node_modules
*.log
.env
EOF
docker build -t app:1.0 .
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 30: Multi-stage build สำหรับ Go
คอมไพล์ในอิมเมจใหญ่ แล้วส่งเฉพาะโปรแกรมที่คอมไพล์แล้ว
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod ./
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 31: Multi-stage build สำหรับหน้าเว็บ
build ไฟล์ส่วนหน้า แล้วให้ Nginx เสิร์ฟ
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 32: รันด้วยผู้ใช้ที่ไม่ใช่ root
จำกัดความเสียหายหากแอปถูกโจมตี
FROM python:3.12-slim
RUN useradd --create-home --uid 10001 appuser
WORKDIR /home/appuser/app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 33: ตรึงเวอร์ชันของอิมเมจฐาน
หลีกเลี่ยงความผิดพลาดจากแท็ก latest ที่เปลี่ยนไปเรื่อยๆ
# Avoid "latest": it changes without warning
FROM python:3.12.5-slim-bookworm
# Pin system packages when the build must be reproducible
RUN apt-get update && apt-get install -y --no-install-recommends curl=8.* \
&& rm -rf /var/lib/apt/lists/*
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 34: จัดลำดับเลเยอร์เพื่อใช้แคช
คัดลอกไฟล์ dependency ก่อนเพื่อให้ build เร็ว
FROM node:20-alpine
WORKDIR /app
# Dependencies change less often than source code, so copy them first
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 35: เพิ่ม label ให้อิมเมจ
บันทึกชื่อ เวอร์ชัน และเจ้าของไว้ในอิมเมจ
FROM alpine:3.20
LABEL org.opencontainers.image.title="sample-api" \
org.opencontainers.image.version="1.4.2" \
org.opencontainers.image.maintainer="[email protected]"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 36: HEALTHCHECK
บอก Docker ว่าแอปยังทำงานปกติหรือไม่
FROM nginx:1.27-alpine
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost/ || exit 1
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 6: วอลุ่มและข้อมูลที่คงอยู่
คอนเทนเนอร์ทิ้งได้ แต่ฐานข้อมูลและไฟล์อัปโหลดทิ้งไม่ได้ วอลุ่มและ bind mount เก็บข้อมูลไว้นอกเลเยอร์ที่เขียนได้ของคอนเทนเนอร์ จึงอยู่รอดหลังรีสตาร์ตและอัปเกรด
named volume
named volume จัดการโดย Docker และเก็บอยู่นอกคอนเทนเนอร์ ใช้กับฐานข้อมูลได้ดี เพราะข้อมูลยังอยู่เมื่อลบคอนเทนเนอร์ และสำรองได้ด้วยคอนเทนเนอร์ช่วย
bind mount
bind mount เชื่อมโฟลเดอร์จากคอมพิวเตอร์เข้าไปในคอนเทนเนอร์ สะดวกตอนพัฒนาเพราะการแก้โค้ดเห็นผลทันที แต่คอนเทนเนอร์เข้าถึงไฟล์ของโฮสต์ได้โดยตรง จึงควรใช้ :ro เมื่อต้องการอ่านอย่างเดียว
การสำรองและข้อมูลชั่วคราว
สำรองวอลุ่มโดยรันคอนเทนเนอร์เล็กที่เมานต์วอลุ่มและเขียนไฟล์ tar ใช้ tmpfs สำหรับไฟล์ชั่วคราวที่ควรอยู่ในหน่วยความจำและหายไปเมื่อคอนเทนเนอร์หยุด
ตัวอย่างที่ 37: สร้าง named volume
เก็บข้อมูลที่ต้องอยู่รอดแม้คอนเทนเนอร์จะถูกลบ
docker volume create app-data
docker volume ls
docker volume inspect app-data
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 38: ใช้ named volume กับฐานข้อมูล
เก็บข้อมูล PostgreSQL ให้ปลอดภัยเมื่อรีสตาร์ตคอนเทนเนอร์
docker run -d --name db -e POSTGRES_PASSWORD=<strong-password> \
-v app-data:/var/lib/postgresql/data postgres:16
docker exec db psql -U postgres -c "select 1"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 39: bind mount โฟลเดอร์ของเครื่อง
แชร์โฟลเดอร์จากคอมพิวเตอร์ของคุณตอนพัฒนา
docker run --rm -it -v "$(pwd)/site:/usr/share/nginx/html:ro" -p 8081:80 nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 40: mount แบบอ่านอย่างเดียว
ให้คอนเทนเนอร์อ่านการตั้งค่าได้แต่แก้ไขไม่ได้
docker run --rm -v /srv/config:/etc/app:ro alpine:3.20 ls -l /etc/app
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 41: สำรองวอลุ่ม
บีบอัดข้อมูลในวอลุ่มเป็นไฟล์ tar ด้วยคอนเทนเนอร์ช่วย
docker run --rm -v app-data:/data -v "$(pwd):/backup" alpine:3.20 \
tar czf /backup/app-data-backup.tgz -C /data .
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 42: กู้คืนวอลุ่ม
นำข้อมูลที่สำรองไว้กลับไปใส่ในวอลุ่มใหม่
docker volume create app-data-restored
docker run --rm -v app-data-restored:/data -v "$(pwd):/backup" alpine:3.20 \
tar xzf /backup/app-data-backup.tgz -C /data
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 43: tmpfs สำหรับข้อมูลชั่วคราว
เก็บไฟล์ชั่วคราวในหน่วยความจำและลบเมื่อหยุด
docker run --rm --tmpfs /tmp:rw,size=64m alpine:3.20 sh -c "echo scratch > /tmp/x && cat /tmp/x"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 7: เครือข่ายของคอนเทนเนอร์
Docker ให้คอนเทนเนอร์มีเครือข่ายของตัวเอง เครือข่ายที่กำหนดเองทำให้คอนเทนเนอร์หากันด้วยชื่อได้ ส่วนการ publish พอร์ตกำหนดว่าบริการใดเข้าถึงจากภายนอกได้
เครือข่าย bridge และเครือข่ายที่กำหนดเอง
เครือข่าย bridge เริ่มต้นให้คอนเทนเนอร์มีเพียงที่อยู่ IP เครือข่าย bridge ที่กำหนดเองมี DNS ด้วย คอนเทนเนอร์หนึ่งจึงเข้าถึงอีกตัวด้วยชื่อได้ เช่น http://api หรือ redis://cache
การ publish พอร์ตอย่างปลอดภัย
โดยค่าเริ่มต้น พอร์ตถูก publish ทุกอินเทอร์เฟซ ให้ผูกกับ 127.0.0.1 เพื่อให้ฐานข้อมูลเข้าถึงได้จากคอมพิวเตอร์ของคุณเท่านั้น EXPOSE เป็นเพียงเอกสารและไม่เปิดพอร์ตจริง
หลายเครือข่าย
คอนเทนเนอร์เข้าร่วมได้หลายเครือข่าย วางฐานข้อมูลไว้ในเครือข่าย back และวางพร็อกซีเว็บไว้ใน front เพื่อให้เฉพาะบริการที่ต้องใช้ฐานข้อมูลเข้าถึงได้
ตัวอย่างที่ 44: ดูและตรวจสอบเครือข่าย
ดูเครือข่ายที่ Docker สร้างและช่วงที่อยู่ IP
docker network ls
docker network inspect bridge --format "{{range .IPAM.Config}}{{.Subnet}}{{end}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 45: สร้างเครือข่ายที่กำหนดเอง
ให้คอนเทนเนอร์ที่เกี่ยวข้องมีเครือข่ายส่วนตัว
docker network create app-net
docker run -d --name api --network app-net nginx:1.27
docker run --rm --network app-net alpine:3.20 wget -qO- http://api
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 46: หาคอนเทนเนอร์ด้วยชื่อ
ใช้ชื่อคอนเทนเนอร์เป็นชื่อโฮสต์ในเครือข่าย
docker run -d --name cache --network app-net redis:7-alpine
docker run --rm --network app-net redis:7-alpine redis-cli -h cache ping
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 47: เชื่อมคอนเทนเนอร์เข้าเครือข่ายที่สอง
ให้บริการหนึ่งเข้าถึงสองเครือข่ายที่แยกกันได้
docker network create back-net
docker network connect back-net api
docker inspect api --format "{{json .NetworkSettings.Networks}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 48: เปิดพอร์ตเฉพาะอินเทอร์เฟซเดียว
ให้ฐานข้อมูลเข้าถึงได้จากเครื่องนี้เท่านั้น
docker run -d -p 127.0.0.1:5432:5432 --name pg -e POSTGRES_PASSWORD=<strong-password> postgres:16
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 49: EXPOSE กับการ publish ต่างกันอย่างไร
EXPOSE เป็นเพียงเอกสาร ส่วน -p ทำให้เข้าถึงจากโฮสต์ได้
FROM node:20-alpine
# EXPOSE only documents the port; it does not open it to the host
EXPOSE 3000
CMD ["node", "server.js"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 50: ใช้เครือข่ายของโฮสต์
ใช้เครือข่ายของ Linux โดยตรงในกรณีพิเศษ
docker run --rm --network host alpine:3.20 sh -c "ip addr show eth0 | head -3"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 8: พื้นฐาน Docker Compose
Compose อธิบายแอปหลายคอนเทนเนอร์ในไฟล์ YAML เดียว คำสั่งเดียวเริ่ม หยุด และตรวจสอบทุกบริการ เครือข่าย และวอลุ่มของโปรเจกต์
ไฟล์ compose
คีย์ services ระบุคอนเทนเนอร์แต่ละตัว แต่ละบริการมีส่วน image หรือ build พร้อม ports, environment, volumes และ networks Compose สร้างเครือข่ายร่วมของโปรเจกต์ให้อัตโนมัติ
คำสั่ง
docker compose up -d เริ่มโปรเจกต์ docker compose ps แสดงสถานะ docker compose logs -f ติดตามผลลัพธ์ และ docker compose down หยุดและลบคอนเทนเนอร์พร้อมเครือข่ายเริ่มต้น
override
compose.yaml เก็บการตั้งค่าที่ใช้ร่วมกัน ส่วน compose.override.yaml ถูกรวมให้อัตโนมัติ ให้เก็บการแก้ไขในเครื่องไว้ในไฟล์ override เพื่อให้ไฟล์หลักเหมือนกันทุกสภาพแวดล้อม
ตัวอย่างที่ 51: ไฟล์ Compose ขั้นต่ำ
อธิบายบริการเว็บในไฟล์ YAML เดียว
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 52: เริ่มและหยุดโปรเจกต์ Compose
เปิดหรือปิดแอปที่มีหลายคอนเทนเนอร์ทั้งชุด
docker compose up -d
docker compose ps
docker compose down
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 53: สร้างอิมเมจภายใน Compose
ให้ Compose สร้างอิมเมจจาก Dockerfile
services:
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "3000:3000"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 54: ตัวแปรสภาพแวดล้อมใน Compose
ตั้งค่าของแต่ละบริการในไฟล์
services:
api:
build: ./api
environment:
APP_ENV: development
LOG_LEVEL: info
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 55: รอให้บริการอื่นพร้อม
ใช้ depends_on ควบคุมลำดับการเริ่ม
services:
api:
build: ./api
depends_on:
- db
db:
image: postgres:16
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 56: named volume ใน Compose
เก็บไฟล์ฐานข้อมูลไว้แม้โปรเจกต์รีสตาร์ต
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: <strong-password>
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 57: log, exec และคำสั่งครั้งเดียว
รันการทดสอบหรือเครื่องมือภายในโปรเจกต์
docker compose logs -f api
docker compose exec db psql -U postgres
docker compose run --rm api npm test
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 58: ไฟล์ override สำหรับการพัฒนา
แยกการตั้งค่าโปรดักชันออกจากการแก้ไขในเครื่อง
# compose.yaml holds the shared settings
services:
api:
build: ./api
# compose.override.yaml is merged automatically on "docker compose up"
services:
api:
volumes:
- ./api:/app
environment:
DEBUG: "1"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 9: Compose สำหรับแอปจริง
แอปจริงมีชั้นเว็บ ฐานข้อมูล แคช และพร็อกซี Compose เชื่อมทั้งหมดเข้าด้วยกัน รอให้พร้อมใช้งาน และปรับจำนวนส่วนที่ต้องการความจุมากขึ้น
เว็บ ฐานข้อมูล และแคช
ให้แต่ละบริการมีหน้าที่เดียว เก็บไฟล์ฐานข้อมูลใน named volume และเข้าถึงแต่ละบริการด้วยชื่อ ส่งสตริงการเชื่อมต่อผ่านตัวแปรสภาพแวดล้อม แทนการฝังชื่อโฮสต์ลงในโค้ด
ลำดับการเริ่มและสุขภาพ
depends_on รอเพียงให้คอนเทนเนอร์เริ่มทำงาน จึงควรเพิ่ม healthcheck ให้ฐานข้อมูลและใช้ condition: service_healthy เพื่อให้แอปเริ่มเมื่อฐานข้อมูลรับการเชื่อมต่อแล้วเท่านั้น
การปรับขนาดและการแยกเครือข่าย
docker compose up --scale เปลี่ยนจำนวนสำเนาของบริการที่ไม่เก็บสถานะ แยกเครือข่ายเพื่อให้พร็อกซีสาธารณะเข้าถึง API ได้ แต่เข้าถึงฐานข้อมูลโดยตรงไม่ได้
ตัวอย่างที่ 59: profile สำหรับบริการเสริม
เปิดเครื่องมือผู้ดูแลเฉพาะเมื่อต้องการ
services:
api:
build: ./api
pgadmin:
image: dpage/pgadmin4
profiles: ["tools"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 60: เริ่มต้น profile
เปิดบริการเสริมจากบรรทัดคำสั่ง
docker compose up -d
docker compose --profile tools up -d
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 61: เว็บแอปพร้อม PostgreSQL
แอปที่สมบูรณ์พร้อมฐานข้อมูลและข้อมูลที่คงอยู่
services:
web:
build: ./web
ports:
- "8000:8000"
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_DB: shop
POSTGRES_USER: shop
POSTGRES_PASSWORD: <strong-password>
volumes:
- pg-data:/var/lib/postgresql/data
volumes:
pg-data:
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 62: เพิ่ม Redis เป็นแคช
เชื่อมต่อที่เก็บคีย์-ค่าที่เร็วด้วยชื่อบริการ
services:
web:
build: ./web
environment:
REDIS_URL: redis://cache:6379
cache:
image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 63: Nginx เป็น reverse proxy
ส่งทราฟฟิกเว็บผ่านจุดเข้าเดียวไปยังแอป
services:
proxy:
image: nginx:1.27-alpine
ports:
- "80:80"
volumes:
- ./proxy/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
app:
build: ./app
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 64: รอจนฐานข้อมูลพร้อมใช้งาน
เริ่มแอปหลังจากฐานข้อมูลรับคำสั่งได้แล้ว
services:
api:
build: ./api
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: <strong-password>
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 10: การตั้งค่าและความลับ
การตั้งค่าเปลี่ยนไปตามสภาพแวดล้อม และความลับต้องไม่หลุดเข้าไปอยู่ในอิมเมจหรือใน Git ใช้ตัวแปรสภาพแวดล้อม ไฟล์ env และไฟล์ secret แล้วอ่านอย่างปลอดภัยในโปรแกรม
ตัวแปรสภาพแวดล้อมและไฟล์ .env
Compose อ่านไฟล์ .env เพื่อแทนค่าตัวแปรในไฟล์ compose ให้เพิ่ม .env ลงใน .gitignore และ commit ไฟล์ .env.example ที่มีค่าตัวอย่าง เพื่อให้นักพัฒนาคนอื่นรู้ว่าต้องตั้งค่าอะไร
ความลับในรูปแบบไฟล์
secret แบบไฟล์จะถูกเมานต์ไว้ใต้ /run/secrets ภายในคอนเทนเนอร์ การอ่านจากไฟล์ทำให้ค่าไม่ปรากฏในผลของ docker inspect และรายการโปรเซส ซึ่งตัวแปรสภาพแวดล้อมอาจทำให้รั่วได้
ห้ามฝังความลับลงในอิมเมจ
ค่าที่คัดลอกลงเลเยอร์จะยังอยู่ในประวัติอิมเมจ แม้ขั้นตอนหลังจะลบมันไปแล้ว ส่งความลับตอนรัน ตรวจประวัติด้วย docker history และหมุนเวียนความลับทุกตัวที่เคยถูก push
ตัวอย่างที่ 65: ปรับจำนวนบริการ
รันผู้ปฏิบัติงานหลายสำเนาพร้อมกัน
docker compose up -d --scale worker=3
docker compose ps worker
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 66: เครือข่าย front-end และ back-end
ไม่ให้ฐานข้อมูลอยู่บนเครือข่ายสาธารณะ
services:
proxy:
image: nginx:1.27-alpine
networks: [front]
ports: ["80:80"]
api:
build: ./api
networks: [front, back]
db:
image: postgres:16
networks: [back]
networks:
front:
back:
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 67: โหลดตัวแปรจาก .env
เก็บค่าในเครื่องออกจากไฟล์ Compose
cat > .env <<'EOF'
POSTGRES_PASSWORD=<strong-password>
APP_PORT=8000
EOF
docker compose config
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 68: ใช้ env_file
โหลดการตั้งค่าทั้งกลุ่มจากไฟล์
services:
api:
build: ./api
env_file:
- ./config/api.env
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 69: ไฟล์ secret ใน Compose
ส่งรหัสผ่านให้บริการโดยไม่ใส่ไว้ในอิมเมจ
services:
api:
build: ./api
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 70: อ่านการตั้งค่าใน Python
ใช้ค่าเริ่มต้นและแปลงสตริงจากสภาพแวดล้อมเป็นตัวเลข
import os
db_host = os.environ.get("DB_HOST", "localhost")
db_port = int(os.environ.get("DB_PORT", "5432"))
print(f"connect to {db_host}:{db_port}")
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 71: อ่านไฟล์ secret ใน Python
โหลดรหัสผ่านจากเส้นทาง secret ภายในคอนเทนเนอร์
from pathlib import Path
secret_file = Path("/run/secrets/db_password")
password = secret_file.read_text(encoding="utf-8").strip()
print("password length:", len(password))
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 72: การตั้งค่าตอนรัน
แทนค่าในการรันครั้งเดียวและตรวจประวัติของอิมเมจ
docker run --rm -e LOG_LEVEL=debug -e DB_HOST=db.internal app:1.0
docker history --no-trunc app:1.0 | grep -i password || echo "no password in the layers"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 11: ทรัพยากร สุขภาพ และ log
คอนเทนเนอร์สามารถใช้หน่วยความจำและ CPU ทั้งหมดของโฮสต์ได้ หากไม่กำหนดข้อจำกัด ขีดจำกัด นโยบายการรีสตาร์ต การตรวจสุขภาพ และการหมุน log ช่วยให้บริการเสถียรและดิสก์ไม่เต็ม
ขีดจำกัดหน่วยความจำและ CPU
--memory หยุดคอนเทนเนอร์ที่ใช้ RAM มากเกินไป และ --cpus จำกัดส่วนแบ่งเวลาประมวลผล ตั้งค่าจากการใช้งานที่วัดได้ด้วย docker stats แทนการเดา
นโยบายการรีสตาร์ต
unless-stopped รีสตาร์ตคอนเทนเนอร์หลังขัดข้องหรือเครื่องโฮสต์รีบูต แต่ไม่รีสตาร์ตเมื่อคุณหยุดเองด้วยมือ ใช้กับบริการที่ทำงานต่อเนื่อง และแก้สาเหตุของการขัดข้องซ้ำๆ แทนการพึ่งการรีสตาร์ต
log และสุขภาพ
Docker เก็บผลลัพธ์ของคอนเทนเนอร์ไว้ในไดรเวอร์ log จำกัดขนาด json-file ด้วย max-size และ max-file สถานะสุขภาพแสดง starting, healthy หรือ unhealthy ซึ่งตัวกระจายโหลดและ orchestrator ใช้ได้
ตัวอย่างที่ 73: จำกัดหน่วยความจำ
ไม่ให้โปรเซสที่ผิดปกติกินแรมทั้งหมด
docker run -d --name api-mem --memory 256m --memory-swap 256m nginx:1.27-alpine
docker stats --no-stream api-mem
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 74: จำกัด CPU
ให้คอนเทนเนอร์ใช้ CPU ครึ่งแกน
docker run -d --name api-cpu --cpus 0.5 nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 75: นโยบายการรีสตาร์ต
รีสตาร์ตบริการหลังจากขัดข้องหรือเครื่องรีบูต
docker run -d --name worker --restart unless-stopped app:1.0
docker inspect worker --format "{{.HostConfig.RestartPolicy.Name}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 76: จำกัดขนาดไฟล์ log
ไม่ให้ไฟล์ log เต็มดิสก์
docker run -d --name api-logs \
--log-driver json-file --log-opt max-size=10m --log-opt max-file=3 nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 77: อ่านสถานะสุขภาพ
ตรวจว่าคอนเทนเนอร์ปกติ กำลังเริ่ม หรือไม่ปกติ
docker ps --format "table {{.Names}}\t{{.Status}}"
docker inspect --format "{{.State.Health.Status}}" api
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 78: ล้างออบเจกต์ที่ไม่ได้ใช้
ลบคอนเทนเนอร์ที่หยุดและอิมเมจที่ค้างอย่างปลอดภัย
docker container prune -f
docker image prune -f
docker volume ls -qf dangling=true
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 12: ความปลอดภัยของคอนเทนเนอร์
คอนเทนเนอร์ใช้เคอร์เนลของโฮสต์ร่วมกัน การตั้งค่าที่ประมาทอาจทำให้ผู้โจมตีได้สิทธิ์มากกว่าที่ตั้งใจ สิทธิ์น้อยที่สุด ระบบไฟล์แบบอ่านอย่างเดียว การสแกน และอิมเมจฐานที่อัปเดตช่วยลดความเสี่ยง
สิทธิ์น้อยที่สุด
รันด้วยผู้ใช้ที่ไม่ใช่ root ตัด Linux capability ที่ไม่ต้องใช้ด้วย --cap-drop ALL และเพิ่มกลับเฉพาะที่จำเป็น ใช้ no-new-privileges เพื่อไม่ให้โปรเซสได้สิทธิ์เพิ่ม
ระบบไฟล์แบบอ่านอย่างเดียว
--read-only ป้องกันไม่ให้คอนเทนเนอร์เปลี่ยนไฟล์ของตัวเอง ให้ใช้ tmpfs กับไดเรกทอรีไม่กี่แห่งที่ต้องเขียนได้ เช่น /tmp
การสแกนและการอัปเดต
สแกนอิมเมจด้วย Docker Scout หรือเครื่องมืออื่น build ใหม่สม่ำเสมอเพื่อรับอิมเมจฐานที่แก้ไขแล้ว และให้ build ล้มเหลวเมื่อพบช่องโหว่ระดับวิกฤต ห้ามรันด้วย --privileged หากไม่มีเหตุผลเฉพาะ
ตัวอย่างที่ 79: ตัดความสามารถของ Linux
ลบสิทธิ์ที่แอปไม่ต้องใช้
docker run -d --name locked --cap-drop ALL --cap-add NET_BIND_SERVICE -p 80:80 nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 80: ระบบไฟล์รากแบบอ่านอย่างเดียว
ไม่ให้คอนเทนเนอร์เขียนไฟล์ระบบของตัวเอง
docker run -d --name ro-app --read-only --tmpfs /tmp --tmpfs /var/run nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 81: รันด้วยหมายเลขผู้ใช้
เลือกรหัสผู้ใช้แทนการเชื่อชื่อ
docker run --rm --user 10001:10001 alpine:3.20 id
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 82: ป้องกันการยกระดับสิทธิ์
ไม่ให้โปรเซสได้สิทธิ์เพิ่มระหว่างทำงาน
docker run -d --name hardened --security-opt no-new-privileges:true nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 83: สแกนอิมเมจหาช่องโหว่
หา CVE ที่รู้จักในแพ็กเกจภายในอิมเมจ
docker scout cves nginx:1.27-alpine
docker scout quickview nginx:1.27-alpine
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 84: การตั้งค่าความปลอดภัยใน Compose
รวมตัวเลือกเสริมความปลอดภัยในบริการเดียว
services:
api:
build: ./api
user: "10001:10001"
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
tmpfs: [/tmp]
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 13: การดีบักและแก้ปัญหา
เมื่อคอนเทนเนอร์ล้มเหลว คำตอบอยู่ใน log ข้อมูล inspect เหตุการณ์ และโปรเซส ขั้นตอนที่เป็นระบบช่วยหาปัญหาส่วนใหญ่ได้ภายในไม่กี่นาที
log เหตุการณ์ และ inspect
อ่าน docker logs ก่อนเสมอ docker events แสดงสิ่งที่ Docker ทำและเวลา ส่วน docker inspect แสดงรหัสออก สภาพแวดล้อม การเมานต์ และเครือข่าย เพื่อเปรียบเทียบคอนเทนเนอร์ที่กำลังรันกับที่คาดไว้
การตรวจทรัพยากรและโปรเซส
docker stats แสดง CPU และหน่วยความจำของแต่ละคอนเทนเนอร์ docker top แสดงรายการโปรเซส รหัสออก 137 มักหมายความว่าเคอร์เนลฆ่าโปรเซสเพราะใช้หน่วยความจำมากเกินไป ซึ่งยืนยันได้ด้วย OOMKilled
การดีบักเครือข่าย
รันอิมเมจเครื่องมือในเนมสเปซเครือข่ายเดียวกันด้วย --network container:name แล้วใช้ curl, dig และ ping จากข้างใน วิธีนี้ทดสอบมุมมองเครือข่ายจริงของคอนเทนเนอร์โดยไม่เปลี่ยนแปลงอะไร
ตัวอย่างที่ 85: ติดตาม event ของ Docker
เฝ้าดูคอนเทนเนอร์เริ่ม หยุด และล้มเหลวแบบเรียลไทม์
docker events --filter type=container --since 10m
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 86: ดูการใช้ทรัพยากร
ดู CPU และหน่วยความจำของแต่ละคอนเทนเนอร์
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 87: แสดงโปรเซสในคอนเทนเนอร์
ดูสิ่งที่กำลังทำงานอยู่ข้างในโดยไม่ต้องเปิด shell
docker top web
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 88: อ่านฟิลด์เดียวจาก inspect
ดู exit code และว่าเคอร์เนลฆ่าโปรเซสหรือไม่
docker inspect web --format "{{.State.ExitCode}} {{.State.OOMKilled}}"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 89: คัดลอกไฟล์เข้าและออก
ดึงไฟล์ตั้งค่าหรือส่งการแก้ไขเข้าคอนเทนเนอร์
docker cp web:/etc/nginx/nginx.conf ./nginx.conf
docker cp ./nginx.conf web:/etc/nginx/nginx.conf
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 90: ดีบักด้วยอิมเมจเครื่องมือเครือข่าย
ใช้เครือข่ายของคอนเทนเนอร์ร่วมกันแล้วทดสอบด้วย curl และ dig
docker run --rm -it --network container:web nicolaka/netshoot
# inside the tools shell: curl -v http://localhost:80
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 14: ไปป์ไลน์ CI/CD สำหรับอิมเมจ
ไปป์ไลน์จะ build ทดสอบ สแกน และ push อิมเมจทุกครั้งที่โค้ดเปลี่ยน การทำให้เป็นอัตโนมัติทำให้อิมเมจแต่ละตัวตามกลับไปยัง commit ได้ และการปล่อยเวอร์ชันเหมือนเดิมทุกรอบ
build และ push ใน CI
ไปป์ไลน์ล็อกอินด้วยข้อมูลรับรองที่มีอายุสั้น build อิมเมจ รันการทดสอบ แล้ว push พร้อมแท็กที่มี commit ขั้นตอนเดียวกันใช้ได้กับ GitHub Actions และ Azure Pipelines
กลยุทธ์การตั้งแท็ก
ตั้งแท็กแต่ละอิมเมจด้วย commit ของ Git เพื่อให้ตามรอยได้ และย้ายแท็กเวอร์ชันเมื่อได้รับการอนุมัติการปล่อยเท่านั้น หลีกเลี่ยงการพึ่ง latest บนโปรดักชัน เพราะมันซ่อนว่าโค้ดใดกำลังทำงานอยู่
การดูแลเครื่อง runner
runner ของ CI สะสมเลเยอร์และวอลุ่ม ควรล้างแคช build ตามตารางเวลา และกำหนดขนาดสูงสุดของแคช เพื่อไม่ให้เครื่อง build ดิสก์เต็มจนล้มเหลวแบบคาดเดาไม่ได้
ตัวอย่างที่ 91: build และ push ด้วย GitHub Actions
อัตโนมัติการสร้างอิมเมจทุกครั้งที่ push ไป main
name: build-image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/example/app:${{ github.sha }}
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 92: สร้างอิมเมจใน Azure Pipelines
ใช้ Docker task กับ service connection
trigger:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
command: buildAndPush
containerRegistry: my-acr-connection
repository: sample-api
tags: |
$(Build.BuildId)
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 93: ติดแท็กอิมเมจด้วย Git commit
ตามรอยอิมเมจกลับไปยังซอร์สโค้ดที่แน่นอน
SHA=$(git rev-parse --short HEAD)
docker build -t app:$SHA .
docker tag app:$SHA app:latest
echo "built app:$SHA"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 94: ล้างแคช build บนเครื่อง CI
ไม่ให้เครื่อง build ดิสก์เต็ม
docker builder prune --filter until=72h -f
docker image prune -a --filter until=168h -f
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
หัวข้อที่ 15: โปรดักชันและการจัดการคอนเทนเนอร์
เครื่องเดียวเพียงพอสำหรับบริการขนาดเล็ก เมื่อต้องการหลายเครื่อง การซ่อมตัวเองและการอัปเดตแบบค่อยเป็นค่อยไป orchestrator เช่น Kubernetes หรือ Docker Swarm จะรันคอนเทนเนอร์ให้
เช็กลิสต์โปรดักชัน
ตรึงเวอร์ชันอิมเมจ ตั้งขีดจำกัดหน่วยความจำ กำหนดนโยบายรีสตาร์ต เพิ่ม health check ผูกพอร์ตกับอินเทอร์เฟซที่ต้องการ เก็บความลับไว้นอกอิมเมจ และส่ง log ไปยังที่เก็บกลาง
พื้นฐาน Kubernetes
Deployment รักษาจำนวนแรปลิกาให้ทำงานอยู่และอัปเดตแบบค่อยเป็นค่อยไป Service ให้แรปลิกาทั้งหมดมีที่อยู่คงที่ อิมเมจคอนเทนเนอร์เดียวกันทำงานโดยไม่ต้องเปลี่ยนทั้งใน Compose และ Kubernetes
Swarm และเมื่อไรควรใช้
Docker Swarm ใช้ไฟล์แบบ Compose ในรูปแบบ stack และรันบริการข้ามหลายเครื่อง Kubernetes มีระบบนิเวศที่ใหญ่กว่าและพบมากในบริการคลาวด์แบบจัดการ จึงควรเลือกเครื่องมือที่ทีมของคุณดูแลได้
ตัวอย่างที่ 95: การตั้งค่า Compose สำหรับโปรดักชัน
ตรึงเวอร์ชัน จำกัดหน่วยความจำ รีสตาร์ต และตรวจสุขภาพ
services:
api:
image: registry.example.com/team/api:1.4.2
restart: unless-stopped
env_file: ./config/api.env
ports:
- "127.0.0.1:8000:8000"
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 3s
retries: 3
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 96: Kubernetes Deployment
รันอิมเมจเดียวกันสามสำเนาในคลัสเตอร์
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: registry.example.com/team/api:1.4.2
ports:
- containerPort: 8000
resources:
limits:
memory: 512Mi
cpu: "500m"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 97: Kubernetes Service
ให้สำเนาทั้งหมดมีที่อยู่คงที่ภายในคลัสเตอร์
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 8000
type: ClusterIP
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 98: ปล่อย stack ของ Docker Swarm
รันบริการเดียวกันบนหลายเครื่อง
docker swarm init
docker stack deploy -c stack.yml shop
docker service ls
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 99: ตรวจคอนเทนเนอร์ด้วย Docker SDK
อ่านสถานะคอนเทนเนอร์จากโค้ด Python
import docker
client = docker.from_env()
for c in client.containers.list():
state = c.attrs["State"].get("Health", {}).get("Status", "no healthcheck")
print(c.name, state)
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
ตัวอย่างที่ 100: สคริปต์เช็กลิสต์ก่อนปล่อยเวอร์ชัน
หยุดการปล่อยถ้าอิมเมจรันเป็น root หรือมี CVE ระดับวิกฤต
#!/usr/bin/env bash
set -euo pipefail
IMAGE="$1"
docker image inspect "$IMAGE" > /dev/null
USER_ID=$(docker run --rm --entrypoint id "$IMAGE" -u)
[ "$USER_ID" != "0" ] || { echo "image runs as root"; exit 1; }
docker scout cves --exit-code --only-severity critical "$IMAGE"
echo "release checks passed for $IMAGE"
*ตัวอย่างนี้ยังไม่ได้รันที่นี่ เพราะเครื่องที่เตรียมบทความไม่ได้ติดตั้ง Docker*
แล็บ: สร้าง รัน และปล่อยเว็บแอปขนาดเล็ก
นำทั้งบทความมารวมในโปรเจกต์เดียว: เว็บแอปขนาดเล็ก Dockerfile ที่มี multi-stage build ไฟล์ Compose ที่มีฐานข้อมูลและวอลุ่ม การตรวจสุขภาพ และการตรวจก่อนปล่อยเวอร์ชัน
1. สร้างโฟลเดอร์ที่มีแอปเล็กๆ และไฟล์ requirements.txt จากนั้นเขียน Dockerfile ที่รันแอปด้วยผู้ใช้ที่ไม่ใช่ root และมี HEALTHCHECK
2. เพิ่มไฟล์ .dockerignore แล้ว build อิมเมจพร้อมแท็กเวอร์ชัน เช่น docker build -t lab-web:1.0.0 .
3. เขียน compose.yaml ที่มีบริการเว็บและบริการ PostgreSQL ที่ใช้ named volume และ healthcheck แล้วเริ่มด้วย docker compose up -d
4. ตรวจสถานะสุขภาพด้วย docker compose ps และอ่าน log ด้วย docker compose logs -f web
5. รัน docker scout cves กับอิมเมจ แก้ไขสิ่งที่ทำได้ด้วยการอัปเดตอิมเมจฐาน แล้ว build ใหม่
6. รันเช็กลิสต์ก่อนปล่อยจากตัวอย่างที่ 100 กับอิมเมจของคุณ และหยุดการปล่อยหากไม่ผ่าน
7. หยุดโปรเจกต์ด้วย docker compose down แล้วลบ named volume เฉพาะเมื่อไม่ต้องใช้ข้อมูลในนั้นแล้ว
ขั้นต่อไป
หลังจากทำแล็บ ให้อ่านเอกสาร Docker อย่างเป็นทางการสำหรับ runtime ที่คุณใช้ ฝึกในโปรเจกต์แยก และเรียน orchestrator หนึ่งตัวอย่างลึกซึ้งเมื่อบริการของคุณต้องใช้มากกว่าหนึ่งเครื่อง เก็บตัวอย่างในบทความนี้ไว้ในรีโพซิทอรี เพื่อรันซ้ำได้หลังทุกการอัปเดต Docker
สรุป
Docker เปลี่ยนแอปพลิเคชันให้เป็นอิมเมจที่รันได้เหมือนกันทุกที่ คุณได้เรียนการรันและจัดการคอนเทนเนอร์ ตรึงอิมเมจด้วยแท็กและ digest เขียน Dockerfile ที่มีประสิทธิภาพด้วย multi-stage build เก็บข้อมูลในวอลุ่ม เชื่อมบริการด้วยเครือข่าย อธิบายแอปทั้งชุดด้วย Compose จัดการการตั้งค่าและความลับอย่างปลอดภัย จำกัดทรัพยากร เสริมความปลอดภัยคอนเทนเนอร์ ดีบักอย่างเป็นระบบ ทำให้การ build เป็นอัตโนมัติด้วย CI/CD และเตรียมพร้อมสำหรับการจัดการในโปรดักชัน