Sign in

Part of 10306343 การจัดการโครงสร้างพื้นฐานและการส่งมอบซอฟต์แวร์อย่างต่อเนื่อง (Development Operations: DevOps)

2026-10-20 · updated 2026-10-29 · 26 min read

บทที่ 7 Docker Compose: รันแอปคู่กับฐานข้อมูลด้วย network, volume และ healthcheck

ShareFacebookXLinkedInLINE

แผนบริหารการสอนประจำบทที่ 7

หัวข้อเนื้อหา

  • 7.1 แอปหลายบริการและบทบาทของ Docker Compose
  • 7.2 โครงสร้างของ compose.yaml และวงจรคำสั่งพื้นฐาน
  • 7.3 network และการเรียกกันด้วยชื่อบริการ
  • 7.4 volume และข้อมูลที่ต้องคงอยู่
  • 7.5 healthcheck และลำดับการเริ่มบริการ
  • 7.6 ตัวแปรสภาพแวดล้อมและไฟล์ .env
  • 7.7 แยกพอร์ตของเครื่องกับพอร์ตในเครือข่ายบริการ
  • 7.8 เริ่มแล้ว พร้อมแล้ว และยังพร้อมอยู่เป็นคนละเงื่อนไข
  • 7.9 volume ไม่ใช่สำเนาสำรอง
  • 7.10 การแก้ปัญหาโดยไม่ทำลายสภาพตั้งต้น
  • ปฏิบัติการที่ 7 รันแอปทีมคู่กับฐานข้อมูลด้วย Compose (Lab A–E)

วัตถุประสงค์เชิงพฤติกรรม

เมื่อศึกษาบทนี้และทำปฏิบัติการครบถ้วนแล้ว ผู้เรียนสามารถปฏิบัติได้ตามผลลัพธ์การเรียนรู้ต่อไปนี้ โดยมีงานที่ใช้วัดผลกำกับไว้

รหัสผลลัพธ์การเรียนรู้วัดจากCLO ของรายวิชา
7.1กำหนดค่า Compose 2 บริการ (แอป + ฐานข้อมูล) ด้วย network, volume และ healthcheck ให้ข้อมูลคงอยู่หลัง docker compose down และ upงานข้อ 1CLO2

CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):

  • CLO2 สามารถกำหนดค่าและใช้งาน DevOps workflow ที่กำหนดมา เพื่อให้กระบวนการพัฒนา–ทดสอบ–ส่งมอบซอฟต์แวร์เป็นไปอย่างถูกต้องและมีประสิทธิภาพ

บทนี้มีผลลัพธ์การเรียนรู้เพียงข้อเดียวตาม 10306343-traceability-matrix.mdx แต่ข้อนี้ประกอบด้วยองค์ประกอบสามส่วน คือ network, volume และ healthcheck ซึ่งต้องผ่านครบทั้งสามส่วนจึงถือว่าบรรลุผลลัพธ์

วิธีสอนและกิจกรรมการเรียนการสอน

  1. บรรยายแนวคิดตามหัวข้อ 7.1–7.6 โดยเริ่มจากปัญหาของการสั่ง docker run หลายคำสั่งด้วยมือ แล้วใช้คำถามตรวจความเข้าใจท้ายหัวข้อเป็นกิจกรรมอภิปรายเป็นคู่
  2. สาธิตการรัน docker compose up กับแอปและฐานข้อมูล แล้วสั่ง down และ up ใหม่สองรอบ รอบแรกไม่มี volume และรอบที่สองมี volume ให้เห็นผลต่างของข้อมูล
  3. ให้ผู้เรียนเรียกบริการ db จาก container ของแอปด้วยชื่อบริการและด้วย localhost แล้วอธิบายว่าเหตุใดผลจึงต่างกัน
  4. ปฏิบัติการ Lab A–E บน repository ทีม แต่ละ Lab ประกอบด้วยส่วนฝึกตามตัวอย่างและส่วนที่ผู้เรียนต้องตัดสินใจเอง
  5. มอบหมายแบบฝึกหัดท้ายบท Learning Log ครั้งที่ 7 และการศึกษาค้นคว้าเพิ่มเติม

สื่อการเรียนการสอน

การวัดผลและการประเมินผล

กำหนดส่ง: ก่อนเริ่มเรียนบทที่ 9 เพื่อเว้นระยะหนึ่งบท

ข้องานหลักฐานผลลัพธ์CLO
1M6 ข้อมูลคงอยู่หลัง down/upลิงก์ pull request ของตนเองที่ merge แล้ว (Lab A, B, D หรือ E) · ผลรันของตนเองจาก main ที่แสดง SELECT ก่อน docker compose down และหลัง docker compose up ได้ข้อมูลเดิม · ผล docker compose ps ที่ทั้งสองบริการมีสถานะ (healthy)7.1CLO2
2Learning Log ครั้งที่ 7ตอบ 4 หัวข้อ: สิ่งที่เรียนในบทนี้ · คำสั่งหรือข้อความ error ที่พบและแก้อย่างไร · แหล่งที่ใช้ค้นหาคำตอบ (ระบุลิงก์เอกสารทางการอย่างน้อย 1 แหล่ง) · ส่วนใดของ compose.yaml ที่ยังไม่เข้าใจ—บันทึกสะท้อนการเรียนรู้ ไม่มีหมวดคะแนน portfolio แยก

เกณฑ์ให้คะแนน

ข้อผ่านยังไม่ผ่าน
1main มี compose.yaml ที่มีบริการ app และ db · มี named volume ของฐานข้อมูล · มี healthcheck ทั้งสองบริการและ depends_on แบบ service_healthy · มี .env.example และไม่มีรหัสผ่านใน compose.yaml · ผล SELECT หลัง down/up ได้แถวเดิม · ผล docker compose ps แสดง (healthy) ทั้งสองบริการไม่มี pull request ของตนเอง หรือข้อมูลหายหลัง down/up หรือมีรหัสผ่านจริงใน compose.yaml หรือไฟล์ .env ถูก commit
2ตอบครบ 4 หัวข้อ และมีลิงก์เอกสารทางการขาดหัวข้อ หรือไม่มีแหล่งอ้างอิง

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

การคิดคะแนนฉบับปัจจุบัน

มติผู้สอน 28 ต.ค. 2569: เข้าเรียน 10% · ใบงานบทที่ 1–13 รวม 40% · FP 20% · FW 20% · โครงงาน 10% ส่วนบทที่ 14–15 ฝึกเพื่อรับข้อเสนอแนะ ไม่นับคะแนน

ใบงาน C07-WS01 มีคะแนนดิบเต็ม 10 คะแนน แยกตามผลลัพธ์ด้านล่าง ระบบนำไปรวมในหมวดใบงาน 40% โดยไม่เพิ่มคะแนน milestone, portfolio หรือจุดตรวจเป็นหมวดแยก

ผลลัพธ์หลักฐานตามงานส่งคะแนนดิบเต็ม
LLO7.1งานข้อ 110

ประเมินงานแต่ละข้อจากหลักฐานของข้อนั้นโดยตรง ไม่ตัดสิทธิ์รับคะแนนของอีกข้อเพราะ milestone หรือจุดตรวจอื่นยังไม่ผ่าน งานที่ยังทำไม่ได้ให้บันทึกปัญหาและหลักฐานที่มีตามจริง

ใช้เกณฑ์ความถูกต้อง เหตุผล และหลักฐานที่ระบุในงานแต่ละข้อประกอบการประเมินราย LLO งานเตรียมพื้นและ Learning Log ที่ไม่มีรหัส LLO ใช้ให้ข้อเสนอแนะ ไม่เพิ่มคะแนนดิบให้ใบงาน

บทนำ

บทที่ 6 บรรจุแอปของทีมลงใน image แล้ว แต่ระบบจริงแทบไม่มีระบบใดประกอบด้วยบริการเดียว แอปเว็บส่วนใหญ่ต้องมีฐานข้อมูล และในบทต่อไปจะมี reverse proxy และระบบเฝ้าระวังเพิ่มเข้ามา บทนี้ใช้ Docker Compose อธิบายบริการหลายตัวไว้ในไฟล์เดียว คือ compose.yaml แล้วสั่งสร้าง เริ่ม และหยุดทั้งระบบด้วยคำสั่งเดียว เนื้อหาครอบคลุมโครงสร้างของไฟล์ network ที่ทำให้บริการเรียกกันด้วยชื่อ volume ที่ทำให้ข้อมูลของฐานข้อมูลอยู่รอดเมื่อ container ถูกลบ healthcheck ที่บอกว่าบริการพร้อมใช้งานจริง และการแยกรหัสผ่านออกจากไฟล์ที่อยู่ใน Git

ความสำคัญ: compose.yaml คือคำอธิบายระบบในรูปแบบโค้ดชิ้นแรกของทีม ผู้ที่ clone repository ไปสามารถสร้างระบบทั้งชุดได้เหมือนกันโดยไม่ต้องจำลำดับคำสั่ง ไฟล์นี้จะถูกใช้ต่อเป็นสิ่งที่เครื่องปลายทางรันในบทที่ 8 เป็นที่ติดตั้ง Nginx ในบทที่ 9 และเป็นที่รัน Prometheus และ Grafana ในบทที่ 11 ส่วนสถานะ healthy จะกลายเป็นเงื่อนไขของ deploy gate ในบทที่ 11

ความเชื่อมโยงกับบทอื่น: บทนี้ใช้ image และ Dockerfile แบบ multi-stage จากบทที่ 6 แนวคิด port และ process จากบทที่ 1 และ .gitignore จากบทที่ 2 ความรู้เรื่องฐานข้อมูลเชิงสัมพันธ์และคำสั่ง SQL พื้นฐานมาจากรายวิชา 10306232 ผลผลิตของบทนี้ถูกใช้ต่อตามตารางต่อไปนี้

สิ่งที่ได้จากบทนี้ใช้ต่อในบท
compose.yaml (แอป + ฐานข้อมูล)8 (เครื่องปลายทางรันระบบด้วย Compose) · 9 (เพิ่มบริการ Nginx) · 11 (เพิ่ม Prometheus และ Grafana)
healthcheck และ docker compose up --wait11 (deploy gate) · 12 (ตรวจผลหลัง rollback)
.env.example และ .env ที่ไม่อยู่ใน Git9 (จุดตรวจความลับครั้งที่ 2) · 10 (ย้ายความลับไป GitHub Secrets)

ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์

ความรู้พื้นฐาน: การ build และรัน container จากบทที่ 6 การทำงานผ่าน pull request ที่ต้องผ่าน CI จากบทที่ 5 และคำสั่ง SQL พื้นฐาน (CREATE TABLE, INSERT, SELECT) จากรายวิชาฐานข้อมูล

เครื่องมือ: เครื่อง Ubuntu ที่ติดตั้ง Docker Engine และ Docker Compose plugin ตรวจได้ด้วย docker compose version (คำสั่งรุ่นปัจจุบันเขียนว่า docker compose มีช่องว่าง ไม่ใช่ docker-compose) และโฟลเดอร์ ~/devops-app ที่ตรงกับ main ล่าสุด ชื่อโฟลเดอร์มีผลต่อชื่อที่ Compose ตั้งให้ network, volume และ container ตัวอย่างทั้งหมดในบทนี้จึงใช้โฟลเดอร์ชื่อ devops-app

รุ่นซอฟต์แวร์ที่ใช้ทดสอบ:

ซอฟต์แวร์รุ่นที่ทดสอบ
Docker Engine29.8.2 ทดสอบเมื่อ 19 ต.ค. 2569
Docker Composev5.5.1
ฐานข้อมูลimage จำลองชื่อ postgres:16 ที่ผู้เขียนสร้างจาก PostgreSQL 16.15 ของ Ubuntu 24.04 ไม่ใช่ image ทางการจาก Docker Hub
base image ของแอปimage จำลอง node:22-slim เดียวกับบทที่ 6
แอปdevops-app เวอร์ชัน 0.2.0 พร้อม Dockerfile แบบ multi-stage จากบทที่ 6
⚠️ข้อจำกัดของการทดสอบในบทนี้

สภาพแวดล้อมที่ใช้เขียนบทความดึง image จาก Docker Hub ไม่ได้ ผู้เขียนจึงสร้าง image จำลองชื่อ postgres:16 จากโปรแกรม PostgreSQL 16.15 ที่ติดตั้งในเครื่อง พร้อม entrypoint ที่เขียนขึ้นเองให้อ่านตัวแปร POSTGRES_USER, POSTGRES_PASSWORD และ POSTGRES_DB และสร้างฐานข้อมูลเฉพาะเมื่อโฟลเดอร์ข้อมูลว่าง ตามพฤติกรรมที่ image ทางการประกาศไว้ คำสั่งของ Compose, network, volume, healthcheck, pg_isready และ psql ในบทนี้ทำงานจริงทั้งหมด แต่ข้อความใน log ของบริการ db ระหว่างเริ่มต้นครั้งแรกจะต่างจาก image ทางการ และพฤติกรรมเฉพาะของ entrypoint ทางการต้องยืนยันอีกครั้งในห้องปฏิบัติการ

ค่าใช้จ่ายและข้อมูลลับ: Docker Compose ใช้ได้โดยไม่มีค่าใช้จ่าย บทนี้เป็นบทแรกที่ระบบมีรหัสผ่าน (รหัสผ่านของผู้ใช้ฐานข้อมูล) กติกาของบทนี้คือ รหัสผ่านจริงต้องไม่ปรากฏใน compose.yaml หรือไฟล์ใดที่ commit เข้า Git ตั้งแต่ commit แรก ไฟล์ที่ commit ได้มีเพียง .env.example ซึ่งเก็บชื่อตัวแปรและค่าตัวอย่างที่ไม่ใช่รหัสผ่านจริง ส่วนค่าจริงอยู่ในไฟล์ .env บนเครื่องซึ่งถูก .gitignore ของบทที่ 2 และ .dockerignore ของบทที่ 6 กันไว้แล้ว ประวัติ commit จะถูกค้นหารหัสผ่านในบทที่ 9 รหัสผ่านที่หลุดเข้า commit ใด ๆ แม้ภายหลังลบออกแล้วจะยังถูกพบ

7.1 แอปหลายบริการและบทบาทของ Docker Compose

ระบบซอฟต์แวร์สมัยใหม่มักแบ่งเป็นบริการ (service) หลายตัวที่แต่ละตัวทำหน้าที่เดียว เช่น บริการแอปที่ตอบ HTTP บริการฐานข้อมูลที่เก็บข้อมูล และบริการ reverse proxy ที่รับการเชื่อมต่อจากภายนอก การแยกเช่นนี้ทำให้แต่ละบริการใช้ image ที่เหมาะกับตนเอง อัปเดตแยกกันได้ และเมื่อบริการหนึ่งล้มก็หาสาเหตุได้ง่ายขึ้น

หากรันระบบเช่นนี้ด้วย docker run ทีละคำสั่ง ทีมต้องจำทั้งลำดับการสร้าง network การสร้าง volume ชื่อ container พอร์ต และตัวแปรสภาพแวดล้อมของแต่ละบริการ ความรู้เหล่านี้อยู่ในหัวของคนที่ตั้งระบบครั้งแรก ไม่ได้อยู่ใน repository Docker Compose แก้ปัญหานี้ด้วยการให้ทีมเขียนคำอธิบายระบบทั้งชุดลงในไฟล์ YAML ไฟล์เดียว แล้วใช้คำสั่ง docker compose สร้างและจัดการทุกบริการตามไฟล์นั้น

ระดับที่ 1: Compose ไม่ได้แทนที่ Docker Engine แต่เป็นเครื่องมือที่อ่านไฟล์ compose.yaml แล้วสั่ง Docker Engine ให้สร้าง network, volume และ container ให้ตามที่เขียนไว้ ทุกสิ่งที่ Compose สร้างจึงเห็นได้ด้วยคำสั่ง docker ps, docker network ls และ docker volume ls จากบทที่ 6

ระดับที่ 2: คำสั่งเดียว docker compose up -d แทนคำสั่ง docker network create, docker volume create และ docker run หลายคำสั่ง และ docker compose down ลบ container และ network ที่สร้างขึ้นทั้งหมดในครั้งเดียว เมื่อสมาชิกคนใดแก้ไฟล์ การเปลี่ยนแปลงผ่าน pull request และ review ได้เหมือนโค้ด

ระดับที่ 3: Compose ออกแบบมาสำหรับระบบบนเครื่องเดียว เช่น เครื่องผู้พัฒนา เครื่องทดสอบ หรือเครื่องปลายทางขนาดเล็กอย่างเครื่องของทีมในบทที่ 8 เมื่อระบบต้องกระจายบนหลายเครื่อง ต้องรอดได้เมื่อเครื่องหนึ่งดับ หรือต้องเพิ่มจำนวนสำเนาตามภาระงาน จึงต้องใช้ระบบจัดการ container อย่าง Kubernetes ซึ่งเป็นเนื้อหาของบทที่ 14–15 แนวคิด service, network, volume และ healthcheck ในบทนี้มีสิ่งที่เทียบเคียงได้ใน Kubernetes ทุกข้อ

คำถามตรวจความเข้าใจ 7.1: สมาชิกใหม่ของทีม clone repository แล้วต้องรันแอปพร้อมฐานข้อมูล เปรียบเทียบสิ่งที่สมาชิกใหม่ต้องรู้ในกรณีที่ทีมใช้ docker run หลายคำสั่ง กับกรณีที่ทีมมี compose.yaml

7.2 โครงสร้างของ compose.yaml และวงจรคำสั่งพื้นฐาน

ไฟล์ Compose ของบทนี้ชื่อ compose.yaml ซึ่งเป็นชื่อที่ Compose ค้นหาเป็นลำดับแรกในโฟลเดอร์ปัจจุบัน ส่วนบนสุดของไฟล์มีคีย์หลักสองคีย์ที่บทนี้ใช้ ได้แก่ services ซึ่งรวมบริการทั้งหมด และ volumes ซึ่งประกาศ named volume ที่บริการใช้ร่วมกัน ตัวอย่างต่อไปนี้เป็นไฟล์ขั้นแรกของ Lab A

YAML
services:
  app:
    build: .
    image: devops-app:0.2.0
    ports:
      - "8080:3000"
    init: true
คีย์ความหมายเทียบกับคำสั่งในบทที่ 6
appชื่อบริการ ใช้อ้างในคำสั่ง Compose และเป็นชื่อที่บริการอื่นใช้เรียก—
build: .build image จาก Dockerfile ในโฟลเดอร์ปัจจุบันdocker build .
image: devops-app:0.2.0ชื่อและ tag ของ image ที่ build ได้ หรือ image ที่จะใช้-t devops-app:0.2.0
portsmap พอร์ตของเครื่องไปยังพอร์ตใน container-p 8080:3000
init: trueให้มี init process จัดการสัญญาณ--init

YAML ใช้การย่อหน้าด้วยช่องว่างแสดงโครงสร้าง ห้ามใช้ tab และระดับการย่อหน้าต้องตรงกันทั้งไฟล์ ค่าพอร์ตควรใส่เครื่องหมายคำพูด เพราะค่าที่มีเครื่องหมาย : บางรูปแบบอาจถูกตีความเป็นตัวเลขฐาน 60 ได้ใน YAML บางรุ่น

ชื่อ container, network และ volume ที่ Compose สร้างขึ้นต้นด้วยชื่อ project ซึ่งค่าเริ่มต้นคือชื่อโฟลเดอร์ที่มีไฟล์ Compose ผลการทดสอบในโฟลเดอร์ devops-app ได้ container ชื่อ devops-app-app-1 และ network ชื่อ devops-app_default

ระดับที่ 1: วงจรคำสั่งพื้นฐานมีห้าคำสั่ง

คำสั่งหน้าที่
docker compose up -dสร้างสิ่งที่ยังไม่มีแล้วเริ่มทุกบริการเบื้องหลัง
docker compose psแสดงสถานะของบริการใน project นี้
docker compose logs บริการแสดง log ของบริการ
docker compose exec บริการ คำสั่งรันคำสั่งใน container ของบริการที่กำลังทำงาน
docker compose downหยุดและลบ container และ network ของ project (ไม่ลบ named volume)

ระดับที่ 2: เมื่อบริการมีทั้ง build และ image คำสั่ง docker compose up จะ build เฉพาะเมื่อยังไม่มี image ชื่อนั้นในเครื่อง ผลการทดสอบในบทนี้แสดงว่าหลังแก้ข้อความใน src/app.js แล้วสั่ง docker compose up -d แอปยังตอบข้อความเดิม และต้องสั่ง docker compose up -d --build จึงได้ข้อความใหม่ ผู้ที่ลืมข้อนี้จะเข้าใจผิดว่าการแก้โค้ดไม่มีผล

ระดับที่ 3: คำสั่ง docker compose config อ่านไฟล์ แทนค่าตัวแปร แล้วพิมพ์ไฟล์ฉบับสมบูรณ์ที่ Compose จะใช้จริง และแจ้ง error เมื่อโครงสร้างผิด ตัวเลือก -q ให้ตรวจอย่างเดียวโดยไม่พิมพ์ จึงเหมาะกับการตรวจก่อนรันหรือการใส่ไว้ใน CI แต่ต้องระวังว่าผลแบบเต็มจะแสดงรหัสผ่านที่แทนค่าแล้ว (หัวข้อ 7.6)

คำถามตรวจความเข้าใจ 7.2: สมาชิกคนหนึ่งแก้ endpoint ใน src/app.js แล้วสั่ง docker compose down ตามด้วย docker compose up -d แต่ endpoint ใหม่ยังไม่ทำงาน อธิบายสาเหตุและคำสั่งที่ถูกต้อง

7.3 network และการเรียกกันด้วยชื่อบริการ

เอกสารทางการของ Docker อธิบายว่า Compose สร้าง network เดียวให้ project โดยอัตโนมัติ ทุกบริการเข้าร่วม network นี้ และเรียกกันได้ด้วยชื่อบริการ (Docker Docs: Networking in Compose) เมื่อแอปต้องเชื่อมต่อฐานข้อมูลในบริการชื่อ db ค่า host ที่ถูกต้องจึงเป็น db ไม่ใช่ที่อยู่ IP ซึ่งเปลี่ยนได้ทุกครั้งที่สร้าง container ใหม่

ผลการทดสอบจาก Lab B เมื่อรันคำสั่ง Node.js ภายใน container ของแอปเพื่อค้นชื่อและเชื่อมต่อพอร์ตของฐานข้อมูล

TEXT
db -> 172.18.0.3
connected to db:5432
connect ECONNREFUSED 127.0.0.1:5432

ระดับที่ 1: ชื่อ db แปลงเป็นที่อยู่ IP ของ container ฐานข้อมูลได้ เพราะ Compose ลงทะเบียนชื่อบริการไว้กับ DNS ภายในของ network ส่วน localhost ใน container ของแอปหมายถึงตัว container ของแอปเอง ซึ่งไม่มีฐานข้อมูลฟังพอร์ต 5432 อยู่ จึงได้ ECONNREFUSED

ระดับที่ 2: บริการที่อยู่ใน network เดียวกันติดต่อกันด้วยพอร์ตภายใน container (container port) โดยไม่ต้องประกาศ ports บริการ db ในบทนี้จึงไม่มี ports เลย เอกสารทางการแยกให้เห็นว่า host port ใช้สำหรับการเข้าถึงจากนอก network เท่านั้น (Docker Docs: Networking in Compose) ผล docker compose ps จึงแสดงพอร์ตของ db เป็น 5432/tcp โดยไม่มี 0.0.0.0: นำหน้า

ระดับที่ 3: การไม่เปิดพอร์ตฐานข้อมูลออกสู่เครื่องเป็นการลดพื้นที่การโจมตี (attack surface) เครื่องอื่นในเครือข่ายห้องปฏิบัติการเชื่อมต่อฐานข้อมูลของทีมไม่ได้ ผู้ดูแลยังเข้าฐานข้อมูลได้ผ่าน docker compose exec db psql หลักเดียวกันจะใช้กับบริการแอปในบทที่ 9 เมื่อ Nginx เป็นบริการเดียวที่รับการเชื่อมต่อจากภายนอก

คำถามตรวจความเข้าใจ 7.3: ทีมหนึ่งตั้งค่าให้แอปเชื่อมต่อฐานข้อมูลที่ localhost:5432 และทดสอบผ่านเมื่อรันแอปด้วย npm start บนเครื่องที่ติดตั้ง PostgreSQL ไว้ แต่ล้มเมื่อรันด้วย Compose อธิบายสาเหตุ และระบุค่าที่ถูกต้องเมื่อรันด้วย Compose

7.4 volume และข้อมูลที่ต้องคงอยู่

บทที่ 6 แสดงว่าไฟล์ที่ container เขียนอยู่ใน writable layer ซึ่งหายไปเมื่อลบ container แนวคิดนี้ดีสำหรับแอปที่ไม่เก็บสถานะ แต่เป็นปัญหาสำหรับฐานข้อมูล เพราะ docker compose down ลบ container ทุกครั้ง Docker แก้ปัญหานี้ด้วย volume ซึ่งเอกสารทางการอธิบายว่าเป็นที่เก็บข้อมูลถาวรที่ Docker จัดการ และคงอยู่แม้ลบ container ที่ใช้มันไปแล้ว จนกว่าจะลบ volume เอง (Docker Docs: Volumes)

YAML
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data
 
volumes:
  db-data:

บรรทัด db-data:/var/lib/postgresql/data หมายถึงนำ volume ชื่อ db-data มาต่อเข้าที่โฟลเดอร์ข้อมูลของ PostgreSQL ใน container และคีย์ volumes ระดับบนสุดประกาศว่า db-data เป็น named volume ของ project ซึ่ง Compose สร้างให้ในชื่อ devops-app_db-data

ระดับที่ 1: ผลการทดสอบในบทนี้ยืนยันความต่างของสองกรณี เมื่อไม่มี named volume ตาราง notes ที่สร้างไว้หายไปหลัง docker compose down และ up (ERROR: relation "notes" does not exist) เมื่อมี named volume แถวเดิมยังอยู่ และ log ของบริการ db แสดงว่าข้ามการสร้างฐานข้อมูลใหม่เพราะพบข้อมูลเดิม

ระดับที่ 2: docker compose down ไม่ลบ named volume แต่ docker compose down -v ลบด้วย ตัวเลือก -v จึงเป็นคำสั่งที่ทำลายข้อมูล ใช้เมื่อตั้งใจเริ่มฐานข้อมูลใหม่ทั้งหมดเท่านั้น ระหว่างการทดสอบยังพบว่า เมื่อไม่ได้ประกาศ named volume ไว้ แต่ image ฐานข้อมูลประกาศโฟลเดอร์ข้อมูลเป็น volume ไว้ใน image เอง Docker จะสร้าง volume ไม่มีชื่อ (anonymous volume) ชื่อเป็นเลขฐานสิบหกยาว ให้ทุกครั้งที่สร้าง container ใหม่ volume เหล่านี้สะสมค้างในเครื่องโดยไม่มี container ใดใช้ และข้อมูลใน volume เก่าไม่ถูกนำกลับมาใช้

ระดับที่ 3: volume ทำให้ข้อมูลอยู่รอดเมื่อ container ถูกลบ แต่ไม่ใช่การสำรองข้อมูล (backup) ข้อมูลยังอยู่บนดิสก์ของเครื่องเดียว หากเครื่องเสียหรือมีคนสั่ง down -v ข้อมูลหายทั้งหมด ระบบจริงต้องมีการสำรองข้อมูลแยกต่างหาก นอกจากนี้ image ฐานข้อมูลส่วนใหญ่ใช้ค่าในตัวแปรสภาพแวดล้อมเฉพาะตอนสร้างฐานข้อมูลครั้งแรก เมื่อ volume มีข้อมูลแล้ว การเปลี่ยน POSTGRES_PASSWORD ใน .env จะไม่เปลี่ยนรหัสผ่านในฐานข้อมูล ผลการทดสอบด้วย image จำลองแสดงข้อความ password authentication failed for user "app" เมื่อใช้รหัสผ่านใหม่

คำถามตรวจความเข้าใจ 7.4: สมาชิกคนหนึ่งต้องการ "ล้างระบบให้สะอาด" ก่อนสาธิต จึงสั่ง docker compose down -v อธิบายผลที่เกิดกับข้อมูลในฐานข้อมูล และเสนอคำสั่งที่หยุดระบบโดยไม่ทำลายข้อมูล

7.5 healthcheck และลำดับการเริ่มบริการ

สถานะ Up ใน docker compose ps บอกเพียงว่า process หลักของ container ยังทำงานอยู่ ไม่ได้บอกว่าบริการพร้อมรับงาน ฐานข้อมูลอาจใช้เวลาหลายวินาทีในการเตรียมไฟล์ก่อนรับการเชื่อมต่อ และแอปที่ค้างอยู่ก็ยังมีสถานะ Up ได้ healthcheck คือคำสั่งที่ Docker รันใน container เป็นระยะเพื่อตรวจว่าบริการพร้อม ถ้าคำสั่งจบด้วย exit code 0 ถือว่าผ่าน

YAML
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
คีย์ความหมาย
testคำสั่งตรวจ CMD-SHELL รันผ่าน shell ส่วน CMD รันโปรแกรมโดยตรง
intervalระยะห่างระหว่างการตรวจแต่ละครั้ง
timeoutเวลาสูงสุดที่รอคำสั่งตรวจแต่ละครั้ง
retriesจำนวนครั้งที่ตรวจไม่ผ่านติดกันก่อนถือว่า unhealthy
start_periodช่วงเริ่มต้นที่การตรวจไม่ผ่านยังไม่นับรวมใน retries

เอกสารทางการอธิบายว่า depends_on โดยปกติรอเพียงให้ container ของบริการที่พึ่งพาเริ่มรัน ไม่ได้รอให้พร้อม หากต้องการรอให้พร้อมต้องใช้ condition: service_healthy คู่กับ healthcheck และตัวอย่างในเอกสารใช้ PostgreSQL กับ pg_isready เช่นเดียวกับบทนี้ (Docker Docs: Control startup order in Compose)

YAML
  app:
    depends_on:
      db:
        condition: service_healthy

ระดับที่ 1: ผลการทดสอบใน Lab D แสดงลำดับที่ Compose ทำเมื่อมีเงื่อนไขนี้ คือเริ่ม db แล้วแสดง Waiting จนกระทั่ง db เป็น Healthy จึงเริ่ม app ผล docker compose ps แสดงสถานะเป็น Up 5 seconds (healthy)

ระดับที่ 2: บริการแอปตรวจสุขภาพได้ด้วยการเรียก /health ของตนเอง image ของแอปไม่มี curl จึงใช้ fetch ของ Node.js แทน ผลการทดสอบเมื่อจงใจตั้งพอร์ตในคำสั่งตรวจผิด แสดงสถานะ Up 12 seconds (unhealthy) และ container ยังทำงานต่อ Docker Compose ไม่ได้ลบหรือเริ่ม container ใหม่ให้เมื่อ unhealthy สถานะนี้มีไว้ให้คนหรือระบบอื่นนำไปตัดสินใจ

ระดับที่ 3: ตัวเลือก docker compose up -d --wait รอจนทุกบริการ healthy แล้วจบด้วย exit code 0 แต่ถ้ามีบริการ unhealthy จะจบด้วย exit code 1 และข้อความ container devops-app-app-1 is unhealthy (ทดสอบแล้ว) พฤติกรรมนี้ทำให้สคริปต์หรือ workflow ใช้ผลของคำสั่งเป็นเงื่อนไขว่า deploy สำเร็จหรือไม่ได้ ซึ่งจะใช้เป็นพื้นฐานของ deploy gate ในบทที่ 11 ข้อควรระวังคือ pg_isready ตรวจเพียงว่าเซิร์ฟเวอร์รับการเชื่อมต่อ ไม่ได้ตรวจว่ารหัสผ่านถูกหรือตารางที่แอปต้องใช้มีอยู่ healthcheck จึงบอกได้ว่า "พร้อมรับการเชื่อมต่อ" ไม่ใช่ "ทำงานถูกต้องทุกส่วน"

คำถามตรวจความเข้าใจ 7.5: compose.yaml ของทีมหนึ่งมี depends_on: [db] แบบรายการธรรมดา และแอปล้มเป็นบางครั้งตอนเริ่มระบบด้วยข้อความว่าเชื่อมต่อฐานข้อมูลไม่ได้ อธิบายสาเหตุและวิธีแก้

7.6 ตัวแปรสภาพแวดล้อมและไฟล์ .env

image postgres รับค่าตั้งผ่านตัวแปรสภาพแวดล้อม เช่น ชื่อผู้ใช้ รหัสผ่าน และชื่อฐานข้อมูล ค่าที่ไม่ลับ เช่น POSTGRES_USER: app เขียนลงใน compose.yaml ได้ แต่รหัสผ่านต้องไม่อยู่ในไฟล์ที่ commit Compose รองรับการแทนค่าตัวแปร (variable interpolation) ในรูปแบบ ${ชื่อตัวแปร} โดยอ่านค่าจากตัวแปรใน shell หรือจากไฟล์ .env ในโฟลเดอร์เดียวกับ compose.yaml

YAML
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is not set}

รูปแบบ ${ตัวแปร:?ข้อความ} กำหนดว่าตัวแปรนี้จำเป็น ถ้าไม่มีค่า Compose จะหยุดพร้อมข้อความ error แทนที่จะส่งค่าว่างให้ฐานข้อมูล ผลการทดสอบเมื่อไม่ได้ตั้งค่า

TEXT
error while interpolating services.db.environment.POSTGRES_PASSWORD: required variable POSTGRES_PASSWORD is missing a value: POSTGRES_PASSWORD is not set

แนวปฏิบัติของทีมในบทนี้แบ่งไฟล์เป็นสองไฟล์ คือ .env.example ที่ commit เข้า Git เพื่อบอกว่าระบบต้องการตัวแปรใดบ้างพร้อมค่าตัวอย่าง และ .env ที่แต่ละคนคัดลอกจาก .env.example แล้วใส่ค่าจริง ซึ่ง .gitignore กันไว้ไม่ให้ commit

ระดับที่ 1: ตรวจได้ว่า Git จะไม่รับ .env ด้วยคำสั่ง git check-ignore -v .env ซึ่งตอบกลับว่ากฎใดในไฟล์ใดกันไฟล์นี้ไว้ ผลการทดสอบได้ .gitignore:2:.env .env คือบรรทัดที่ 2 ของ .gitignore ที่เพิ่มไว้ในบทที่ 2

ระดับที่ 2: ค่าจากตัวแปรใน shell มีลำดับก่อนค่าในไฟล์ .env ผลการทดสอบเมื่อไฟล์ .env ตั้ง POSTGRES_PASSWORD=dev-only-pass แต่สั่ง POSTGRES_PASSWORD=from-shell docker compose config ได้ค่า from-shell ในผล ลำดับนี้ทำให้เครื่องปลายทางหรือ workflow ส่งค่าผ่านตัวแปรสภาพแวดล้อมได้โดยไม่ต้องมีไฟล์ .env แต่ก็เป็นสาเหตุของความสับสนเมื่อเคย export ค่าเก่าค้างไว้ใน terminal

ระดับที่ 3: การย้ายรหัสผ่านไปไว้ใน .env ป้องกันไม่ให้รหัสผ่านเข้า Git แต่ไม่ได้ทำให้รหัสผ่านเป็นความลับอย่างสมบูรณ์ ค่าที่ส่งผ่านตัวแปรสภาพแวดล้อมยังเห็นได้จาก docker compose config และ docker inspect ของ container ผู้ที่เข้าถึง Docker บนเครื่องได้จึงอ่านรหัสผ่านได้ บทที่ 9 จะอภิปรายข้อจำกัดนี้และกลไก secrets ส่วนบทที่ 10 จะย้ายความลับที่ workflow ใช้ไปเก็บใน GitHub Secrets

คำถามตรวจความเข้าใจ 7.6: สมาชิกคนหนึ่งแนบผล docker compose config ทั้งหมดลงในคำอธิบาย pull request เพื่อให้ผู้ตรวจทานเห็นไฟล์ฉบับสมบูรณ์ ระบุปัญหาที่เกิดขึ้นและเสนอวิธีแสดงผลการตรวจไฟล์ที่ปลอดภัยกว่า

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

7.7 แยกพอร์ตของเครื่องกับพอร์ตในเครือข่ายบริการ

การเรียกบริการใน Compose ต้องเริ่มจากระบุว่าผู้เรียกอยู่ที่ใด สมมติว่า app และ db อยู่ในเครือข่าย Compose เดียวกัน แอปควรใช้ชื่อบริการ db และพอร์ตภายในของ PostgreSQL ส่วนผู้ใช้บนเครื่อง host อาจเข้าผ่านพอร์ตที่ publish ไว้ การนำพอร์ตฝั่ง host ไปใส่ในการเชื่อมต่อระหว่าง container เป็นความสับสนของขอบเขตเครือข่าย

ตัวอย่างที่ไม่ถูกต้องคือกำหนดฐานข้อมูลเป็น localhost:5432 ภายใน container ของแอป โดยคิดว่า localhost หมายถึงเครื่องเดียวกับที่รัน Docker ในบริบทนี้ localhost ชี้กลับไปยัง network namespace ของแอปเอง ถ้าไม่มีฐานข้อมูลอยู่ใน container นั้น การเชื่อมต่อจะล้ม แม้ฐานข้อมูลของอีกบริการยังทำงานอยู่ การแก้สำหรับโครงสร้างของบทนี้คือใช้ชื่อบริการที่ resolve ได้ในเครือข่ายร่วมกัน แล้วตรวจพอร์ตและสิทธิ์เชื่อมต่อต่อไป

อีกตัวอย่างคือ publish ฐานข้อมูลเป็นพอร์ต host 15432 แล้วให้แอปเชื่อม db:15432 ทั้งที่ฐานข้อมูลยังฟังที่ 5432 การ publish เป็นการจับคู่ช่องทางจากภายนอก ไม่ได้เปลี่ยนพอร์ตที่โปรแกรมภายในฟัง การตรวจควรวาดลูกศรสองเส้นแยกกัน ได้แก่ host → published port → database port และ app → service name → database port

กรณีที่ถูกต้องแต่ยังเชื่อมไม่ได้อาจเกิดจากชื่อผู้ใช้ รหัสผ่าน หรือฐานข้อมูลไม่ตรง การที่ชื่อ db resolve ได้เพียงพิสูจน์ชั้นชื่อและเครือข่ายบางส่วน ยังไม่พิสูจน์การยืนยันตัวตน การสืบเหตุควรแยก DNS, การเชื่อมต่อ TCP, การยืนยันตัวตน และคำสั่งฐานข้อมูล เพื่อไม่ให้แก้เครือข่ายเมื่อปัญหาอยู่ที่ข้อมูลรับรอง

7.8 เริ่มแล้ว พร้อมแล้ว และยังพร้อมอยู่เป็นคนละเงื่อนไข

Compose เริ่มบริการตาม dependency ได้ แต่สถานะ running ไม่ได้แปลว่าบริการพร้อมรับงาน ฐานข้อมูลอาจเริ่มกระบวนการแล้วแต่ยังเตรียมข้อมูลไม่เสร็จ เอกสาร Docker จึงแยก service_started, service_healthy และ service_completed_successfully สำหรับเงื่อนไขที่ต่างกัน (Docker Docs: Control startup order)

ตัวอย่างที่ผิดคือใช้ depends_on แบบสั้นแล้วสรุปว่าแอปจะไม่พบ connection refused ในช่วงเริ่มระบบ การแก้ในโจทย์นี้คือกำหนด healthcheck ที่เหมาะสมให้ฐานข้อมูล และให้ dependency รอ service_healthy พร้อมออกแบบให้แอปรับมือการหลุดเชื่อมต่อภายหลัง เพราะการรอพร้อมครั้งแรกไม่ใช่การรับประกันความพร้อมตลอดอายุระบบ

ตัวอย่างที่ผิดอีกแบบคือ healthcheck ของแอปใช้เพียง echo ok คำสั่งนี้สำเร็จแม้ HTTP server ไม่ตอบ จึงตรวจเพียงว่ารันคำสั่ง echo ได้ ตัวตรวจควรสัมพันธ์กับสิ่งที่ต้องการรับรอง เช่น endpoint ที่ให้ข้อมูลความพร้อมตามสัญญา และต้องมีเครื่องมือเรียก endpoint อยู่ใน image จริงด้วย ถ้าตัวตรวจหา curl ไม่พบ ความล้มเหลวเป็นของตัวตรวจ ไม่ใช่ข้อพิสูจน์ว่าแอปตอบไม่ได้

เส้นเวลาสมมติช่วยอธิบายได้ชัดเจน: วินาทีที่ 0 เริ่มฐานข้อมูล วินาทีที่ 3 กระบวนการ running วินาทีที่ 12 ตรวจพร้อมผ่าน วินาทีที่ 13 เริ่มแอป และนาทีที่ 5 ฐานข้อมูลหยุด การตั้ง dependency ช่วยจัดการช่วง 0–13 วินาที แต่แอปยังต้องมีพฤติกรรมเมื่อฐานข้อมูลหายหลังนาทีที่ 5 เช่น retry ที่มีขอบเขตหรือรายงานสถานะไม่พร้อม การใส่ sleep คงที่แทน readiness เพียงย้ายความเสี่ยงไปยังเครื่องที่ช้าหรือเร็วต่างกัน

ควรแยก restart: true ภายใต้ dependency ออกจาก restart policy ของบริการด้วย เอกสาร Docker อธิบายว่าค่าภายใต้ dependency เกี่ยวข้องกับการ restart จากการดำเนินการ Compose ที่ชัดเจน ไม่ใช่ระบบตรวจสุขภาพที่สั่ง restart ทุกครั้งที่ฐานข้อมูล unhealthy การอ่านตำแหน่ง YAML จึงสำคัญพอ ๆ กับชื่อ key

7.9 volume ไม่ใช่สำเนาสำรอง

volume ทำให้ข้อมูลคงอยู่แยกจาก container แต่ยังมีวงจรชีวิตของตนเอง การเก็บ PostgreSQL ใน named volume จึงช่วยให้สร้าง container ใหม่โดยข้อมูลเดิมยังอยู่ หาก mount ถูกตำแหน่งและใช้ volume เดิม แต่ไม่ได้ป้องกันการลบข้อมูลโดยคำสั่งผิด ความเสียหายของดิสก์ หรือการแก้ข้อมูลที่ไม่ต้องการ (Docker Docs: Volumes)

ตัวอย่างที่ผิดคือสรุปว่า “มี volume แล้วไม่ต้อง backup” หากแอปส่งคำสั่งลบข้อมูลอย่างถูกสิทธิ์ volume จะเก็บผลการลบนั้นเช่นกัน การสำรองต้องมีสำเนาที่กู้คืนได้ กระบวนการคืนข้อมูล และหลักฐานทดสอบการคืน การตรวจเพียงว่าไฟล์ backup ถูกสร้างยังไม่ยืนยันว่าเปิดใช้งานได้

อีกตัวอย่างคือใช้คำสั่งที่ลบ volume ระหว่างเก็บกวาดโดยไม่แยกฐานข้อมูลทดลองออกจากข้อมูลที่ต้องเก็บ คำสั่ง docker compose down -v มีผลต่างจาก down ปกติ จึงไม่ควรคัดลอกเป็นขั้นปิดงานทั่วไปโดยไม่อธิบายผล ในบทนี้การทดลองลบต้องใช้ข้อมูลสังเคราะห์และ volume ที่ระบุชัด ส่วนคำสั่งเก็บกวาดเดิมของแต่ละ Lab ต้องอ่านขอบเขตก่อนใช้

การทดลองที่ถูกต้องเปรียบเทียบสามเหตุการณ์ด้วยข้อมูลสมมติหนึ่งรายการ ได้แก่ restart container, สร้าง container ใหม่โดยใช้ named volume เดิม และสร้างสภาพแวดล้อมใหม่ที่ใช้ volume คนละชื่อ บันทึกว่ารายการนั้นปรากฏหรือไม่ในแต่ละกรณี พร้อมชื่อ volume ที่ mount อยู่ วิธีนี้แสดงบทบาทของตัวเก็บข้อมูลได้ชัดกว่าการสั่ง restart แล้วเห็นข้อมูลยังอยู่เพียงครั้งเดียว

7.10 การแก้ปัญหาโดยไม่ทำลายสภาพตั้งต้น

เมื่อระบบไม่พร้อม ไม่ควรเริ่มด้วยลบทุกอย่างแล้วสร้างใหม่ เพราะอาจลบหลักฐานและข้อมูล ควรอ่าน docker compose config เพื่อตรวจค่าที่ Compose ตีความโดยระวังไม่เผยค่าลับ อ่าน ps เพื่อแยก running กับ health status และอ่าน log ของบริการที่เกี่ยวข้องตามลำดับเวลา จากนั้นจึงเลือกการเปลี่ยนที่ทดสอบสมมติฐานหนึ่งข้อ

ข้อมูลจำลองมีแอปขึ้นแต่ตอบ 500 และฐานข้อมูล healthy ข้อสรุปว่า “ฐานข้อมูล healthy จึงไม่ใช่ปัญหาฐานข้อมูล” กว้างเกินหลักฐาน เพราะ healthcheck อาจตรวจเพียงว่ารับการเชื่อมต่อได้ ยังไม่ตรวจ schema หรือสิทธิ์ของบัญชีแอป ควรดู error ของแอปและทดลองคำสั่งด้วยบัญชีที่แอปใช้แทนการใช้บัญชีผู้ดูแล

อีกกรณีคือค่าจาก .env เปลี่ยนแล้วแต่ container เดิมยังใช้ค่าเก่า ต้องแยกไฟล์ที่ Compose ใช้แทนค่าตอนอ่าน YAML ออกจาก environment ที่กระบวนการได้รับตอนสร้าง container การแก้ไฟล์บนดิสก์ไม่ทำให้ environment ของกระบวนการที่กำลังทำงานเปลี่ยนเอง การตรวจจึงต้องอ้างถึงค่าตั้งที่ถูกนำไปใช้จริงและวิธีสร้างใหม่ตามขอบเขตที่เหมาะสม

ผลการสืบเหตุที่ส่งต่อให้ทีมควรระบุบริการที่มีอาการ หลักฐานก่อนแก้ การเปลี่ยนเพียงจุดเดียว และผลหลังแก้ หากยังไม่ทราบสาเหตุต้องระบุว่าเป็นการบรรเทาอาการ ข้อมูลนี้จะกลายเป็น runbook และประตูตรวจหลัง deploy ในบทถัดไป

กรณีอธิบายต่อเนื่อง: ฐานข้อมูลพร้อมแต่แอปยังเข้าไม่ได้

ข้อมูลสมมติมีบริการ db healthy และบริการ app เริ่มแล้วล้มด้วยข้อความ authentication failed สมาชิกเสนอเพิ่ม depends_on และ sleep อีก 30 วินาที แต่ข้อความนี้ชี้ว่าการเชื่อมต่อไปถึงขั้นยืนยันตัวตนแล้ว ความล่าช้าของการเริ่มอาจไม่ใช่เหตุหลัก ต้องตรวจชื่อผู้ใช้ ฐานข้อมูล และแหล่งค่าลับที่แอปได้รับจริงโดยไม่พิมพ์ค่าลับลง log

ถ้า healthcheck ของฐานข้อมูลใช้บัญชีผู้ดูแล แต่แอปใช้บัญชีจำกัดสิทธิ์ ผล healthy เพียงพิสูจน์เงื่อนไขของบัญชีตัวตรวจ ไม่ยืนยันสิทธิ์ของแอป การทดสอบที่ตรงโจทย์ควรตรวจการเชื่อมต่อและคำสั่งขั้นต่ำที่แอปต้องใช้ด้วยตัวตนเดียวกันในสภาพแวดล้อมฝึกที่กำหนด

คำแนะนำที่ผิดอีกแบบคือเปลี่ยนให้แอปใช้บัญชีผู้ดูแลเพื่อให้ผ่านทันที แม้อาการอาจหายแต่เพิ่มสิทธิ์เกินบทบาทและซ่อนการกำหนดสิทธิ์ที่ไม่ครบ วิธีที่เหมาะสมคือระบุสิทธิ์ที่ต้องใช้และปรับเฉพาะสิ่งนั้น พร้อมทดสอบกรณีที่บัญชีแอปไม่ควรทำได้ด้วย เช่น งานบริหารที่นอกขอบเขต

หากค่าตั้งใน container ไม่ตรงกับไฟล์ที่เพิ่งแก้ ต้องตรวจว่าถูกสร้างจากไฟล์ใดและเมื่อใด การแก้ค่าบน host แล้ว restart กระบวนการเดิมไม่ได้มีความหมายเหมือนการสร้าง container ด้วย environment ใหม่ทุกกรณี การรายงานผลจึงควรระบุขั้นที่ทำจริงแทนคำกว้าง ๆ ว่าเริ่มใหม่แล้ว

ปฏิบัติการที่ 7 รันแอปทีมคู่กับฐานข้อมูลด้วย Compose

ปฏิบัติการนี้ประกอบด้วย Lab A–E ทุกคนรันระบบบนเครื่องของตนเอง ส่วนการเปลี่ยนแปลงที่เข้า repository แบ่งผู้รับผิดชอบดังนี้ เพื่อให้ทุกคนมี pull request ของตนเองใน milestone M6

pull requestbranchLabผู้รับผิดชอบผู้ตรวจทาน
เพิ่ม compose.yaml สำหรับบริการแอปcompose/add-appAสมาชิกคนที่ 1สมาชิกคนที่ 2
เพิ่มบริการฐานข้อมูลและ named volumecompose/add-dbB และ Cสมาชิกคนที่ 2สมาชิกคนที่ 3
เพิ่ม healthcheck และ depends_oncompose/healthcheckDสมาชิกคนที่ 3สมาชิกคนที่ 1
เพิ่ม .env.examplecompose/env-exampleEสมาชิกคนที่ 1สมาชิกคนที่ 3

pull request ทุกรายการต้องผ่าน CI จากบทที่ 5 ก่อน merge workflow CI ยังไม่ได้รัน Compose ผู้ตรวจทานจึงต้อง checkout branch แล้วรันคำสั่งตรวจที่ระบุในแต่ละ Lab บนเครื่องของตนเอง แต่ละ Lab แบ่งเป็นส่วน ฝึกตามตัวอย่าง และส่วน ตัดสินใจเอง (A.4, B.4, C.3, D.3 และ E.3)

ℹ️ผลลัพธ์อาจต่างจากตัวอย่าง

ผลลัพธ์ในปฏิบัติการนี้ได้จากการรันจริงบน Docker Engine 29.8.2 และ Docker Compose v5.5.1 โดยใช้ image จำลองตามที่ระบุในบทนำ ผล docker compose up ของ Compose รุ่นนี้พิมพ์บางบรรทัดซ้ำสองครั้งระหว่างแสดงความคืบหน้า ตัวอย่างในบทนี้ตัดบรรทัดซ้ำออก และแสดงผล docker compose ps เฉพาะคอลัมน์ที่สำคัญ ที่อยู่ IP และเวลาในผลจะต่างจากตัวอย่าง

Lab A: Compose แอปเดียว

สถานการณ์: ทีมรันแอปด้วยคำสั่ง docker run -d --name devops-app -p 8080:3000 devops-app:0.2.0 จากบทที่ 6 ทุกครั้ง และต้องการย้ายคำสั่งนี้ไปอยู่ในไฟล์ที่ commit ได้

เป้าหมาย: เขียน compose.yaml ที่มีบริการแอปบริการเดียว แล้วใช้วงจรคำสั่ง up, ps, logs และ down (พื้นฐานของ LLO 7.1)

ฝึกตามตัวอย่าง

A.1 เขียน compose.yaml (สมาชิกคนที่ 1)

หยุด container จากบทที่ 6 ที่อาจค้างอยู่ก่อน เพราะใช้พอร์ต 8080 เดียวกัน

BASH
docker rm -f devops-app
cd ~/devops-app
git switch main
git pull
git switch -c compose/add-app
nano compose.yaml
YAML
services:
  app:
    build: .
    image: devops-app:0.2.0
    ports:
      - "8080:3000"

A.2 รันและตรวจระบบ (สมาชิกทุกคน หลังจากสมาชิกคนที่ 1 push branch แล้ว)

BASH
docker compose config -q
docker compose up -d --build
docker compose ps
curl -s http://localhost:8080/health
docker compose logs app
TEXT
 Network devops-app_default Created
 Container devops-app-app-1 Created
 Container devops-app-app-1 Started
NAME               SERVICE   STATUS         PORTS
devops-app-app-1   app       Up 2 seconds   0.0.0.0:8080->3000/tcp
{"status":"ok","version":"0.2.0"}
app-1  | listening on port 3000

docker compose config -q ไม่พิมพ์อะไรเมื่อไฟล์ถูกต้อง Compose สร้าง network devops-app_default และ container devops-app-app-1 ให้เอง ชื่อทั้งสองขึ้นต้นด้วยชื่อโฟลเดอร์ตามที่อธิบายในหัวข้อ 7.2

A.3 หยุดระบบ

BASH
docker compose down
TEXT
 Container devops-app-app-1 Stopped
 Container devops-app-app-1 Removed
 Network devops-app_default Removed

ตัดสินใจเอง A.4: ระหว่างทดสอบพบว่า docker compose down ใช้เวลาประมาณ 10 วินาทีทุกครั้ง ซึ่งเป็นอาการเดียวกับ docker stop ในบทที่ 6 ค้นจากเอกสาร Compose file reference แล้วเพิ่มคีย์ที่ให้ผลเหมือนตัวเลือกที่ใช้แก้ปัญหานี้ในบทที่ 6 ลงในบริการ app จากนั้นวัดเวลาของ docker compose down ใหม่ แล้ว commit และเปิด pull request

ผลลัพธ์ที่คาดหวัง A.4: docker compose down จบภายในไม่ถึง 1 วินาที และ docker compose config -q ยังไม่มี error

จุดตรวจ A: สมาชิกทุกคนรันระบบด้วย Compose ได้ ตอบ /health ได้ และอธิบายได้ว่า Compose สร้างอะไรให้บ้าง ผู้ตรวจทานรัน A.2 และ A.3 บนเครื่องตนเองก่อน approve

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
Bind for 0.0.0.0:8080 failed: port is already allocatedcontainer จากบทที่ 6 ยังใช้พอร์ต 8080 อยู่docker rm -f devops-app แล้ว docker compose up -d ใหม่
docker: 'compose' is not a docker commandไม่ได้ติดตั้ง Docker Compose pluginแจ้งผู้ดูแลเครื่อง
yaml: line N: found character that cannot start any tokenใช้ tab ย่อหน้าเปลี่ยน tab เป็นช่องว่าง และตั้ง editor ให้ใช้ช่องว่าง
no configuration file provided: not foundสั่งคำสั่งนอกโฟลเดอร์ที่มี compose.yamlcd ~/devops-app ก่อน

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

Lab B: เพิ่มฐานข้อมูล

สถานการณ์: ทีมต้องการฐานข้อมูล PostgreSQL ไว้รองรับข้อมูลของแอปในงานต่อยอด และต้องการตรวจว่าแอปเรียกฐานข้อมูลได้ภายใน network ของ Compose

เป้าหมาย: เพิ่มบริการ db โดยรหัสผ่านไม่อยู่ในไฟล์ ตรวจการเรียกกันด้วยชื่อบริการ และสังเกตว่าข้อมูลหายเมื่อยังไม่มี volume (LLO 7.1 ส่วน network)

ℹ️ขอบเขตของแอปตั้งต้น

แอปตั้งต้นของรายวิชายังไม่อ่านหรือเขียนฐานข้อมูล ปฏิบัติการนี้จึงตรวจการเชื่อมต่อจาก container ของแอปไปยังพอร์ตของฐานข้อมูล และตรวจข้อมูลผ่าน psql ในบริการ db โดยไม่แก้โค้ดของแอป การให้แอปบันทึกข้อมูลลงฐานข้อมูลจริงเป็นหัวข้องานต่อยอดที่เลือกได้

ฝึกตามตัวอย่าง

B.1 เพิ่มบริการ db (สมาชิกคนที่ 2 หลังจาก pull request ของ Lab A merge แล้ว)

BASH
git switch main
git pull
git switch -c compose/add-db
nano compose.yaml

เพิ่มบริการ db ต่อจากบริการ app โดยย่อหน้าให้อยู่ระดับเดียวกับ app

YAML
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is not set}
      POSTGRES_DB: app

B.2 ตั้งรหัสผ่านใน shell แล้วรันระบบ (สมาชิกทุกคน)

รหัสผ่านของขั้นนี้ตั้งเป็นตัวแปรใน terminal ชั่วคราว Lab E จะย้ายไปไว้ในไฟล์ .env

BASH
export POSTGRES_PASSWORD=dev-only-pass
docker compose up -d
docker compose ps
docker compose logs db | tail -n 3
TEXT
NAME               SERVICE   STATUS         PORTS
devops-app-app-1   app       Up 3 seconds   0.0.0.0:8080->3000/tcp
devops-app-db-1    db        Up 3 seconds   5432/tcp
db-1  | ... LOG:  listening on IPv4 address "0.0.0.0", port 5432
db-1  | ... LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db-1  | ... LOG:  database system is ready to accept connections

พอร์ตของ db แสดงเป็น 5432/tcp โดยไม่มี 0.0.0.0: เพราะไม่ได้เปิดออกสู่เครื่อง

B.3 ตรวจการเรียกกันด้วยชื่อบริการ

image ของแอปไม่มีเครื่องมือเครือข่ายอย่าง ping หรือ nc จึงใช้ Node.js ใน container ของแอปตรวจแทน

BASH
docker compose exec app node -e "require('dns').lookup('db', (e, a) => console.log('db ->', a))"
docker compose exec app node -e "require('net').connect(5432, 'db').on('connect', () => { console.log('connected to db:5432'); process.exit(0) }).on('error', e => { console.log(e.message); process.exit(1) })"
docker compose exec app node -e "require('net').connect(5432, 'localhost').on('connect', () => { console.log('ok'); process.exit(0) }).on('error', e => { console.log(e.message); process.exit(1) })"
TEXT
db -> 172.18.0.3
connected to db:5432
connect ECONNREFUSED 127.0.0.1:5432

จากนั้นสร้างตารางและเพิ่มข้อมูลด้วย psql ในบริการ db

BASH
docker compose exec db psql -U app -d app -c "CREATE TABLE notes (id serial PRIMARY KEY, body text NOT NULL);" -c "INSERT INTO notes (body) VALUES ('บันทึกแรกของทีม');" -c "SELECT * FROM notes;"
TEXT
CREATE TABLE
INSERT 0 1
 id |     body
----+--------------
  1 | บันทึกแรกของทีม
(1 row)

ตัดสินใจเอง B.4: ก่อน commit ให้ทดลองสองข้อ แล้วบันทึกผลและคำอธิบายลงในคำอธิบาย pull request (1) เปิด terminal ใหม่ที่ยังไม่ได้ export รหัสผ่าน แล้วสั่ง docker compose up -d อธิบายว่าเหตุใด Compose จึงไม่ยอมรันและเหตุใดพฤติกรรมนี้ดีกว่าการส่งค่าว่างให้ฐานข้อมูล (2) สั่ง docker compose down ตามด้วย docker compose up -d แล้ว SELECT * FROM notes; ใหม่ บันทึกผล และใช้ docker volume ls อธิบายว่าข้อมูลไปอยู่ที่ใด

ผลลัพธ์ที่คาดหวัง B.4: ข้อ (1) ได้ข้อความ error ที่มีคำว่า required variable POSTGRES_PASSWORD is missing a value ข้อ (2) ได้ ERROR: relation "notes" does not exist และอธิบายได้ว่าข้อมูลอยู่ใน volume ที่ไม่ถูกนำกลับมาใช้

จุดตรวจ B: สมาชิกทุกคนอธิบายได้ว่าเหตุใด db ใช้ได้แต่ localhost ใช้ไม่ได้ และไม่มีรหัสผ่านจริงใน compose.yaml

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
required variable POSTGRES_PASSWORD is missing a valueยังไม่ได้ export หรือเปิด terminal ใหม่export POSTGRES_PASSWORD=... ใน terminal ที่ใช้
validating .../compose.yaml: services.db additional properties 'imgae' not allowedสะกดคีย์ผิด (ตัวอย่างนี้พิมพ์ image เป็น imgae)ตรวจด้วย docker compose config -q แล้วแก้ตามบรรทัดที่แจ้ง
psql: error: ... FATAL: role "postgres" does not existสั่ง psql โดยไม่ระบุ -U appใช้ผู้ใช้ที่ตั้งใน POSTGRES_USER
ข้อความ the database system is starting upสั่ง psql เร็วเกินไปก่อนฐานข้อมูลพร้อมรอสักครู่แล้วสั่งใหม่ (Lab D แก้ปัญหานี้)

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

Lab C: volume และการทดสอบ down/up

สถานการณ์: ข้อมูลหายทุกครั้งที่ทีมหยุดระบบด้วย docker compose down ซึ่งยอมรับไม่ได้สำหรับฐานข้อมูล

เป้าหมาย: เพิ่ม named volume ให้ฐานข้อมูล และพิสูจน์ด้วยผลก่อนและหลัง down/up ว่าข้อมูลคงอยู่ (LLO 7.1 ส่วน volume และหลักฐานของ M6)

ฝึกตามตัวอย่าง

C.1 เพิ่ม named volume (สมาชิกคนที่ 2 ต่อใน branch compose/add-db)

ก่อนแก้ไฟล์ ให้หยุดระบบและลบ anonymous volume ที่ค้างจาก Lab B ซึ่งไม่มีข้อมูลที่ต้องเก็บ

BASH
docker compose down
docker volume prune -f
nano compose.yaml

เพิ่ม volumes ในบริการ db และประกาศ volume ระดับบนสุดของไฟล์

YAML
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is not set}
      POSTGRES_DB: app
    volumes:
      - db-data:/var/lib/postgresql/data
 
volumes:
  db-data:

C.2 ทดสอบ down/up (สมาชิกทุกคน)

BASH
docker compose config -q
docker compose up -d
docker compose exec db psql -U app -d app -c "CREATE TABLE notes (id serial PRIMARY KEY, body text NOT NULL);" -c "INSERT INTO notes (body) VALUES ('บันทึกแรกของทีม');"
docker compose exec db psql -U app -d app -c "SELECT * FROM notes;"
docker compose down
docker volume ls
docker compose up -d
docker compose logs db | grep -i skipping
docker compose exec db psql -U app -d app -c "SELECT * FROM notes;"
TEXT
 Volume devops-app_db-data Created
 id |     body
----+--------------
  1 | บันทึกแรกของทีม
(1 row)
 
DRIVER    VOLUME NAME
local     devops-app_db-data
db-1  | PostgreSQL Database directory appears to contain a database; Skipping initialization
 id |     body
----+--------------
  1 | บันทึกแรกของทีม
(1 row)

docker compose down ลบ container และ network แต่ volume devops-app_db-data ยังอยู่ เมื่อ up ใหม่ ฐานข้อมูลพบข้อมูลเดิมจึงข้ามการสร้างใหม่ และ SELECT ได้แถวเดิม ผลชุดนี้คือหลักฐานส่วนแรกของ M6 (ถ้าคำสั่ง psql ขึ้นข้อความว่าฐานข้อมูลยังเริ่มไม่เสร็จ ให้รอสักครู่แล้วสั่งใหม่)

ตัดสินใจเอง C.3: สมาชิกคนหนึ่งเสนอให้ทีมใช้ bind mount แบบ ./pgdata:/var/lib/postgresql/data แทน named volume เพื่อ "มองเห็นไฟล์ข้อมูลในโฟลเดอร์ของ repository" ค้นจากเอกสาร Docker Docs: Volumes แล้วตัดสินใจว่าทีมจะใช้แบบใด เขียนเหตุผลอย่างน้อย 2 ข้อลงในคำอธิบาย pull request โดยอ้างเอกสาร และระบุสิ่งที่ต้องเพิ่มใน .gitignore หากทีมเลือก bind mount

ผลลัพธ์ที่คาดหวัง C.3: คำอธิบาย pull request มีการตัดสินใจ เหตุผลอย่างน้อย 2 ข้อ และลิงก์เอกสาร Volumes

จุดตรวจ C: สมาชิกทุกคนได้ผล SELECT เดิมหลัง down/up บนเครื่องตนเอง และอธิบายความต่างของ docker compose down กับ docker compose down -v ได้ หลังผู้ตรวจทาน approve สมาชิกคนที่ 2 merge pull request ที่รวม Lab B และ C

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
service "db" refers to undefined volume db-dataลืมประกาศ volumes: ระดับบนสุดเพิ่ม volumes: และ db-data: ที่ระดับเดียวกับ services:
ข้อมูลยังหายหลัง down/upสั่ง docker compose down -v หรือพิมพ์ path ใน container ผิดใช้ down ไม่มี -v และตรวจ path ให้ตรงกับ /var/lib/postgresql/data
password authentication failed หลังเปลี่ยนรหัสผ่านฐานข้อมูลใน volume สร้างไว้ด้วยรหัสผ่านเดิมใช้รหัสผ่านเดิม หรือถ้าข้อมูลไม่สำคัญ ให้ down -v แล้วสร้างใหม่

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

Lab D: healthcheck และ depends_on

สถานการณ์: ในห้องปฏิบัติการบางครั้งคำสั่ง psql ที่สั่งทันทีหลัง up ล้มเพราะฐานข้อมูลยังเริ่มไม่เสร็จ ในอนาคตเมื่อแอปเชื่อมต่อฐานข้อมูลจริง แอปจะล้มด้วยสาเหตุเดียวกัน

เป้าหมาย: เพิ่ม healthcheck ให้ทั้งสองบริการ ให้แอปเริ่มหลังฐานข้อมูลพร้อม และใช้ --wait รอจนระบบพร้อม (LLO 7.1 ส่วน healthcheck)

ฝึกตามตัวอย่าง

D.1 เพิ่ม healthcheck ของ db และเงื่อนไขของ app (สมาชิกคนที่ 3 หลังจาก pull request ของ Lab B–C merge แล้ว)

BASH
git switch main
git pull
git switch -c compose/healthcheck
nano compose.yaml

เพิ่มในบริการ app ต่อจาก init: true

YAML
    depends_on:
      db:
        condition: service_healthy

เพิ่มในบริการ db ต่อจาก volumes

YAML
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

D.2 ดูลำดับการเริ่มบริการ (สมาชิกทุกคน)

BASH
docker compose down
docker compose up -d
docker compose ps
docker compose exec db pg_isready -U app -d app
TEXT
 Container devops-app-db-1 Started
 Container devops-app-db-1 Waiting
 Container devops-app-db-1 Healthy
 Container devops-app-app-1 Started
NAME               STATUS
devops-app-app-1   Up Less than a second
devops-app-db-1    Up 5 seconds (healthy)
/var/run/postgresql:5432 - accepting connections

Compose เริ่ม db ก่อน รอจน db เป็น Healthy แล้วจึงเริ่ม app ตามที่เอกสาร Control startup order in Compose อธิบาย ส่วน app ยังไม่มีคำว่า (healthy) เพราะยังไม่มี healthcheck

ตัดสินใจเอง D.3: เพิ่ม healthcheck ให้บริการ app โดยเรียก http://localhost:3000/health จากภายใน container ซึ่งไม่มี curl หรือ wget (ใบ้: Node.js 22 มีฟังก์ชัน fetch ในตัว และ healthcheck แบบ CMD รับคำสั่งเป็นรายการ) จากนั้นทดลองสองกรณีและบันทึกผลในคำอธิบาย pull request (1) ระบบปกติ: docker compose up -d --wait แล้ว echo $? (2) จงใจแก้พอร์ตในคำสั่งตรวจเป็น 3001 แล้วทำซ้ำ บันทึก exit code และสถานะใน docker compose ps แล้วแก้กลับก่อน commit

ผลลัพธ์ที่คาดหวัง D.3: กรณีปกติ exit code 0 และทั้งสองบริการ (healthy) กรณีพอร์ตผิด exit code 1 พร้อมข้อความ container devops-app-app-1 is unhealthy

จุดตรวจ D: docker compose ps บนเครื่องของผู้ตรวจทานแสดง (healthy) ทั้งสองบริการ ผลชุดนี้คือหลักฐานส่วนที่สองของ M6

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
dependency failed to start: container devops-app-db-1 has no healthcheck configuredใช้ service_healthy แต่ db ไม่มี healthcheckเพิ่ม healthcheck ให้ db
สถานะค้างที่ (health: starting) นานinterval หรือ start_period ยาวรอให้ครบช่วงเวลา หรือลดค่าลงสำหรับการทดสอบ
(unhealthy) ทั้งที่แอปตอบ curl จากเครื่องได้คำสั่งตรวจเรียกพอร์ตของเครื่อง (8080) แทนพอร์ตใน container (3000)คำสั่งตรวจรันใน container ต้องใช้พอร์ต 3000

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

Lab E: .env.example

สถานการณ์: สมาชิกทุกคนต้อง export POSTGRES_PASSWORD ทุกครั้งที่เปิด terminal ใหม่ และสมาชิกใหม่ไม่รู้ว่าระบบต้องการตัวแปรใดบ้าง

เป้าหมาย: ย้ายรหัสผ่านไปไว้ในไฟล์ .env ที่ไม่เข้า Git และ commit .env.example เป็นเอกสารของตัวแปรที่ต้องตั้ง (LLO 7.1 และเตรียมจุดตรวจความลับในบทที่ 9)

ฝึกตามตัวอย่าง

E.1 สร้าง .env.example และ .env (สมาชิกคนที่ 1 หลังจาก pull request ของ Lab D merge แล้ว)

BASH
git switch main
git pull
git switch -c compose/env-example
nano .env.example
TEXT
# คัดลอกเป็น .env แล้วเปลี่ยนค่า ห้าม commit ไฟล์ .env
POSTGRES_PASSWORD=change-me
BASH
cp .env.example .env
nano .env
git status --short
git check-ignore -v .env
TEXT
?? .env.example
.gitignore:2:.env	.env

ในไฟล์ .env ให้เปลี่ยน change-me เป็นรหัสผ่านที่ใช้ในเครื่องตนเอง ถ้าเครื่องมีฐานข้อมูลใน volume จาก Lab C อยู่แล้ว ต้องใช้รหัสผ่านเดิม (dev-only-pass) ตามที่อธิบายในหัวข้อ 7.4 git status แสดงเพียง .env.example และ git check-ignore ยืนยันว่า .env ถูกกันด้วยบรรทัดที่ 2 ของ .gitignore

E.2 ตรวจว่า Compose อ่านค่าจาก .env (สมาชิกทุกคน ในเทอร์มินัลใหม่ที่ไม่ได้ export)

BASH
unset POSTGRES_PASSWORD
docker compose config -q
docker compose up -d --wait
docker compose exec db psql -U app -d app -tc "SELECT current_user;"
TEXT
 Container devops-app-app-1 Healthy
 app

ตัดสินใจเอง E.3: ทีมจะเพิ่มขั้นตอนสั้น ๆ ใน README.md สำหรับสมาชิกใหม่ ให้เขียนขั้นตอนตั้งแต่ clone จนระบบ healthy ไม่เกิน 5 ขั้น โดยต้องมีขั้นที่เกี่ยวกับ .env และคำเตือนเรื่อง docker compose down -v และ docker compose config อย่างละหนึ่งประโยค แล้ว commit พร้อม .env.example ใน pull request เดียวกัน ผู้ตรวจทานต้องทำตามขั้นตอนใน README.md บนโฟลเดอร์ที่ clone ใหม่และยืนยันว่าทำได้จริง

ผลลัพธ์ที่คาดหวัง E.3: ผู้ตรวจทานทำตามขั้นตอนบน clone ใหม่แล้ว docker compose up -d --wait จบด้วย exit code 0 และ git ls-files ของ main ไม่มี .env

จุดตรวจ E: compose.yaml และไฟล์ใดใน repository ไม่มีรหัสผ่านจริง หลัง merge ทุกคนทำ C.2 ซ้ำบน main เพื่อเก็บหลักฐาน M6 ของตนเอง

ปัญหาที่พบบ่อย

อาการสาเหตุวิธีแก้
git status แสดง .envไฟล์ .gitignore ไม่มีบรรทัด .env หรือสร้าง .env ในโฟลเดอร์อื่นเพิ่ม .env ใน .gitignore และตรวจด้วย git check-ignore -v .env
Compose ใช้รหัสผ่านเก่าแม้แก้ .env แล้วยังมีค่าที่ export ค้างใน terminal ซึ่งมีลำดับก่อน .envunset POSTGRES_PASSWORD หรือเปิด terminal ใหม่
commit .env ไปแล้วลืมตรวจก่อน git addแจ้งผู้สอน ลบไฟล์ออกจาก repository และเปลี่ยนรหัสผ่าน การลบใน commit ถัดไปไม่ได้ลบออกจากประวัติ

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

เก็บกวาดเมื่อจบปฏิบัติการ

ระบบของบทนี้ใช้ต่อในบทที่ 8 และ 9 จึงไม่ต้องลบ volume ให้หยุดระบบด้วย docker compose down เท่านั้น และลบ anonymous volume ที่ค้างจาก Lab B ด้วย docker volume prune -f ซึ่งลบเฉพาะ volume ที่ไม่มี container ใดใช้และไม่มีชื่อ ก่อนสั่งให้ตรวจด้วย docker volume ls ว่า devops-app_db-data ยังอยู่หลังสั่ง ห้ามใช้ docker compose down -v หรือ docker volume prune -af เพราะทั้งสองคำสั่งลบข้อมูลของฐานข้อมูลที่ใช้เป็นหลักฐาน M6

สรุป

Docker Compose อธิบายระบบที่มีหลายบริการไว้ในไฟล์ compose.yaml ไฟล์เดียว แล้วสร้าง เริ่ม และหยุดทั้งระบบด้วยคำสั่ง docker compose up และ docker compose down ไฟล์นี้อยู่ใน repository ผ่าน review ได้เหมือนโค้ด และทำให้ทุกคนได้ระบบที่เหมือนกัน เมื่อบริการมีทั้ง build และ image ต้องใช้ --build ทุกครั้งที่โค้ดเปลี่ยน

Compose สร้าง network ของ project ให้อัตโนมัติ บริการเรียกกันด้วยชื่อบริการและพอร์ตภายใน container ส่วน localhost ภายใน container หมายถึงตัว container เอง ฐานข้อมูลไม่จำเป็นต้องเปิดพอร์ตออกสู่เครื่อง ซึ่งช่วยลดพื้นที่การโจมตี named volume ทำให้ข้อมูลคงอยู่เมื่อ container ถูกลบ docker compose down ไม่ลบ volume แต่ down -v ลบ และ volume ไม่ใช่การสำรองข้อมูล

healthcheck บอกว่าบริการพร้อมรับงาน ต่างจากสถานะ Up ที่บอกเพียงว่า process ยังทำงาน depends_on ต้องใช้ condition: service_healthy จึงจะรอให้บริการที่พึ่งพาพร้อมจริง และ docker compose up --wait คืน exit code ที่บอกได้ว่าระบบ healthy หรือไม่ รหัสผ่านแยกไปไว้ในไฟล์ .env ที่ไม่เข้า Git โดยมี .env.example เป็นเอกสารของตัวแปรที่ต้องตั้ง

สิ่งที่ส่งต่อไปยังโครงงานและ Capstone

สิ่งที่ได้นำไปใช้ที่
compose.yaml และ M6บทที่ 8 เครื่องปลายทางรันระบบด้วย Compose · บทที่ 9 เพิ่มบริการ Nginx · บทที่ 11 เพิ่ม Prometheus และ Grafana
healthcheck และ docker compose up --waitบทที่ 11 deploy gate · บทที่ 12 ตรวจผลหลัง rollback
.env.example และ .env ที่ไม่อยู่ใน Gitบทที่ 9 จุดตรวจความลับครั้งที่ 2 · บทที่ 10 ย้ายความลับไป GitHub Secrets
ขั้นตอนรันระบบใน README.mdสมาชิกใหม่และผู้ประเมิน Capstone ใช้รันระบบของทีม
Learning Log ครั้งที่ 7portfolio ที่ส่งในบทที่ 13

สรุปคำสั่งในบทนี้

คำสั่งหน้าที่
docker compose versionตรวจว่ามี Compose plugin และรุ่นที่ใช้
docker compose config -qตรวจไฟล์ Compose โดยไม่พิมพ์ผล (ผลแบบเต็มแสดงค่าที่แทนแล้ว รวมรหัสผ่าน)
docker compose up -dสร้างและเริ่มทุกบริการเบื้องหลัง
docker compose up -d --buildbuild image ใหม่ก่อนเริ่ม
docker compose up -d --waitรอจนทุกบริการ healthy แล้วคืน exit code 0 หรือ 1
docker compose psแสดงสถานะและสุขภาพของบริการ
docker compose logs บริการแสดง log ของบริการ
docker compose exec บริการ คำสั่งรันคำสั่งใน container ของบริการ
docker compose exec db psql -U app -d app -c "SQL"รันคำสั่ง SQL ในฐานข้อมูล
docker compose exec db pg_isready -U app -d appตรวจว่าฐานข้อมูลรับการเชื่อมต่อแล้ว
docker compose downหยุดและลบ container และ network (เก็บ named volume)
docker compose down -vหยุดและลบรวม volume (ข้อมูลหาย)
docker volume ls, docker volume prune -fแสดง volume และลบ anonymous volume ที่ไม่มี container ใช้
git check-ignore -v ไฟล์ตรวจว่าไฟล์ถูก .gitignore กันไว้ด้วยกฎใด

แบบฝึกหัดท้ายบท

ตอนที่ 1 ฝึกทบทวน

ชุด FP10306343-P07 จำนวน 12 ข้อ ข้อละ 1 คะแนน รวม 12 คะแนน ครอบคลุมโครงสร้างของ compose.yaml คำสั่ง up และ down การเรียกบริการผ่าน network และพอร์ต volume ที่ทำให้ข้อมูลคงอยู่ healthcheck กับลำดับการเริ่มบริการ และการแทนค่าตัวแปรจาก .env

เข้าสู่ระบบห้องเรียนด้วยบัญชีที่ลงทะเบียนในรายวิชา 10306343 แล้วตอบคำถามให้ครบ ระบบตรวจคำตอบและแสดงคำอธิบายเพื่อทบทวน เมื่อทำครบและส่งผล ระบบจะบันทึกคะแนนไว้กับบัญชีของผู้เรียน สามารถทำซ้ำเพื่อทบทวนได้ ส่วนคะแนนที่นำไปรวมในรายวิชาเป็นไปตามเกณฑ์และกำหนดส่งในสมุดคะแนน

แบบฝึกหัดเพิ่มเติมสำหรับอภิปรายและเขียนตอบ

แบบฝึกหัดแบ่งเป็นสามกลุ่ม ได้แก่ ความเข้าใจ การประยุกต์ และการวิเคราะห์ปัญหา ทุกข้อฝึกผลลัพธ์การเรียนรู้ 7.1

ก. ความเข้าใจ

  • ก1 (LLO 7.1) อธิบายความต่างของ docker compose down, docker compose down -v และ docker compose stop ในแง่ของสิ่งที่ถูกลบ
  • ก2 (LLO 7.1) อธิบายว่าเหตุใดบริการ db ในบทนี้จึงไม่มี ports แต่บริการ app ยังเชื่อมต่อได้
  • ก3 (LLO 7.1) อธิบายความต่างของสถานะ Up, Up (healthy) และ Up (unhealthy) และระบุว่า Compose ทำอะไรกับ container ที่ unhealthy

ข. การประยุกต์

  • ข1 (LLO 7.1) ไฟล์ compose.yaml ต่อไปนี้มีจุดผิด 3 จุด ระบุแต่ละจุด อธิบายอาการที่เกิดขึ้นเมื่อสั่ง docker compose config -q หรือ docker compose up -d และเขียนส่วนที่แก้แล้ว (แอปฟังพอร์ต 3000 และไฟล์ .env ตั้งรหัสผ่านไว้แล้ว)
YAML
services:
  app:
    image: devops-app:0.2.0
    ports:
      - "3000:8080"
    depends_on:
      db:
        condition: service_healthy
 
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is not set}
    volumes:
      - db-data:/var/lib/postgresql/data
  • ข2 (LLO 7.1) ทีมต้องการเพิ่มบริการ cache จาก image redis:7 ที่บริการ app เรียกได้ที่พอร์ต 6379 แต่เครื่องอื่นในเครือข่ายเรียกไม่ได้ เขียนส่วนที่ต้องเพิ่มใน compose.yaml และระบุชื่อ host ที่แอปต้องใช้
  • ข3 (LLO 7.1) เขียนลำดับคำสั่งที่ใช้เป็นหลักฐานว่าข้อมูลคงอยู่หลัง down/up ตั้งแต่ระบบยังไม่ทำงานจนได้ผล SELECT ครั้งที่สอง

ค. การวิเคราะห์ปัญหา

  • ค1 (LLO 7.1) หลังสมาชิกคนหนึ่งเปลี่ยน POSTGRES_PASSWORD ใน .env แล้ว docker compose up -d ใหม่ แอปในงานต่อยอดซึ่งเชื่อมต่อฐานข้อมูลด้วยรหัสผ่านจาก .env ล้มด้วย password authentication failed for user "app" ทั้งที่รหัสผ่านใน .env กับที่แอปใช้ตรงกัน วิเคราะห์สาเหตุและเสนอทางแก้สองทาง ทางหนึ่งเก็บข้อมูลเดิมไว้ อีกทางหนึ่งไม่เก็บ
  • ค2 (LLO 7.1) เครื่องของสมาชิกคนหนึ่งมีพื้นที่ดิสก์เหลือน้อย ผล docker volume ls มี volume ชื่อเป็นเลขฐานสิบหกยาวหลายสิบรายการ วิเคราะห์ว่า volume เหล่านี้เกิดจากอะไร และเสนอคำสั่งเก็บกวาดที่ไม่กระทบข้อมูลของทีม
  • ค3 (LLO 7.1) สคริปต์ deploy ของทีมหนึ่งสั่ง docker compose up -d แล้วรายงานว่า "deploy สำเร็จ" ทุกครั้ง แม้บางครั้งแอปตอบ /health ไม่ได้ วิเคราะห์จุดอ่อนของสคริปต์ และเสนอการแก้โดยใช้สิ่งที่เรียนในบทนี้

เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ

การศึกษาค้นคว้าเพิ่มเติม

  1. อ่าน Docker Docs: Networking in Compose แล้วอธิบายใน Learning Log ว่า host port กับ container port ต่างกันอย่างไรในระบบของทีม
  2. อ่าน Docker Docs: Volumes เพื่อตอบ Lab C.3 และเปรียบเทียบ named volume กับ bind mount
  3. อ่าน Docker Docs: Control startup order in Compose แล้วเทียบตัวอย่างในเอกสารกับ compose.yaml ของทีม
  4. อ่านบทความ Lab: รัน Full Stack ในเครื่องด้วย Docker Compose พร้อม Hot Reload เพื่อดูระบบที่มีบริการมากกว่าสองบริการ และพิจารณาว่าส่วนใดใช้กับงานต่อยอดของตนได้
  5. ทำแบบฝึกหัดท้ายบทให้ครบ และเขียน Learning Log ครั้งที่ 7

เอกสารอ้างอิง

แหล่งข้อมูลเพิ่มเติมสำหรับตัวอย่างและกรณีอธิบาย

ตรวจเอกสารออนไลน์เมื่อ 23 ตุลาคม 2569 ตัวเลขและเหตุการณ์ที่ระบุว่าสมมติใช้เพื่ออธิบายกลไก ไม่ใช่ผลทดลองหรือสถิติขององค์กรจริง

เอกสารอ้างอิงของเนื้อหาเดิม

By Watcharin Sarachai