เรียนรู้วิธีการจัดการแอปพลิเคชันที่มีหลาย Container เชื่อมต่อกันอย่างมีประสิทธิภาพ ลดความซับซ้อนในการรันคำสั่งด้วยมือ
docker-compose.ymlup, down, ps, และ logsการประกอบแอปพลิเคชันแบบ Multi-Container และการจัดการ Service Orchestration เบื้องต้น
docker-compose.ymlgit clone -b wk04 https://github.com/wichitpaulsombat/djrepo
แม่พิมพ์ (Blueprint) ที่ Read-only ใช้สร้าง Container
FROM python:3.13-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY . .
EXPOSE 8000
CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
docker docker build -t djrepo:0.04.1 .
พื้นที่เก็บข้อมูลถาวร (Persistent Data) ที่แยกจาก Lifecycle ของ Container
docker volume create djrepo-media
docker run -v djrepo-media:/app/media djrepo:0.04.1
docker run --mount type=volume,src=djrepo-media,dst=/app/media djrepo:0.04.1
docker run --mount type=bind,src=$(pwd),dst=/app djrepo:0.04.1
ช่องทางสื่อสารระหว่าง Container ด้วย DNS Name
docker network create djrepo-network
docker run -d --name db --network djrepo-net postgres:15
docker run --network djrepo-net --mount type=bind,src=$(pwd),dst=/app djrepo:0.04.1
วันนี้เราจะนำองค์ประกอบเหล่านี้มารวมร่างกัน!
แอปพลิเคชันสมัยใหม่ ไม่ได้มีแค่โปรแกรมเดียวโดดๆ มักประกอบด้วย:
ถ้าต้องรันแอปฯ ข้างต้น คุณต้องพิมพ์คำสั่งแบบนี้ทุกครั้ง:
# 1. Create Network
$ docker network create my-net
# 2. Run Database
$ docker run -d --name db --net my-net -v db-data:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret postgres
# 3. Run Backend (ต้องรอ DB start เสร็จก่อนไหม?)
$ docker run -d --name backend --net my-net -e DB_HOST=db my-backend:v1
# 4. Run Frontend
$ docker run -d --name frontend --net my-net -p 80:80 my-frontend:v1
จำ Flag ผิด, ลืมใส่ Env Vars, ลืม map volume ทำให้ข้อมูลหาย
ลำดับการรันสำคัญมาก (Backend ห้ามรันก่อน Database) การจัดการลำดับด้วยมือทำได้ยาก
Developer A รันด้วยคำสั่งนึง แต่ Developer B รันอีกคำสั่งนึง ทำให้ผลลัพธ์ไม่เหมือนกัน ("It works on my machine")
ต้องมานั่งไล่ docker stop และ docker rm ทีละตัว
จะดีกว่าไหม? ถ้าเราเขียน "Configuration" ของการรัน Container ทั้งหมดลงในไฟล์เดียว
นี่คือหน้าที่ของ Docker Compose
เครื่องมือสำหรับ นิยาม (Define) และ รัน (Run) แอปพลิเคชัน Docker แบบ Multi-container
YAML เพื่อกำหนดค่า Services, Networks และ Volumesประกอบด้วย 3 ส่วนหลัก (Top-level elements)
Service คือคำนิยามของ Container ใน Compose
services:
frontend: # ชื่อ Service (ใช้เป็น DNS Name ได้เลย)
build: ./frontend
ports:
- "3000:3000"
database:
image: postgres:15
environment:
- POSTGRES_PASSWORD=secret
💡 Tip: ชื่อ Service (frontend, database) จะกลายเป็น Hostname ใน Network อัตโนมัติ
ใช้เมื่อต้องการดึง Image สำเร็จรูปจาก Registry (เช่น Docker Hub) หรือ Image ที่ Build ไว้แล้ว
image: redis:alpine
ใช้เมื่อต้องการสร้าง Image จาก Source Code (Dockerfile) ในเครื่องเรา
build:
context: .
dockerfile: Dockerfile.dev
สามารถทำได้ 2 วิธีหลักใน Compose
environment: - DEBUG=True - DB_HOST=postgres
env_file: - .env
*เก็บค่าความลับ (Secrets) ไว้ใน .env และอย่าลืมเพิ่มใน .gitignore
เมื่อรัน docker-compose up Docker จะทำสิ่งเหล่านี้ให้อัตโนมัติ:
[project_name]_defaultdocker network create เองอีกต่อไป!
กำหนดพื้นที่เก็บข้อมูลได้ 2 รูปแบบหลัก
เชื่อมโฟลเดอร์เครื่องเราเข้า Container (เหมาะสำหรับ Dev)
volumes:
- ./src:/app/src
ให้ Docker จัดการพื้นที่ (เหมาะสำหรับ Database)
volumes:
- pgdata:/var/lib/postgresql/data
*ถ้าใช้ Named Volume ต้องประกาศที่ top-level volumes key ด้วย
ถ้าใช้ Named Volume ต้องประกาศบอก Compose เสมอ
services:
db:
image: postgres
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: # ต้องประกาศตรงนี้ด้วย!
ใช้ depends_on เพื่อบอกว่า Service ไหนต้องเริ่มก่อน
services:
web:
build: .
depends_on:
- db
- redis
db: ...
redis: ...
depends_on แค่รอให้ Container "Start" แต่ไม่ได้รอให้ Database "Ready" (พร้อมรับ Connection) แอปพลิเคชันยังต้องมี Logic ในการ Retry การเชื่อมต่อเอง
การเปิดประตูสู่โลกภายนอก (Host Machine)
ports:
- "HOST_PORT:CONTAINER_PORT"
ports:
- "8000:80"
เข้าผ่าน localhost:8000 (Host) จะทะลุไป Port 80 (Container)
expose:
- "5432"
เปิด Port 5432 เฉพาะให้ Container อื่นใน Network เดียวกันคุยได้ (คนนอกเข้าไม่ได้)
จะทำอย่างไรถ้า Container พัง (Crash) หรือเครื่อง Reboot?
services: django_app: build: . command: uv run python manage.py runserver 0.0.0.0:8000 volumes: - .:/app ports: - "8000:8000" depends_on: - db environment: - DB_HOST=db - DB_NAME=app_db - DB_USER=user - DB_PASS=pass db: image: postgres:13 volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_DB=app_db - POSTGRES_USER=user - POSTGRES_PASSWORD=pass volumes: postgres_data:
สร้าง, เริ่ม, และเชื่อมต่อทุกอย่างในคำสั่งเดียว
$ docker-compose up
$ docker-compose up -d
$ docker-compose up -d --build
หยุดและล้างบาง (Cleanup)
$ docker-compose down
-v (Volumes)
$ docker-compose down -v จะลบ Named Volumes ด้วย (ข้อมูลหายหมด!)
ดูสถานะของ Container ในโปรเจกต์ปัจจุบันเท่านั้น
ดู Output จากทุก Container รวมกัน
-f : Follow (ดูแบบ Real-time)web : ดูเฉพาะ service ชื่อ web$ docker-compose logs -f web
การเข้าไปสั่งงานภายใน Container
รันคำสั่งใน Container ที่ กำลังทำงานอยู่ (Running)
# เข้าไปใน shell ของ db
$ docker-compose exec db psql -U user
สร้าง Container ตัวใหม่ขึ้นมาเพื่อรันคำสั่ง (One-off task)
# รัน migration แล้วจบไป
$ docker-compose run web python manage.py migrate
stop
หยุด Container แต่ไม่ลบ (Data ใน Mem หาย, FS ยังอยู่)
start
เริ่ม Container ที่หยุดไปแล้วขึ้นมาใหม่
restart
Stop แล้ว Start ใหม่ (ใช้เมื่อแก้ Config หรือ App crash)
💡 สรุป Lifecycle:
Create -> Start -> [Running] -> Stop -> [Stopped] -> rm -> [Deleted]
docker-compose up = Create + Start (ถ้ายังไม่มี) หรือ Restart (ถ้า Config เปลี่ยน)
เพิ่มจำนวน Container ของ Service ได้ง่ายๆ (แต่ต้องระวังเรื่อง Port Choc!)
$ docker-compose up -d --scale web=3
ถ้าคุณ map port แบบ "8000:80" คุณจะ Scale ไม่ได้ เพราะ Port 8000 บน Host มีได้แค่หนึ่งเดียว
ทางแก้: ใช้ Load Balancer (เช่น Nginx) อยู่หน้าสุด หรือให้ Docker สุ่ม Port (ระบุแค่ target port)
docker run (จาก Lab ที่แล้ว) มาเป็น docker-compose.ymldocker-compose.override.yml สำหรับ Dev Environment (Optional)จัดระเบียบไฟล์ให้ถูกต้องก่อนเริ่มงาน
my-project/
├── docker-compose.yml
├── .env # เก็บ Secrets
├── .gitignore
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── src/
└── frontend/
├── Dockerfile
└── package.json
สร้าง, เริ่ม, หยุด ทุกอย่างด้วยคำสั่งเดียว
สร้างสภาพแวดล้อมที่แยกขาดจากกันได้ง่าย (Project Name)
เก็บรักษา Volume data เมื่อ Container ถูกสร้างใหม่
แค่มีไฟล์ docker-compose.yml ก็รันที่ไหนก็ได้
Compose ดีมาก แต่ไม่ใช่คำตอบของทุกสิ่ง
คู่มือฉบับสมบูรณ์จาก Docker Official
ตัวอย่าง Compose files สำหรับ Tech Stack ต่างๆ (React, Django, Go, etc.)
DevOps — wichit2s documentation: Week 5
มีคำถามสงสัยตรงไหนก่อนเริ่ม Lab ไหมครับ?
เปลี่ยนจาก Monolith เป็น Modern Architecture แยก Frontend และ Backend ออกจากกัน
React + Vite + Tailwind
Django REST + uv
Redis + PostgreSQL
Caddy Web Server
User Browser
Reverse Proxy / LB
Port 80
ใช้ Code เดิมจาก djrepo แต่ปรับให้เป็น API Mode
# 1. ย้ายไฟล์ทั้งหมดไปไว้ที่ backend
$ mkdir backend
$ mv * backend
# 2. ติดตั้ง Library เพิ่มเติมใน requirements.txt
djangorestframework
django-cors-headers
django-redis
psycopg2-binary
gunicorn
# 2. แก้ไข djrepo/settings.py
INSTALLED_APPS = [..., 'rest_framework', 'corsheaders', 'main']
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...] + MIDDLEWARE
CORS_ALLOW_ALL_ORIGINS = True # สำหรับ Lab เท่านั้น
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://redis:6379/1',
}
}
# main/serializers.py
from rest_framework import serializers
from .models import *
class SkillSerializer(serializers.ModelSerializer):
class Meta:
model = Skill
fields = ['name', 'level']
class ResumeSerializer(serializers.ModelSerializer):
skills = SkillSerializer(many=True, read_only=True)
avg_rating = serializers.ReadOnlyField()
class Meta:
model = Resume
fields = '__all__'
# main/views.py
from rest_framework import viewsets
from .models import *
from .serializers import *
class ResumeViewSet(viewsets.ModelViewSet):
queryset = Resume.objects.all()
serializer_class = ResumeSerializer
# Example Redis Caching logic could go here
# def list(self, request):
# ... check cache ...
เราจะใช้ uv เพื่อความเร็วในการ Build และ gunicorn สำหรับ Production
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Collect static files (optional for API, but good practice)
RUN python manage.py collectstatic --noinput
# Run with Gunicorn
CMD ["gunicorn", "mysite.wsgi:application", "--bind", "0.0.0.0:8000"]
สร้างโปรเจกต์ React ใหม่แยกจากโฟลเดอร์ Backend
# ออกจากโฟลเดอร์ backend ไปที่ root ของ project
$ cd ..
# สร้างโปรเจกต์ Vite ชื่อ 'frontend'
$ npm create vite@latest frontend -- --template react
$ cd frontend
# ติดตั้ง tailwindcss https://tailwindcss.com/docs/installation/using-vite
$ npm run dev
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})
@import "tailwindcss";
ไฟล์: src/App.jsx (เรียกข้อมูลจาก Backend)
import { useEffect, useState } from 'react'
function App() {
const [resumes, setResumes] = useState([])
useEffect(() => {
// Fetch ผ่าน /api ซึ่ง Caddy จะ Route ไปหา Backend ให้
fetch('/api/resumes/')
.then(res => res.json())
.then(data => setResumes(data))
}, [])
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6 text-center text-blue-600">Resume Hub</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{resumes.map(resume => (
<div key={resume.id} className="bg-white p-6 rounded-lg shadow-lg hover:shadow-xl transition">
<h2 className="text-xl font-bold">{resume.title}</h2>
<p className="text-gray-600">{resume.summary}</p>
<div className="mt-4 flex flex-wrap gap-2">
{resume.skills.map(skill => (
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">{skill.name}</span>
))}
</div>
</div>
))}
</div>
</div>
)
}
export default App
เราจะใช้ Docker เพื่อ Build ไฟล์ static (html, css, js) แล้วส่งให้ Caddy เสิร์ฟ
# frontend/Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ใน Production จริง เราอาจใช้ Container นี้แค่ Copy file ออกมา
# หรือใช้ Multi-stage เพื่อรัน Nginx/Caddy ในตัวก็ได้
# แต่ใน Lab นี้เราจะใช้ Caddy หลักตัวเดียวในการ mount volume
CMD ["echo", "Build Complete"]
Caddy จะทำหน้าที่เป็น Web Server (เสิร์ฟ Frontend) และ Reverse Proxy (ส่ง Request ไป Backend)
# Caddyfile (สร้างไว้ที่ root ของโปรเจกต์)
:80 {
# Serve Static Files (Frontend)
root * /srv
file_server
try_files {path} /index.html
# Proxy API requests to Backend
handle /api/* {
reverse_proxy backend:8000
}
}
services: backend: build: ./backend command: gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 depends_on: - db - redis environment: - DB_HOST=db - DB_PASSWORD=secret deploy: replicas: 3 # Scaling Backend to 3 instances! frontend-build: build: ./frontend volumes: - frontend_dist:/app/dist command: sh -c "npm run build && cp -r dist/* /shared/" # Copy build to volume volumes: - static_volume:/shared caddy: image: caddy:2-alpine restart: unless-stopped ports: - "80:80" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - static_volume:/srv # Serve frontend from volume depends_on: - backend - frontend-build db: image: postgres:15-alpine environment: - POSTGRES_PASSWORD=secret volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:alpine volumes: pgdata: static_volume:
สั่งรันระบบทั้งหมดด้วยคำสั่งเดียว
$ docker-compose up -d --build
$ docker-compose ps
คุณจะเห็น backend-1, backend-2, backend-3 รันอยู่
พิสูจน์ว่า Caddy กระจายงานไปยัง Backend ทั้ง 3 ตัว
# ดู Log ของ Backend แบบ Real-time
$ docker-compose logs -f backend
เปิด Browser เข้าเว็บ http://localhost แล้ว Refresh หน้าเว็บหลายๆ ครั้ง
สังเกตที่ Terminal: คุณจะเห็น Log สลับไปมาระหว่าง:
ตรวจสอบว่าข้อมูลถูกเก็บใน Redis จริงหรือไม่
# 1. เข้าไปใน Redis Container
$ docker-compose exec redis redis-cli
# 2. ดู Keys ทั้งหมด
127.0.0.1:6379> KEYS *
1) ":1:views.decorators.cache.cache_header..."
2) ":1:views.decorators.cache.cache_page..."
# 3. ถ้าเจอ Keys แสดงว่า Django Caching ทำงานถูกต้อง
เนื่องจาก Database เริ่มต้นว่างเปล่า หน้าเว็บจะโล่ง
$ docker-compose exec backend python manage.py createsuperuser$ docker-compose exec backend python manage.py generate_resumes --number 100http://localhost/admin (Caddy จะ Route ไป Backend ให้ถ้า Config ถูกต้อง หรืออาจต้องเพิ่ม /admin ใน Caddyfile)http://localhost เพื่อดูผลลัพธ์*หมายเหตุ: ใน Caddyfile ที่ให้ไป เรา map แค่ /api/* ถ้าจะเข้า admin ต้องเพิ่ม `handle /admin/* { reverse_proxy backend:8000 }` ด้วย
สาเหตุ: Caddy เริ่มก่อน Backend หรือ Backend พัง
แก้: รอสักพัก หรือเช็ค logs backend (`docker-compose logs backend`)
สาเหตุ: Vite Build ไม่ผ่าน หรือ Volume mount ไม่ตรง
แก้: เช็ค logs frontend-build และดูว่าใน container caddy มีไฟล์ใน /srv ไหม
สาเหตุ: Django ปฏิเสธ Request จาก Frontend
แก้: เช็ค `CORS_ALLOW_ALL_ORIGINS = True` ใน settings.py
Mission Complete! 🚀 คุณได้สร้างระบบ Microservices-ready Architecture แล้ว