Skip to content

บทที่ 04 — Docker Compose: จัดการหลาย Container ด้วยไฟล์เดียว

← บทที่ 03 | สารบัญ | บทที่ 05: Security + Optimization →

3 บทที่ผ่านมา เราสั่ง container ด้วย docker run ทีละตัว — พิมพ์ option ยาวเหยียด. บทนี้สอน Docker Compose — เครื่องมือที่ทำให้จัดการ "ระบบที่มีหลาย container" ด้วยไฟล์ config ไฟล์เดียว

อ่านจบจะ:

  • เข้าใจว่าทำไม docker run ทีละตัว ไม่ไหวเมื่อระบบโต
  • เขียน compose.yaml เป็น — อธิบายได้ทุกบรรทัด
  • รัน stack ทั้งระบบ (web + api + database + cache) ด้วยคำสั่งเดียว
  • ใช้ healthcheck, depends_on, profile, override สำหรับ production

ใช้เวลา 3-4 ชั่วโมง

📚 ไม่ต้องอ่านรวดเดียวจบ — แบ่งอ่านได้ ลองเขียน compose.yaml ตาม Lab ไปด้วยจะเข้าใจกว่าอ่านยาว ๆ รวดเดียว


Part 1: ปัญหาของ docker run

ลองนึกระบบจริง — มี 4 container:

bash
# network
docker network create app-net

# database
docker run -d --name db --network app-net \
    -e POSTGRES_PASSWORD=secret \
    -e POSTGRES_DB=myapp \
    -v pgdata:/var/lib/postgresql/data \
    postgres:16

# redis cache
docker run -d --name cache --network app-net \
    redis:7

# backend api
docker run -d --name api --network app-net \
    -e DATABASE_HOST=db \
    -e REDIS_HOST=cache \
    -e DATABASE_PASSWORD=secret \
    myapi:v1

# nginx
docker run -d --name web --network app-net \
    -p 80:80 \
    -v ./nginx.conf:/etc/nginx/nginx.conf:ro \
    nginx:1.25

ปัญหา:

  • พิมพ์ยาวมาก — จำ option ทุกตัวไม่ไหว
  • ลำดับสำคัญ — ต้องรัน db ก่อน api
  • แชร์กับทีมยาก — ต้องส่งคำสั่งทั้งหมดให้เพื่อน
  • เก็บประวัติยาก — config ไม่ได้อยู่ใน Git
  • ลบ/restart ทั้งระบบ — ต้องไล่ทำทีละตัว

Docker Compose แก้ทุกข้อ — เขียน config ทั้งหมดเป็นไฟล์ compose.yaml ไฟล์เดียว แล้วสั่ง docker compose up ครั้งเดียวจบ


Part 2: Compose ไฟล์แรก

compose.yaml ที่ทำงานเทียบเท่าคำสั่งยาว ๆ ข้างบน:

ก่อนดู YAML — ลองนึกภาพระบบที่ compose ไฟล์นี้สร้างขึ้น:

yaml
name: myapp                    # ⭐ project name — pin ไว้ให้คงที่ทุกเครื่อง
                               #    (ถ้าไม่ใส่ Compose จะใช้ชื่อโฟลเดอร์แทน)

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}      # ⚠️ อย่าฝัง password ตรง ๆ — ดู Part 9
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data

  cache:
    image: redis:7

  api:
    image: myapi:v1
    environment:
      DATABASE_HOST: db
      REDIS_HOST: cache
      DATABASE_PASSWORD: ${DB_PASSWORD}      # ⚠️ อย่าฝัง password ตรง ๆ — ดู Part 9

  web:
    image: nginx:1.25
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

volumes:
  pgdata:

แล้วสั่ง:

bash
docker compose up -d

เท่านี้ — Docker Compose สร้าง network, สร้าง volume, รัน 4 container ให้ครบ ตามลำดับที่เหมาะสม

💡 Tip: ตัวอย่างข้างบนตัด option หลาย ๆ ตัวออกเพื่อให้อ่านง่าย — ในงานจริงควรเพิ่ม restart: unless-stopped ให้ container restart อัตโนมัติถ้า crash หรือ server reboot รายละเอียดครบใน Part 5

⚠️ version: field obsolete แล้ว — Compose Specification ใหม่ (ตั้งแต่ Compose v2) ถอด version: ออก ถ้าใส่จะเจอ warning เวลาสั่ง docker compose up ไฟล์เก่า ๆ ที่ขึ้นต้นด้วย version: "3.8" ลบบรรทัดนั้นทิ้งได้เลย — ไม่ต้องแทนที่ด้วยอะไร

📝 Compose v2 vs v1: Compose v2 = เวอร์ชันใหม่ที่ติดมากับ Docker โดยตรง (สั่งด้วย docker compose มี space) ส่วน v1 = เวอร์ชันเก่าที่ต้องลงแยก (docker-compose มีขีด) — v1 EOL แล้วตั้งแต่ปี 2023

💡 name: top-level = pin project name ให้คงที่ ไม่ขึ้นกับชื่อโฟลเดอร์ (ถ้าไม่ใส่ Compose ใช้ชื่อโฟลเดอร์ปัจจุบันเป็น project name — เปลี่ยน path แล้วเจอ container/volume คนละชุดได้)

2.1 ทำไม Compose ไม่ต้องระบุ network

สังเกตว่าใน compose.yaml ข้างบน — เราไม่ได้สร้าง network เลย แต่ container คุยกันได้

เพราะ Compose สร้าง network ให้อัตโนมัติ 1 อัน แล้วใส่ทุก service เข้า network นั้น — และมันเป็น custom bridge → มี DNS → service เรียกชื่อกันได้

นั่นคือเหตุผลที่ api ตั้ง DATABASE_HOST: db ได้ — db คือ "ชื่อ service" ใน compose ซึ่งกลายเป็นชื่อ DNS ที่ใช้เรียกได้


Part 3: รู้จัก YAML ก่อน (สำหรับมือใหม่)

compose.yaml เขียนด้วยภาษา YAML (อ่านว่า "แยม-เมิล" หรือ "ยา-เมิล" — ย่อจาก "YAML Ain't Markup Language") — มือใหม่ต้องเข้าใจกฎพื้นฐาน

yaml
# comment ขึ้นต้นด้วย #

key: value                  # คู่ key-value

parent:                     # nested — ใช้ "การเยื้อง (indent)" บอกระดับ
  child: value
  child2: value

list:                       # list — ขึ้นต้นด้วย -
  - item1
  - item2

key: "value with: colon"    # ถ้า value มีอักขระพิเศษ ใส่ quote

⚠️ กฎที่ห้ามผิด ของ YAML:

  • เยื้องด้วย space เท่านั้น — ห้ามใช้ Tab (Tab ทำให้ error)
  • เยื้องต้องสม่ำเสมอ (มัก ใช้ 2 space)
  • การเยื้องคือสิ่งที่บอกความสัมพันธ์ "ใครอยู่ใต้ใคร"

💡 แนะนำใช้ VS Code (ฟรี) พร้อม extension "YAML" ของ Red Hat — มันจะแจ้งเตือนทันทีถ้าใช้ Tab แทน space, highlight indent level ให้เห็นชัด และ autocomplete key ของ compose.yaml ได้

ตัวอย่างที่ผิดบ่อย:

yaml
services:
  web:
   image: nginx        # ❌ เยื้อง 3 space ไม่ตรงกับ 2 space ของบรรทัดอื่น

Part 4: โครงสร้างของ compose.yaml

ไฟล์ compose มี 3 ส่วนหลัก:

yaml
services:        # ← container ต่าง ๆ (ส่วนหลัก)
  ...

volumes:         # ← volume ที่ใช้
  ...

networks:        # ← network ที่ใช้ (ถ้าต้องการกำหนดเอง)
  ...

💡 หมายเหตุชื่อไฟล์: ชื่อมาตรฐานใหม่คือ compose.yaml — แต่ชื่อเดิม docker-compose.yml ก็ยังใช้ได้ Compose อ่านทั้งคู่


Part 5: นิยาม Service ทุก Option (อธิบายละเอียด)

มาดู service ตัวหนึ่งแบบเต็ม ๆ พร้อมคำอธิบายทุกบรรทัด:

yaml
services:
  api:
    # --- เลือก image หรือ build ---
    image: myapi:v1                  # ใช้ image สำเร็จรูป
    # หรือ build จาก Dockerfile:
    build:
      context: .                     # โฟลเดอร์ที่มี Dockerfile
      dockerfile: Dockerfile         # ชื่อไฟล์ (default: Dockerfile)
      args:                          # build arg
        VERSION: "1.0"
      target: production             # multi-stage: build แค่ stage นี้

    container_name: my-api           # ตั้งชื่อ container (ปกติไม่ต้องตั้ง)

    # --- restart policy ---
    restart: unless-stopped          # restart อัตโนมัติถ้าล่ม

    # --- port mapping ---
    # 💡 ใส่ quote รอบ port เสมอ — กัน YAML parse "5060:5061" เป็นเลขฐาน 60
    ports:
      - "8080:3000"                  # host:container
      - "127.0.0.1:9090:9090"        # bind localhost เท่านั้น

    # --- environment variables ---
    environment:
      NODE_ENV: production
      DATABASE_HOST: db
    env_file:                        # หรือโหลดจากไฟล์
      - .env

    # --- volumes ---
    volumes:
      - mydata:/app/data             # named volume
      - ./config:/app/config:ro      # bind mount, read-only

    # --- network ---
    networks:
      - frontend
      - backend

    # --- depends_on: ลำดับการ start ---
    depends_on:
      db:
        condition: service_healthy   # รอจน db healthy ก่อน

    # --- healthcheck ---
    # ✅ ใช้ wget เป็น default — รองรับ alpine (ซึ่งติดมากับ busybox) โดยไม่ต้องลงเพิ่ม
    # ⚠️ ถ้า image ไม่มีทั้ง wget และ curl (เช่น distroless) — healthcheck จะ fail ตลอด
    #    ทางเลือก:
    #    - ถ้า image มี curl: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
    #    - ติดตั้ง curl ใน Dockerfile: `RUN apk add --no-cache curl`
    #    ดูรายละเอียดเพิ่มใน Docker บท 01 Part 3.12
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
      interval: 30s                  # ตรวจทุก 30 วินาที
      timeout: 5s                    # รอผลไม่เกิน 5 วินาที
      retries: 3                     # fail 3 ครั้งติด = ป่วย
      start_period: 10s              # ช่วง warm-up ช่วงแรก ไม่นับ fail

    # --- resource limits ---
    # ✅ ตั้งแต่ Docker Compose v2.17+: `deploy.resources.limits` ทำงานได้กับ
    #    `docker compose up` ปกติโดยไม่ต้อง flag พิเศษ — memory และ cpus limits
    #    ถูกบังคับใช้จริง (ตรวจเวอร์ชันด้วย `docker compose version`)
    # 📝 Docker Swarm (เครื่องมือ orchestration เก่าของ Docker สำหรับรัน container
    #    ข้ามหลายเครื่อง — ปัจจุบัน maintenance mode แล้ว) ใช้ `deploy:` block เช่นกัน
    #    แต่มี field เพิ่มเติมที่ Compose ปกติไม่รองรับ เช่น replicas, placement
    deploy:
      resources:
        limits:
          cpus: "1"                  # ใช้ CPU ไม่เกิน 1 core
          memory: 512M               # ใช้ RAM ไม่เกิน 512 MB

    # --- user ---
    user: "1000:1000"

    # --- command override (override CMD ใน Dockerfile) ---
    command: ["node", "server.js", "--verbose"]

ไม่ต้องใส่ทุก option — ใส่เท่าที่ต้องการ ส่วนใหญ่มี default ที่ดีอยู่แล้ว


Part 6: depends_on + healthcheck — เริ่มให้ถูกลำดับ

6.1 ปัญหา

api ต้องการ db พร้อมก่อนถึงจะทำงานได้ — ถ้า api เริ่มก่อน db พร้อม → api เชื่อม database ไม่ได้ → crash

6.2 depends_on แบบธรรมดา — ยังไม่พอ

yaml
api:
  depends_on:
    - db

อันนี้บอกแค่ "เริ่ม db ก่อน api" — แต่ปัญหาคือ "เริ่ม container db" ≠ "database พร้อมรับ connection"

PostgreSQL ใช้เวลา 2-5 วินาทีหลัง container เริ่ม กว่าจะพร้อมรับ connection จริง — ระหว่างนั้น api อาจเริ่มไปแล้วและ crash

6.3 depends_on + healthcheck — วิธีที่ถูก

yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  api:
    image: myapi:v1
    depends_on:
      db:
        condition: service_healthy    # ← รอจน db "healthy" จริง ๆ

อธิบาย:

  • db มี healthcheck — ใช้คำสั่ง pg_isready (อ่านว่า "พีจี-อิส-เรดดี้" มาจาก "Postgres is ready?")
  • pg_isready เป็นเครื่องมือของ PostgreSQL ที่เช็คว่า database พร้อมรับ connection หรือยัง
  • api มี depends_on แบบระบุ condition: service_healthy — แปลว่า "อย่าเริ่ม api จนกว่า db จะ healthcheck ผ่าน"

⚠️ condition: service_healthy ต้องคู่กับ healthcheck เสมอ — ถ้า dependency ไม่มี healthcheck: กำหนดไว้ Compose จะ error ว่า "service has no healthcheck"

condition มี 3 แบบ:

  • service_started — แค่ container เริ่ม (default)
  • service_healthy — รอจน healthcheck ผ่าน ⭐ ใช้บ่อยสุด
  • service_completed_successfully — รอจน container ทำงานเสร็จและ exit 0 (สำหรับ container ที่ทำงานเป็น job)

Part 7: Volumes + Networks ใน Compose

7.1 Volumes

yaml
services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data     # ใช้ named volume

volumes:
  pgdata:                                    # ประกาศ volume ตรงนี้
  redis-data:
    driver: local
  external-vol:
    external: true                           # ใช้ volume ที่สร้างไว้แล้วข้างนอก

ทุก named volume ที่ service ใช้ ต้องประกาศใน section volumes: ด้วย

7.2 Networks

ถ้าไม่กำหนดอะไร — Compose สร้าง network เดียวให้ทุก service (พอใช้สำหรับงานง่าย ๆ)

ถ้าต้องการ network isolation (จากบท 02):

yaml
services:
  web:
    networks: [frontend]
  api:
    networks: [frontend, backend]
  db:
    networks: [backend]

networks:
  frontend:
  backend:
    internal: true            # network นี้ออก internet ไม่ได้

ผลคือ: web คุย db ตรง ๆ ไม่ได้ (ไม่มี network ร่วม) — ต้องผ่าน api


Part 8: Stack สมบูรณ์ — ตัวอย่าง production จริง

yaml
services:
  # --- Reverse proxy ---
  proxy:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      # ⚠️ mount เป็น conf.d/default.conf ไม่ใช่ /etc/nginx/nginx.conf
      #    เพื่อไม่ทับ events{} / http{} block หลัก
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      # ⚠️ cert ต้องสร้างก่อนแยกต่างหาก — เช่น ด้วย certbot/acme.sh (Let's Encrypt)
      #    หรือ cert manager — compose ไม่ได้ออก cert ให้ ต้องจัดการ rotation เองด้วย
      - ./certs:/etc/nginx/ssl:ro            # path มาตรฐานของ nginx SSL
    networks: [frontend]
    depends_on:
      - api

  # --- Backend API ---
  api:
    build:
      context: ./api
      target: production
    restart: unless-stopped
    environment:
      DATABASE_HOST: db
      DATABASE_PASSWORD: ${DB_PASSWORD}      # อ่านจากไฟล์ .env
      REDIS_HOST: cache
    networks: [frontend, backend]
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    healthcheck:
      # ✅ ใช้ wget — รองรับ alpine image โดยไม่ต้องลงเพิ่ม
      # ถ้า api image มี curl: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 512M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  # --- Database ---
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: appuser
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks: [backend]
    healthcheck:
      # -d myapp = ระบุ database ให้ตรง — ไม่งั้น pg_isready จะลอง connect db ชื่อ appuser
      # (default) ซึ่งอาจไม่มี
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  # --- Cache ---
  cache:
    image: redis:7-alpine
    restart: unless-stopped
    # AOF = append-only file (persist ทุก write). Redis 7+ ใช้ AOF+RDB hybrid โดย default แล้ว
    # ตัวอย่างนี้บังคับเปิด AOF ชัดเจน — ใช้ได้เมื่อ Redis ทำหน้าที่เป็น "ที่เก็บข้อมูลหลัก"
    # ⚠️ ถ้าใช้ Redis เป็น cache อย่างเดียว (ข้อมูลหายได้) → ไม่ต้องใช้ --appendonly yes
    #    เพราะ AOF เพิ่ม write overhead โดยไม่มีประโยชน์สำหรับ cache
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    networks: [backend]

volumes:
  pgdata:
  redis-data:

networks:
  frontend:
  backend:
    internal: true

อ่าน compose ไฟล์นี้แล้วจะเห็นภาพระบบทั้งหมด — ดีกว่าไล่อ่านคำสั่ง docker run หลายสิบบรรทัด


Part 9: Environment Variables + .env

อย่าฝังรหัสผ่านไว้ใน compose.yaml ตรง ๆ (เพราะไฟล์นี้มัก commit เข้า Git)

ใช้ไฟล์ .env (อยู่ข้าง ๆ compose.yaml):

.env:

DB_PASSWORD=super-secret-123
TAG=v1.2.3

compose.yaml:

yaml
services:
  db:
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}     # ${} ดึงค่าจาก .env
  api:
    image: myapi:${TAG}

แล้วใส่ .env ใน .gitignore — รหัสผ่านไม่หลุดเข้า Git

💡 Compose โหลด .env อัตโนมัติ โดยไม่ต้องระบุ env_file: ทุกครั้งที่สั่ง docker compose up Compose จะอ่าน .env ที่อยู่ข้าง ๆ compose.yaml เสมอ — ทำให้ environment บน CI ที่ไม่มีไฟล์ .env อาจต่างจาก local ใช้ docker compose config เพื่อตรวจว่า variable ถูก resolve ถูกต้อง

9.1 Compose Secrets — วิธีที่ปลอดภัยกว่า env var

.env ดีกว่าฝัง plaintext ใน YAML แต่ยังมีปัญหา: env var ของ container มองเห็นได้ ผ่าน docker inspect, ไฟล์ระบบที่แสดง environment ของ process (โปรแกรมใด ๆ บนเครื่องอาจอ่านได้), error message ที่ dump env, log

secrets: block ของ Compose แก้ปัญหานี้ — mount ความลับเป็น ไฟล์ ที่ /run/secrets/<name> แทน env var:

yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: appuser
      POSTGRES_DB: myapp
      # postgres image รองรับ _FILE suffix — อ่านค่าจากไฟล์แทน env ตรง ๆ
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./db_password.txt    # ไฟล์ที่เก็บ password (chmod 600, .gitignore)

ข้อดี:

  • docker inspect ไม่เห็นค่า — เห็นแค่ว่ามี secret mount ไว้
  • ไฟล์อยู่ใน tmpfs (RAM) ของ container ไม่เขียนลง disk
  • เปลี่ยน secret = แก้ไฟล์ → restart service เดียว ไม่ต้อง rebuild image

💡 production จริงจัง — เก็บ secret ใน vault (HashiCorp Vault, AWS Secrets Manager, Doppler) แล้ว inject เข้า container ตอน start


Part 10: คำสั่ง Compose ที่ใช้บ่อย

bash
# เริ่มทุก service (สร้าง + รัน) แบบเบื้องหลัง
docker compose up -d

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

# ดู service ที่รันอยู่
docker compose ps

# ดู log
docker compose logs                  # ทั้งหมด
docker compose logs api              # service เดียว
docker compose logs -f api           # ตามแบบ real-time

# หยุดทุก service (แต่ยังเก็บ container ไว้)
docker compose stop

# เริ่มที่หยุดไว้
docker compose start

# restart
docker compose restart api

# หยุด + ลบ container + ลบ network
docker compose down

# down + ลบ volume ด้วย (⚠️ ข้อมูลหาย!)
docker compose down -v

# รันคำสั่งใน service ที่กำลังรัน
docker compose exec api sh

# รันคำสั่งใน container ใหม่ชั่วคราว
# 💡 ใส่ --rm เสมอ — ไม่งั้น Compose จะเก็บ container ที่ run ทิ้งไว้สะสมเรื่อย ๆ
docker compose run --rm api npm test

# ดู config ที่ resolved แล้ว (เช็คว่า .env ถูกแทนค่าถูกไหม)
docker compose config

# ดึง image ล่าสุด
docker compose pull

# build อย่างเดียว
docker compose build

⚠️ v1 ตายแล้ว — ใช้ docker compose (มี space) เท่านั้น

  • docker compose (มี space) = v2 — เวอร์ชันใหม่ที่ติดมากับ Docker ใช้งานได้โดยตรง — ใช้ตัวนี้
  • docker-compose (มีขีด) = v1 — เวอร์ชันเก่าที่ต้องลงแยก ปัจจุบัน EOL ตั้งแต่ 2023 ถูกถอดออกจาก Docker Desktop รุ่นใหม่แล้ว

ตรวจรุ่นที่ติดตั้ง: docker compose version — ขึ้น Docker Compose version v2.x.x = พร้อมใช้


Part 11: Profile — เปิด service บางตัวเฉพาะบางสถานการณ์

บางทีเราอยากมี service ที่เปิดเฉพาะตอน debug — ไม่อยากให้รันตอนปกติ

yaml
services:
  api:
    image: myapi

  db:
    image: postgres:16

  # tool สำหรับดู database — เปิดเฉพาะตอนต้องการ
  adminer:
    image: adminer
    ports: ["8081:8080"]
    profiles: [debug]            # ← อยู่ใน profile "debug"
bash
docker compose up -d                      # รันแค่ api + db (adminer ไม่รัน)
docker compose --profile debug up -d      # รัน api + db + adminer

service ที่มี profiles: จะไม่รัน เว้นแต่เราระบุ profile นั้น — ใช้แยก dev tool, test service ออกจาก service หลัก


Part 11.5: develop.watch — Dev Workflow แบบใหม่ (Compose v2.22+)

วิธีดั้งเดิม dev ด้วย Compose คือ bind mount source code เข้า container แล้วใช้ nodemon/air/ts-node-dev ดู file change — ใช้ได้ แต่:

  • bind mount บน Mac/Windows ช้า (filesystem ข้าม VM)
  • ต้องมี watcher อยู่ใน container — เพิ่ม dependency
  • เปลี่ยน package.json ต้อง rebuild image เอง

develop.watch (Compose v2.22+, ปี 2026 = วิธีมาตรฐาน) ทำใน Compose โดยตรง:

yaml
services:
  api:
    build: ./api
    develop:
      watch:
        - path: ./api/src              # ดูโฟลเดอร์นี้
          action: sync                 # เปลี่ยน → sync ไฟล์เข้า container ทันที
          target: /app/src
        - path: ./api/package.json     # เปลี่ยน package.json
          action: rebuild              # → rebuild image ใหม่อัตโนมัติ
        - path: ./api/static
          action: sync+restart         # → sync แล้ว restart container
          target: /app/static

สั่งด้วย:

bash
docker compose watch

ข้อดี:

  • ไม่ต้อง bind mount → เร็วบน Mac/Windows
  • 3 action ครอบคลุมทุกกรณี: sync (ไฟล์ source), rebuild (manifest เปลี่ยน), sync+restart (config ที่ต้อง reload)
  • ไม่ต้องมี watcher ใน container

💡 ตอนนี้แทบทุก dev workflow ที่เคยใช้ volumes: [.:/app] เริ่มย้ายมาใช้ develop.watch แทน — production ยังคงใช้ image แบบ build เต็มเหมือนเดิม


Part 12: Override File — config ต่างกันตาม environment

🚀 โซนขั้นสูง — ข้ามได้ Part 12 (override file สำหรับ dev/prod) เป็นเทคนิคที่ใช้ตอนระบบเริ่มโตและต้องแยก config หลาย environment — มือใหม่ที่เพิ่งหัดเขียน compose ข้ามไปก่อนได้ แล้วค่อยกลับมาตอนต้องแยก dev/prod จริง

ปัญหา: dev กับ production ต้องการ config ต่างกัน (dev อยาก mount source code, production ไม่อยาก)

Compose แก้ด้วยการ merge หลายไฟล์

compose.yaml — config พื้นฐานร่วมกัน:

yaml
services:
  api:
    image: myapi
    environment:
      LOG_LEVEL: info

compose.override.yaml — config สำหรับ dev:

💡 โหลดอัตโนมัติเสมอ ไม่ต้องระบุ flag — ทุกครั้งที่สั่ง docker compose up (ไม่มี -f) Compose จะอ่าน compose.yaml + compose.override.yaml แล้ว merge ให้เอง ถ้าอยากใช้ไฟล์อื่นแทน override default ต้องระบุด้วย -f ทั้งสองไฟล์

yaml
services:
  api:
    build: .                       # dev: build จาก source
    volumes:
      - .:/app                     # dev: mount source code
    environment:
      LOG_LEVEL: debug             # dev: log ละเอียด

compose.prod.yaml — config สำหรับ production:

yaml
services:
  api:
    image: ghcr.io/acme/myapi:v1.0.0
    environment:
      LOG_LEVEL: warn

ใช้งาน:

bash
# dev — Compose โหลด compose.yaml + compose.override.yaml อัตโนมัติ
docker compose up -d

# production — ระบุไฟล์เอง
docker compose -f compose.yaml -f compose.prod.yaml up -d

เวลา merge — ไฟล์ที่ระบุทีหลังจะ "ทับ" ไฟล์ก่อนหน้า


Part 13: Scaling

bash
docker compose up -d --scale api=3

รัน api 3 ตัวพร้อมกัน

⚠️ ข้อจำกัด: Compose ทำ scaling ได้พื้นฐานเท่านั้น — ไม่มี load balancer ในตัว, ไม่มี auto-restart ข้ามเครื่อง, ไม่มี rolling update

→ ถ้าต้องการ scaling/orchestration จริงจัง → ใช้ Kubernetes (หมวดถัดไป!) — Compose เหมาะกับ dev + production เล็ก ๆ


Part 14: Logging Config

yaml
services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"          # log แต่ละไฟล์ไม่เกิน 10MB
        max-file: "3"            # เก็บไม่เกิน 3 ไฟล์

ทำไมสำคัญ: ถ้าไม่จำกัด — log ไฟล์โตไม่หยุดจนเต็ม disk → server ล่ม. ตั้ง max-size + max-file เสมอ


Part 15: Compose vs Kubernetes — เลือกตอนไหน

Docker ComposeKubernetes
ความซับซ้อนง่ายยาก (เรียนนาน)
เครื่อง1 เครื่องหลายเครื่อง (cluster)
Auto-restartระดับ containerระดับ cluster
Auto-scaling
Rolling update❌ (ทำเองได้บ้าง)
Self-healingจำกัด
เหมาะกับdev, prototype, prod เล็กproduction จริงจัง, scale

Compose = เริ่มต้นที่นี่ ใช้ dev เสมอ, prod เล็ก ๆ ก็ได้ → Kubernetes = เมื่อระบบโต ต้อง scale, ต้อง HA — เรียนหมวด kubernetes/ ต่อ

📝 HA (High Availability — "ความพร้อมใช้งานสูง") = ระบบที่ออกแบบให้ "แทบไม่ล่ม" ถ้าเครื่องหนึ่งพังก็ยังมีตัวสำรองทำงานต่อทันที ผู้ใช้ไม่รู้สึกว่ามีอะไรเกิดขึ้น

ข่าวดี: ความรู้ Compose ปูพื้นไป K8s ได้ดีมาก — concept คล้ายกัน (service, network, volume, healthcheck)


Part 16: Lab

Lab 1: Stack แรก

แล็บแรกพาเขียน compose.yaml หลายบริการในไฟล์เดียว — web (nginx) + api ที่ผูกกันด้วย depends_on แล้วสั่งทั้ง stack ขึ้น/ลงด้วย docker compose up/down คำสั่งเดียว แทนการ docker run ทีละตัว:

compose.yaml:

yaml
services:
  web:
    image: nginx:1.25-alpine
    ports: ["8080:80"]
    depends_on: [api]

  api:
    image: hashicorp/http-echo
    command: ["-text=Hello from API"]
bash
docker compose up -d
docker compose ps
docker compose logs

⚠️ ก่อน curl: Lab นี้ สาธิตแค่ depends_on (ลำดับการเริ่ม service) เท่านั้น — web (nginx) ยังไม่ได้ตั้งค่าให้ proxy ต่อไปยัง api จริง ๆ การรัน curl http://localhost:8080 จะได้ หน้า default ของ nginx ไม่ใช่ข้อความจาก api — นั่นคือผลที่ถูกต้องของ Lab นี้

วิธีตรวจว่า Lab สำเร็จ: รัน docker compose ps — ถ้าเห็นทั้ง web และ api อยู่ใน state running = สำเร็จ

bash
# ทดสอบ api โดยตรง (ไม่ผ่าน nginx):
docker compose exec api /bin/echo "api is running"
# หรือดู log api:
docker compose logs api

# ทดสอบ nginx (จะได้หน้า default — ถูกต้องตามที่ควรเป็น):
curl http://localhost:8080

docker compose down

📝 การต่อ nginx ให้ proxy ไป backend ทำตาม "Reverse Proxy Pattern" ในบท 02 Part 10 (ต้อง mount nginx.conf ที่ตั้ง proxy_pass)

Lab 2: App + DB + healthcheck

แล็บนี้ยกระดับด้วยการใช้ healthcheck คุม dependency จริง — ตั้ง depends_on ให้ adminer รอจน db ผ่าน healthcheck (พร้อมรับ connection) ก่อนค่อยเริ่ม แก้ปัญหาคลาสสิกที่แอปพังเพราะสตาร์ตเร็วกว่า database:

yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

  adminer:
    image: adminer
    ports: ["8081:8080"]
    depends_on:
      db:
        condition: service_healthy

volumes:
  data:
bash
docker compose up -d
# เปิด http://localhost:8081 → login server=db user=postgres password=secret

Lab 3: dev/prod override

ทำตาม Part 12 — สร้าง 3 ไฟล์ — ลองรันทั้ง 2 mode เทียบกัน


Part 17: Checkpoint

  1. ปัญหาของ docker run ทีละตัว มีอะไรบ้าง?
  2. compose.yaml มี 3 section หลักอะไร?
  3. ทำไม service ใน compose เรียกชื่อกันได้โดยไม่ต้องสร้าง network เอง?
  4. กฎ YAML ที่ห้ามผิดคืออะไร?
  5. depends_on ธรรมดา ทำไมยังไม่พอ?
  6. condition: service_healthy ทำอะไร?
  7. .env ใช้ทำอะไร? ทำไมไม่ฝัง password ใน compose.yaml?
  8. docker compose down กับ down -v ต่างกันยังไง?
  9. Profile ใช้ทำอะไร?
  10. Compose vs Kubernetes — เลือกตอนไหน?

Part 18: สรุปบทนี้

  • Docker Compose = นิยามหลาย container ในไฟล์ compose.yaml ไฟล์เดียว → docker compose up ครั้งเดียวจบ
  • Compose สร้าง network ให้อัตโนมัติ → service เรียกชื่อกันได้
  • เขียนด้วย YAML — เยื้องด้วย space ห้าม Tab
  • depends_on + healthcheck + condition: service_healthy = เริ่มให้ถูกลำดับ
  • .env = เก็บความลับแยกจาก compose.yaml
  • Profile = service ที่เปิดเฉพาะบางสถานการณ์
  • Override file = config ต่างกันตาม dev/prod
  • Compose เหมาะกับ dev + prod เล็ก — prod ใหญ่จริงจังใช้ Kubernetes

บทต่อไป (สุดท้ายของหมวด Docker) — Security + Optimization


← บทที่ 03 | สารบัญ | บทที่ 05: Security + Optimization →