Docker Compose DevOps Containers

Docker Compose — จัดการหลาย Containers ด้วยไฟล์เดียว 🐳Docker Compose — Run Many Containers From One File 🐳

By Anirach Mingkhwan DevOps & Vibe Coding 2026
Docker Compose — จัดการหลาย Containers ด้วยไฟล์เดียว
ในบทความนี้
  1. Docker Compose คืออะไร?
  2. ตัวอย่างแรก — Web App + Database
  3. Full Stack — React + API + DB + Redis + Nginx
  4. 🔧 Docker Compose คำสั่งสำคัญ
  5. 📁 .env File — แยก Config ออกจาก YAML
  6. 🔀 Multi-Environment — Dev vs Production
  7. 💾 Volumes — เก็บข้อมูลถาวร
  8. 🌐 Networks — Container คุยกันยังไง
  9. ❤️ Health Checks — ตรวจสุขภาพ
  10. 🚀 Production Tips
  11. สรุป
In this post
  1. What Is Docker Compose?
  2. A First Example — Web App + Database
  3. Full Stack — React + API + DB + Redis + Nginx
  4. 🔧 The Docker Compose Commands That Matter
  5. 📁 .env Files — Config Out of the YAML
  6. 🔀 Multi-Environment — Dev vs Production
  7. 💾 Volumes — Data That Persists
  8. 🌐 Networks — How Containers Talk
  9. ❤️ Health Checks — Taking the Pulse
  10. 🚀 Production Tips
  11. Summary

ในโพสต์ Docker vs VMs เราเรียนรู้ว่า Docker สร้าง container ได้ง่าย แต่ app จริงมักใช้หลาย services — web server, API, database, cache, queue...

ถ้าต้อง docker run ทีละตัว ตั้ง network เอง ตั้ง volume เอง... จะบ้าตาย! 😵

Docker Compose แก้ปัญหานี้ — กำหนดทุก service ในไฟล์ YAML เดียว แล้วรันด้วยคำสั่งเดียว! 🐳🐕


Docker Compose คืออะไร?

Docker Compose คือเครื่องมือที่ช่วย กำหนด + รันหลาย containers พร้อมกัน ด้วยไฟล์ docker-compose.yml (หรือ compose.yml)

📝 เขียน YAML
กำหนด services
→
🚀 docker compose up
รันทุก service
→
✅ ทำงานร่วมกัน!
network + volumes พร้อม
💡 จำง่ายๆ: Dockerfile = สูตรทำ 1 container | Docker Compose = สูตรทำทั้งร้านอาหาร (หลาย containers)

ตัวอย่างแรก — Web App + Database

มาเริ่มจากตัวอย่างง่ายที่สุด — web app + PostgreSQL database แค่ 2 services แต่ถ้าไม่มี Compose ต้อง: สร้าง network เอง, รัน postgres container ตั้ง password + volume, รัน app container ตั้ง env vars + เชื่อม network... รวม 5-6 คำสั่ง! Docker Compose ทำให้เหลือแค่ docker compose up:

# docker-compose.yml
services:
  # Web Application
  app:
    build: .                     # build จาก Dockerfile ใน directory นี้
    ports:
      - "3000:3000"              # host:container
    environment:
      - DATABASE_URL=postgres://myuser:mypass@db:5432/mydb
      - NODE_ENV=development
    volumes:
      - .:/app                   # mount code เข้าไป (dev mode)
      - /app/node_modules        # ไม่ mount node_modules
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  # PostgreSQL Database
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypass
      - POSTGRES_DB=mydb
    volumes:
      - pgdata:/var/lib/postgresql/data    # persist data
    ports:
      - "5432:5432"
    healthcheck:
      test: pg_isready -U myuser
      interval: 10s
      retries: 5

volumes:
  pgdata:                        # named volume สำหรับ DB data
# รันทุก service
docker compose up -d
# [+] Running 3/3
#  ✔ Network myapp_default  Created
#  ✔ Container myapp-db-1   Started
#  ✔ Container myapp-app-1  Started

# ดูสถานะ
docker compose ps
# NAME           SERVICE   STATUS    PORTS
# myapp-app-1    app       running   0.0.0.0:3000->3000/tcp
# myapp-db-1     db        running   0.0.0.0:5432->5432/tcp

# ดู logs
docker compose logs -f app      # follow logs ของ app
docker compose logs              # logs ทุก service

# หยุดทุก service
docker compose down              # หยุด + ลบ containers
docker compose down -v           # + ลบ volumes ด้วย (⚠️ data หาย!)

Full Stack — React + API + DB + Redis + Nginx

ตัวอย่างจริงที่ใกล้ production — 5 services ทำงานร่วมกัน: React frontend, Node.js API, PostgreSQL database, Redis cache, และ Nginx reverse proxy สังเกตการใช้ depends_on กำหนดลำดับการ start, healthcheck ตรวจว่า service พร้อมจริงก่อน start ตัวที่พึ่ง, volumes สำหรับ persistent data, และ networks แยก frontend/backend traffic:

Docker Compose Stack
Nginx
:80 → frontend
/api → backend
↓ reverse proxy ↓
Frontend
React :3000
Backend
Node.js :8080
↓
PostgreSQL
:5432
Redis
:6379
# docker-compose.yml — Full Stack
services:
  # ─── Nginx Reverse Proxy ───
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - frontend
      - backend
    restart: always

  # ─── React Frontend ───
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    environment:
      - REACT_APP_API_URL=/api
    expose:
      - "3000"                   # expose ให้ nginx เห็น (ไม่เปิด host)

  # ─── Node.js Backend ───
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=postgres://app:secret@db:5432/appdb
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}  # อ่านจาก .env file
    expose:
      - "8080"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  # ─── PostgreSQL ───
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: pg_isready -U app
      interval: 10s
      retries: 5
    restart: unless-stopped

  # ─── Redis Cache ───
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Nginx Config

Nginx ทำหน้าที่เป็น reverse proxy — รับ traffic ทั้งหมดแล้วส่งต่อไปยัง frontend หรือ API ตาม URL path ข้อดีคือ expose port เดียว (80) แทนที่ต้องเปิดหลาย ports และสามารถ cache, gzip, load balance ได้:

# nginx/default.conf
upstream frontend {
    server frontend:3000;
}

upstream backend {
    server backend:8080;
}

server {
    listen 80;

    # Frontend
    location / {
        proxy_pass http://frontend;
        proxy_set_header Host $host;
    }

    # API → Backend
    location /api/ {
        proxy_pass http://backend/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSocket support
    location /ws {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

🔧 Docker Compose คำสั่งสำคัญ

คำสั่ง Docker Compose ที่ใช้บ่อยในชีวิตประจำวัน — ตั้งแต่ start/stop services, ดู logs, exec เข้าไปใน container, ไปจนถึง rebuild images หลังแก้ Dockerfile สังเกตว่า docker compose (ไม่มี hyphen) เป็น v2 ที่แนะนำ แทน docker-compose v1:

# ─── Lifecycle ───
docker compose up                # รัน (foreground)
docker compose up -d             # รัน (background) ⭐
docker compose down              # หยุด + ลบ containers
docker compose down -v           # + ลบ volumes
docker compose restart           # restart ทุก service
docker compose stop              # หยุด (ไม่ลบ)
docker compose start             # เริ่มจาก stop

# ─── Build ───
docker compose build             # build ทุก service
docker compose build --no-cache  # build ใหม่ ไม่ใช้ cache
docker compose up --build        # build แล้วรันเลย ⭐

# ─── Status & Logs ───
docker compose ps                # ดูสถานะ
docker compose logs              # ดู logs ทั้งหมด
docker compose logs -f backend   # follow logs ของ backend
docker compose top               # ดู processes ในแต่ละ container

# ─── Execute ───
docker compose exec backend sh   # เข้าไปใน container ⭐
docker compose exec db psql -U app appdb   # เข้า PostgreSQL
docker compose run --rm backend npm test   # รันแล้วลบ container

# ─── Scale ───
docker compose up -d --scale backend=3   # รัน backend 3 ตัว!

📁 .env File — แยก Config ออกจาก YAML

.env file ช่วยแยก sensitive config (passwords, API keys) ออกจาก docker-compose.yml — ทำให้สามารถ commit compose file เข้า Git ได้โดยไม่ leak secrets เพราะ .env อยู่ใน .gitignore Docker Compose อ่าน .env อัตโนมัติ:

# .env (อยู่ข้างๆ docker-compose.yml)
POSTGRES_USER=app
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=myapp
JWT_SECRET=my-jwt-secret-key
NODE_ENV=development
APP_PORT=3000
# docker-compose.yml — ใช้ตัวแปรจาก .env
services:
  backend:
    build: ./backend
    ports:
      - "${APP_PORT}:${APP_PORT}"
    environment:
      - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - JWT_SECRET=${JWT_SECRET}
      - NODE_ENV=${NODE_ENV}
      - PORT=${APP_PORT}

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
🐕 สำคัญ! อย่าลืมใส่ .env ใน .gitignore — ไม่ commit secrets เข้า Git!

🔀 Multi-Environment — Dev vs Production

Dev environment ต้องการ hot-reload, debug tools, volume mounts สำหรับ live coding ส่วน production ต้อง optimize image size, set resource limits, ปิด debug mode จะจัดการ config ต่างๆ ยังไงให้ไม่ต้อง maintain 2 ไฟล์ที่ซ้ำกัน? Docker Compose มี 2 วิธีหลัก:

วิธีที่ 1: Override Files

Override files คือการแยก base config (docker-compose.yml) กับ environment-specific config (docker-compose.override.yml หรือ docker-compose.prod.yml) Docker Compose จะ merge ทั้ง 2 ไฟล์เข้าด้วยกัน — base กำหนด services ทั้งหมด, override เพิ่ม/แก้เฉพาะค่าที่ต่างกัน:

# docker-compose.yml (base — shared config)
services:
  backend:
    build: ./backend
    environment:
      - DATABASE_URL=postgres://app:pass@db:5432/mydb
    depends_on:
      - db
  db:
    image: postgres:16-alpine

# docker-compose.override.yml (dev — auto-loaded!)
services:
  backend:
    volumes:
      - ./backend:/app           # hot reload
    ports:
      - "8080:8080"
      - "9229:9229"              # debug port
    environment:
      - NODE_ENV=development
    command: npm run dev

# docker-compose.prod.yml (production)
services:
  backend:
    restart: always
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
# Development (auto-loads override)
docker compose up

# Production (explicit file)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

วิธีที่ 2: Profiles

Profiles (Docker Compose v2) ช่วยให้ group services ตาม use case — เช่น debug tools, monitoring stack, seeding scripts ที่ไม่ต้องรันทุกครั้ง แค่ docker compose --profile debug up จะ start เฉพาะ services ที่ tag ด้วย profile "debug":

services:
  backend:
    build: ./backend
    # ไม่มี profile = รันเสมอ

  db:
    image: postgres:16-alpine
    # ไม่มี profile = รันเสมอ

  # Dev tools — รันเฉพาะ dev
  adminer:
    image: adminer
    ports:
      - "8888:8080"
    profiles:
      - dev                      # รันเมื่อ --profile dev

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
    profiles:
      - dev

  # Monitoring — รันเฉพาะ monitoring
  prometheus:
    image: prom/prometheus
    profiles:
      - monitoring
# รันแค่ base services
docker compose up -d

# รัน + dev tools
docker compose --profile dev up -d

# รัน + monitoring
docker compose --profile monitoring up -d

# รันทั้งหมด
docker compose --profile dev --profile monitoring up -d

💾 Volumes — เก็บข้อมูลถาวร

โดย default เมื่อ container หยุด ข้อมูลข้างในหายหมด! Volumes แก้ปัญหานี้โดยเก็บข้อมูลไว้นอก container — database data, uploaded files, logs จะไม่หายแม้ docker compose down มี 2 แบบ: named volumes (Docker จัดการ path ให้ เหมาะกับ data) และ bind mounts (mount directory จาก host เข้า container เหมาะกับ development):

services:
  db:
    image: postgres:16-alpine
    volumes:
      # Named volume — Docker จัดการให้ (production)
      - pgdata:/var/lib/postgresql/data

      # Bind mount — map folder จาก host (dev)
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql

  backend:
    build: ./backend
    volumes:
      # Bind mount — hot reload สำหรับ dev
      - ./backend/src:/app/src

      # Anonymous volume — ป้องกัน overwrite
      - /app/node_modules

volumes:
  pgdata:                        # ประกาศ named volume
    driver: local
# ดู volumes
docker volume ls

# ลบ volumes ที่ไม่ใช้
docker volume prune

# Backup database
docker compose exec db pg_dump -U app mydb > backup.sql

# Restore database
cat backup.sql | docker compose exec -T db psql -U app mydb

🌐 Networks — Container คุยกันยังไง

Docker Compose สร้าง default network ให้ทุก service อัตโนมัติ — containers สื่อสารกันโดยใช้ service name เป็น hostname เช่น API container เรียก database ด้วย postgres://db:5432 ไม่ต้องใช้ IP address สำหรับ security ที่ดีขึ้น ควรสร้าง networks แยก — frontend network เฉพาะ web+nginx, backend network เฉพาะ API+DB ทำให้ frontend เข้าถึง DB โดยตรงไม่ได้:

services:
  frontend:
    networks:
      - frontend-net             # เข้าถึงได้จาก nginx

  backend:
    networks:
      - frontend-net             # nginx เข้าถึงได้
      - backend-net              # เข้าถึง db ได้

  db:
    networks:
      - backend-net              # เฉพาะ backend เข้าถึง ✅
                                 # frontend เข้าไม่ได้ ✅ (security!)

networks:
  frontend-net:
  backend-net:
💡 Security Tip: แยก network เพื่อ จำกัดการเข้าถึง — frontend ไม่ควรเข้า DB ตรงได้!

❤️ Health Checks — ตรวจสุขภาพ

Health checks ตรวจว่า service ทำงานจริง ไม่ใช่แค่ "process ยังรันอยู่" — เช่น PostgreSQL container อาจ start แล้วแต่ยังรับ connection ไม่ได้ ถ้า API start ก่อน DB พร้อม จะ crash! depends_on + condition: service_healthy แก้ปัญหานี้โดย wait จน health check ผ่านก่อนค่อย start service ที่พึ่ง:

services:
  backend:
    build: ./backend
    healthcheck:
      test: curl -f http://localhost:8080/health || exit 1
      interval: 30s              # ตรวจทุก 30 วินาที
      timeout: 10s               # timeout 10 วินาที
      retries: 3                 # ลองใหม่ 3 ครั้ง
      start_period: 40s          # รอ 40 วินาทีก่อนเริ่มตรวจ
    depends_on:
      db:
        condition: service_healthy   # รอจน db healthy ก่อน!

  db:
    image: postgres:16-alpine
    healthcheck:
      test: pg_isready -U app
      interval: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: redis-cli ping
      interval: 10s
      retries: 3

🚀 Production Tips

Docker Compose ใช้ใน production ได้สำหรับ small-medium apps (ถ้ายังไม่ต้องการ Kubernetes) แต่ต้องตั้งค่าให้ production-ready — restart policies, resource limits, logging, security hardening เคล็ดลับสำคัญ:

# docker-compose.prod.yml
services:
  backend:
    image: myregistry.com/myapp:v2.1.0   # ใช้ image จาก registry (ไม่ build)
    restart: always
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
        reservations:
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"          # จำกัดขนาด log file
        max-file: "3"            # เก็บแค่ 3 ไฟล์
    read_only: true              # filesystem read-only (security)
    tmpfs:
      - /tmp                     # ให้เขียนได้แค่ /tmp
    security_opt:
      - no-new-privileges:true

Docker Compose vs Kubernetes

คำถามที่พบบ่อย — "ควรใช้ Docker Compose หรือ Kubernetes?" คำตอบง่ายๆ: Compose สำหรับ single-host (1 server, dev environment, small apps), Kubernetes สำหรับ multi-host (หลาย servers, auto-scaling, high availability, large teams) เริ่มด้วย Compose แล้วย้ายไป K8s เมื่อ scale จนจำเป็น:

FeatureDocker ComposeKubernetes
เหมาะกับDev, small productionLarge-scale production
เครื่องเครื่องเดียวหลายเครื่อง (cluster)
Auto-scaling❌ Manual scale✅ HPA auto-scale
Self-healing⚠️ restart policy✅ Full self-healing
Rolling update⚠️ Basic✅ Zero-downtime
ความซับซ้อนง่าย (1 YAML file)ซับซ้อน (many manifests)
Learning curve30 นาที30 วัน+
💡 กฎง่ายๆ: เครื่องเดียว + ทีมเล็ก = Docker Compose | หลายเครื่อง + scale = Kubernetes

สรุป

Docker Compose เป็นเครื่องมือที่ทุก developer ควรรู้ — ทำให้การ setup development environment เหลือแค่ docker compose up ไม่ต้อง install dependencies บนเครื่อง ทุกคนในทีมได้ environment เดียวกัน "works on my machine" หมดไป:

  • Docker Compose = จัดการหลาย containers ด้วย docker-compose.yml ไฟล์เดียว
  • คำสั่งหลัก = up -d (รัน), down (หยุด), logs -f (ดู log), exec (เข้า container)
  • .env file = แยก secrets/config ออกจาก YAML (อย่า commit!)
  • Multi-env = Override files หรือ Profiles แยก dev/prod
  • Volumes = Named volumes สำหรับ data, Bind mounts สำหรับ dev
  • Networks = แยก network เพื่อ security (frontend ≠ database)
  • Health checks = ตรวจสุขภาพ + depends_on condition: service_healthy
  • Production = resource limits, logging, read-only filesystem 🐳🐕

In the Docker vs VMs post we learned that Docker makes a container easy to build — but a real app usually runs several services: web server, API, database, cache, queue...

Running docker run one at a time, wiring the network yourself, setting up the volumes yourself... you would lose your mind! 😵

Docker Compose solves exactly this — declare every service in a single YAML file, then start them all with one command! 🐳🐕


What Is Docker Compose?

Docker Compose is the tool that lets you declare and run many containers together from one docker-compose.yml file (or compose.yml).

📝 Write the YAML
declare the services
→
🚀 docker compose up
run every service
→
✅ They work together!
network + volumes ready
💡 An easy way to remember it: a Dockerfile is the recipe for 1 container | Docker Compose is the recipe for the whole restaurant (many containers)

A First Example — Web App + Database

Let's start with the simplest possible example — a web app + a PostgreSQL database. Only 2 services, but without Compose you would have to: create the network yourself, run the postgres container with its password and volume, run the app container with its env vars and attach it to the network... 5-6 commands in all! Docker Compose reduces that to docker compose up:

# docker-compose.yml
services:
  # Web Application
  app:
    build: .                     # build from the Dockerfile in this directory
    ports:
      - "3000:3000"              # host:container
    environment:
      - DATABASE_URL=postgres://myuser:mypass@db:5432/mydb
      - NODE_ENV=development
    volumes:
      - .:/app                   # mount the code in (dev mode)
      - /app/node_modules        # do not mount node_modules
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  # PostgreSQL Database
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypass
      - POSTGRES_DB=mydb
    volumes:
      - pgdata:/var/lib/postgresql/data    # persist data
    ports:
      - "5432:5432"
    healthcheck:
      test: pg_isready -U myuser
      interval: 10s
      retries: 5

volumes:
  pgdata:                        # named volume for the DB data
# run every service
docker compose up -d
# [+] Running 3/3
#  ✔ Network myapp_default  Created
#  ✔ Container myapp-db-1   Started
#  ✔ Container myapp-app-1  Started

# check the status
docker compose ps
# NAME           SERVICE   STATUS    PORTS
# myapp-app-1    app       running   0.0.0.0:3000->3000/tcp
# myapp-db-1     db        running   0.0.0.0:5432->5432/tcp

# read the logs
docker compose logs -f app      # follow the app's logs
docker compose logs              # logs from every service

# stop every service
docker compose down              # stop + remove the containers
docker compose down -v           # + remove the volumes too (⚠️ data gone!)

Full Stack — React + API + DB + Redis + Nginx

A realistic example, close to production — 5 services working together: a React frontend, a Node.js API, a PostgreSQL database, a Redis cache, and an Nginx reverse proxy. Notice how depends_on sets the start order, healthcheck proves a service is genuinely ready before anything that depends on it starts, volumes hold the persistent data, and networks separate frontend from backend traffic:

Docker Compose Stack
Nginx
:80 → frontend
/api → backend
↓ reverse proxy ↓
Frontend
React :3000
Backend
Node.js :8080
↓
PostgreSQL
:5432
Redis
:6379
# docker-compose.yml — Full Stack
services:
  # ─── Nginx Reverse Proxy ───
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - frontend
      - backend
    restart: always

  # ─── React Frontend ───
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    environment:
      - REACT_APP_API_URL=/api
    expose:
      - "3000"                   # expose it to nginx (not published to the host)

  # ─── Node.js Backend ───
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=postgres://app:secret@db:5432/appdb
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}  # read from the .env file
    expose:
      - "8080"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  # ─── PostgreSQL ───
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: pg_isready -U app
      interval: 10s
      retries: 5
    restart: unless-stopped

  # ─── Redis Cache ───
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:

Nginx Config

Nginx acts as the reverse proxy — it takes all the traffic and forwards it to the frontend or the API according to the URL path. The benefit is that you expose a single port (80) instead of opening several, and you get caching, gzip and load balancing along the way:

# nginx/default.conf
upstream frontend {
    server frontend:3000;
}

upstream backend {
    server backend:8080;
}

server {
    listen 80;

    # Frontend
    location / {
        proxy_pass http://frontend;
        proxy_set_header Host $host;
    }

    # API → Backend
    location /api/ {
        proxy_pass http://backend/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSocket support
    location /ws {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

🔧 The Docker Compose Commands That Matter

The Docker Compose commands you reach for every day — starting and stopping services, reading logs, exec-ing into a container, rebuilding images after a Dockerfile change. Note that docker compose (no hyphen) is v2, the recommended form, in place of docker-compose v1:

# ─── Lifecycle ───
docker compose up                # run (foreground)
docker compose up -d             # run (background) ⭐
docker compose down              # stop + remove the containers
docker compose down -v           # + remove the volumes
docker compose restart           # restart every service
docker compose stop              # stop (without removing)
docker compose start             # start again from stopped

# ─── Build ───
docker compose build             # build every service
docker compose build --no-cache  # rebuild, ignoring the cache
docker compose up --build        # build, then run ⭐

# ─── Status & Logs ───
docker compose ps                # status
docker compose logs              # all the logs
docker compose logs -f backend   # follow the backend's logs
docker compose top               # processes inside each container

# ─── Execute ───
docker compose exec backend sh   # get a shell in the container ⭐
docker compose exec db psql -U app appdb   # into PostgreSQL
docker compose run --rm backend npm test   # run, then remove the container

# ─── Scale ───
docker compose up -d --scale backend=3   # run 3 backends!

📁 .env Files — Config Out of the YAML

An .env file keeps the sensitive config (passwords, API keys) out of docker-compose.yml — which is what makes the compose file safe to commit to Git without leaking secrets, because .env lives in .gitignore. Docker Compose reads .env automatically:

# .env (sits next to docker-compose.yml)
POSTGRES_USER=app
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=myapp
JWT_SECRET=my-jwt-secret-key
NODE_ENV=development
APP_PORT=3000
# docker-compose.yml — using the variables from .env
services:
  backend:
    build: ./backend
    ports:
      - "${APP_PORT}:${APP_PORT}"
    environment:
      - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - JWT_SECRET=${JWT_SECRET}
      - NODE_ENV=${NODE_ENV}
      - PORT=${APP_PORT}

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
🐕 Important! Do not forget to put .env in .gitignore — never commit secrets to Git!

🔀 Multi-Environment — Dev vs Production

A dev environment wants hot reload, debug tools and volume mounts for live coding. Production wants an optimized image size, resource limits and debug mode off. So how do you manage the differing config without maintaining two files that mostly repeat each other? Docker Compose offers two main approaches:

Approach 1: Override Files

Override files split the base config (docker-compose.yml) from the environment-specific config (docker-compose.override.yml or docker-compose.prod.yml). Docker Compose merges the two files — the base declares all the services, the override adds or changes only the values that differ:

# docker-compose.yml (base — shared config)
services:
  backend:
    build: ./backend
    environment:
      - DATABASE_URL=postgres://app:pass@db:5432/mydb
    depends_on:
      - db
  db:
    image: postgres:16-alpine

# docker-compose.override.yml (dev — auto-loaded!)
services:
  backend:
    volumes:
      - ./backend:/app           # hot reload
    ports:
      - "8080:8080"
      - "9229:9229"              # debug port
    environment:
      - NODE_ENV=development
    command: npm run dev

# docker-compose.prod.yml (production)
services:
  backend:
    restart: always
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
# Development (auto-loads the override)
docker compose up

# Production (explicit files)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Approach 2: Profiles

Profiles (Docker Compose v2) let you group services by use case — debug tools, a monitoring stack, seeding scripts, anything that need not run every time. A plain docker compose --profile debug up starts only the services tagged with the "debug" profile:

services:
  backend:
    build: ./backend
    # no profile = always runs

  db:
    image: postgres:16-alpine
    # no profile = always runs

  # Dev tools — dev only
  adminer:
    image: adminer
    ports:
      - "8888:8080"
    profiles:
      - dev                      # runs with --profile dev

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
    profiles:
      - dev

  # Monitoring — monitoring only
  prometheus:
    image: prom/prometheus
    profiles:
      - monitoring
# just the base services
docker compose up -d

# base + dev tools
docker compose --profile dev up -d

# base + monitoring
docker compose --profile monitoring up -d

# everything
docker compose --profile dev --profile monitoring up -d

💾 Volumes — Data That Persists

By default, when a container stops everything inside it is gone! Volumes solve this by keeping the data outside the container — database data, uploaded files and logs all survive even a docker compose down. There are two kinds: named volumes (Docker manages the path for you, the right choice for data) and bind mounts (mount a directory from the host into the container, the right choice for development):

services:
  db:
    image: postgres:16-alpine
    volumes:
      # Named volume — Docker manages it (production)
      - pgdata:/var/lib/postgresql/data

      # Bind mount — map a folder from the host (dev)
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql

  backend:
    build: ./backend
    volumes:
      # Bind mount — hot reload for dev
      - ./backend/src:/app/src

      # Anonymous volume — keeps it from being overwritten
      - /app/node_modules

volumes:
  pgdata:                        # declare the named volume
    driver: local
# list the volumes
docker volume ls

# remove the unused volumes
docker volume prune

# Backup database
docker compose exec db pg_dump -U app mydb > backup.sql

# Restore database
cat backup.sql | docker compose exec -T db psql -U app mydb

🌐 Networks — How Containers Talk

Docker Compose creates a default network for every service automatically — containers reach each other by using the service name as the hostname. The API container talks to the database at postgres://db:5432, no IP address needed. For better security, create separate networks — a frontend network for web+nginx, a backend network for API+DB — so the frontend cannot reach the DB directly at all:

services:
  frontend:
    networks:
      - frontend-net             # reachable from nginx

  backend:
    networks:
      - frontend-net             # nginx can reach it
      - backend-net              # it can reach db

  db:
    networks:
      - backend-net              # backend only ✅
                                 # frontend cannot ✅ (security!)

networks:
  frontend-net:
  backend-net:
💡 Security Tip: split the networks to limit what can reach what — the frontend has no business touching the DB directly!

❤️ Health Checks — Taking the Pulse

Health checks prove that a service is genuinely working, not merely that "the process is still running" — a PostgreSQL container may have started and still not be accepting connections. If the API starts before the DB is ready, it crashes! depends_on plus condition: service_healthy fixes this by waiting for the health check to pass before starting anything that depends on it:

services:
  backend:
    build: ./backend
    healthcheck:
      test: curl -f http://localhost:8080/health || exit 1
      interval: 30s              # check every 30 seconds
      timeout: 10s               # 10-second timeout
      retries: 3                 # retry 3 times
      start_period: 40s          # wait 40 seconds before checking at all
    depends_on:
      db:
        condition: service_healthy   # wait until db is healthy first!

  db:
    image: postgres:16-alpine
    healthcheck:
      test: pg_isready -U app
      interval: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: redis-cli ping
      interval: 10s
      retries: 3

🚀 Production Tips

Docker Compose is usable in production for small and medium apps (when you do not yet need Kubernetes) — but it has to be configured production-ready: restart policies, resource limits, logging, security hardening. The tips that matter:

# docker-compose.prod.yml
services:
  backend:
    image: myregistry.com/myapp:v2.1.0   # use the image from the registry (no build)
    restart: always
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
        reservations:
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"          # cap the log file size
        max-file: "3"            # keep only 3 files
    read_only: true              # read-only filesystem (security)
    tmpfs:
      - /tmp                     # writable at /tmp only
    security_opt:
      - no-new-privileges:true

Docker Compose vs Kubernetes

The question that comes up constantly — "should I use Docker Compose or Kubernetes?" The simple answer: Compose is for a single host (1 server, dev environments, small apps), Kubernetes is for many hosts (several servers, auto-scaling, high availability, large teams). Start with Compose and move to K8s when the scale actually demands it:

FeatureDocker ComposeKubernetes
Best forDev, small productionLarge-scale production
MachinesA single machineMany machines (cluster)
Auto-scaling❌ Manual scale✅ HPA auto-scale
Self-healing⚠️ restart policy✅ Full self-healing
Rolling update⚠️ Basic✅ Zero-downtime
ComplexitySimple (1 YAML file)Complex (many manifests)
Learning curve30 minutes30 days+
💡 The simple rule: one machine + a small team = Docker Compose | many machines + scale = Kubernetes

Summary

Docker Compose is a tool every developer should know — it reduces setting up a development environment to docker compose up, with no dependencies installed on the machine, everyone on the team on an identical environment, and "works on my machine" gone for good:

  • Docker Compose = run many containers from one docker-compose.yml file
  • The core commands = up -d (run), down (stop), logs -f (read the logs), exec (get into a container)
  • .env files = keep secrets and config out of the YAML (do not commit them!)
  • Multi-env = override files or profiles to separate dev from prod
  • Volumes = named volumes for data, bind mounts for dev
  • Networks = split the networks for security (frontend ≠ database)
  • Health checks = take the pulse, plus depends_on condition: service_healthy
  • Production = resource limits, logging, a read-only filesystem 🐳🐕
บทความจากซีรีส์ DevOps & Vibe Coding 2026An article from the DevOps & Vibe Coding 2026 series
← Previous
Software Testing — ทดสอบยังไงให้มั่นใจก่อน Deploy
Next →
GitOps & ArgoCD — Git-driven Deployments ที่ทุกคนต้องรู้