แผนการเรียนรู้
/

สัปดาห์ที่ 5

การประกอบแอปพลิเคชันด้วย Docker Compose

เรียนรู้วิธีการจัดการแอปพลิเคชันที่มีหลาย Container เชื่อมต่อกันอย่างมีประสิทธิภาพ ลดความซับซ้อนในการรันคำสั่งด้วยมือ

หัวข้อการเรียนรู้

  • The Problem: ปัญหาของการรันหลาย Containers ด้วยมือ
  • Core Concept: Docker Compose คืออะไร? และโครงสร้างไฟล์ docker-compose.yml
  • Key Commands: การใช้งานคำสั่ง up, down, ps, และ logs
  • Resource Management: การจัดการ Services, Networks และ Volumes ใน Compose
DevOps Course: Week 5

สัปดาห์ที่ 5: Application Assembly with Docker Compose

การประกอบแอปพลิเคชันแบบ Multi-Container และการจัดการ Service Orchestration เบื้องต้น

หัวข้อการเรียนรู้

  • ปัญหาของการรัน Container หลายตัวด้วยมือ
  • Docker Compose คืออะไร?
  • โครงสร้างไฟล์ docker-compose.yml
  • การจัดการ Services, Networks และ Volumes
  • คำสั่งพื้นฐาน (up, down, ps, logs)
DevOps Course: Week 5

ทบทวน: สิ่งที่เรียนไปเมื่อสัปดาห์ที่แล้ว

git clone -b wk04 https://github.com/wichitpaulsombat/djrepo

Docker Image

แม่พิมพ์ (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 .

Docker Volume

พื้นที่เก็บข้อมูลถาวร (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

Docker Network

ช่องทางสื่อสารระหว่าง 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

วันนี้เราจะนำองค์ประกอบเหล่านี้มารวมร่างกัน!

สถานการณ์จริง: Modern Applications

แอปพลิเคชันสมัยใหม่ ไม่ได้มีแค่โปรแกรมเดียวโดดๆ มักประกอบด้วย:

🖥️ Frontend (React/Vue)
+
⚙️ Backend (Django/Node)
+
🗄️ Database (Postgres/MySQL)
+
Cache (Redis)

ปัญหาของการรันด้วยมือ (The Pain of Manual Run)

ถ้าต้องรันแอปฯ ข้างต้น คุณต้องพิมพ์คำสั่งแบบนี้ทุกครั้ง:

# 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

ทำไมวิธี Manual ถึงไม่เวิร์ค?

❌ Human Error

จำ Flag ผิด, ลืมใส่ Env Vars, ลืม map volume ทำให้ข้อมูลหาย

❌ Dependency Hell

ลำดับการรันสำคัญมาก (Backend ห้ามรันก่อน Database) การจัดการลำดับด้วยมือทำได้ยาก

❌ Configuration Drift

Developer A รันด้วยคำสั่งนึง แต่ Developer B รันอีกคำสั่งนึง ทำให้ผลลัพธ์ไม่เหมือนกัน ("It works on my machine")

❌ Cleanup ยาก

ต้องมานั่งไล่ docker stop และ docker rm ทีละตัว

ทางออก: Infrastructure as Code

จะดีกว่าไหม? ถ้าเราเขียน "Configuration" ของการรัน Container ทั้งหมดลงในไฟล์เดียว

📄
🐳 🐳 🐳

นี่คือหน้าที่ของ Docker Compose

Docker Compose คืออะไร?

เครื่องมือสำหรับ นิยาม (Define) และ รัน (Run) แอปพลิเคชัน Docker แบบ Multi-container

  • ใช้ไฟล์ YAML เพื่อกำหนดค่า Services, Networks และ Volumes
  • สั่งงานแอปพลิเคชันทั้งหมดด้วยคำสั่งเดียว
  • เหมาะอย่างยิ่งสำหรับ: Local Development, Automated Testing, และ Staging Environments
*อ้างอิง: Docker Documentation [1], Wichit2s Docs [2]

โครงสร้างไฟล์ docker-compose.yml

ประกอบด้วย 3 ส่วนหลัก (Top-level elements)

# docker-compose.yml
services:
  web:
    image: nginx
  db:
    image: postgres

volumes:
  db-data:

networks:
  my-net:
1. Services: หัวใจหลัก ระบุ Container ที่จะรัน (Image, Port, Command)
2. Volumes: ระบุ Named Volumes ที่ใช้เก็บข้อมูล
3. Networks: ระบุการเชื่อมต่อที่ซับซ้อน (ปกติไม่ต้องใส่ก็ได้ เพราะ Compose สร้าง Default ให้)

เจาะลึก: Services

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 อัตโนมัติ

แหล่งที่มา: Build vs Image

image:

ใช้เมื่อต้องการดึง Image สำเร็จรูปจาก Registry (เช่น Docker Hub) หรือ Image ที่ Build ไว้แล้ว

image: redis:alpine

build:

ใช้เมื่อต้องการสร้าง Image จาก Source Code (Dockerfile) ในเครื่องเรา

build:
  context: .
  dockerfile: Dockerfile.dev

การจัดการตัวแปรสภาพแวดล้อม (Environment Variables)

สามารถทำได้ 2 วิธีหลักใน Compose

1. กำหนดโดยตรง (environment)

environment:
  - DEBUG=True
  - DB_HOST=postgres

2. ใช้ไฟล์ .env (env_file) - แนะนำ!

env_file:
  - .env

*เก็บค่าความลับ (Secrets) ไว้ใน .env และอย่าลืมเพิ่มใน .gitignore

Networking ใน Docker Compose

Default Behavior

เมื่อรัน docker-compose up Docker จะทำสิ่งเหล่านี้ให้อัตโนมัติ:

  1. สร้าง Network ใหม่ชื่อ [project_name]_default
  2. นำ Container ทุกตัวในไฟล์ไปใส่ใน Network นี้
  3. Container สามารถคุยกันได้ผ่าน Service Name
ไม่ต้องสั่ง docker network create เองอีกต่อไป!

Volumes ใน Docker Compose

กำหนดพื้นที่เก็บข้อมูลได้ 2 รูปแบบหลัก

1. Host Bind Mount

เชื่อมโฟลเดอร์เครื่องเราเข้า Container (เหมาะสำหรับ Dev)

volumes:
  - ./src:/app/src

2. Named Volume

ให้ Docker จัดการพื้นที่ (เหมาะสำหรับ Database)

volumes:
  - pgdata:/var/lib/postgresql/data

*ถ้าใช้ Named Volume ต้องประกาศที่ top-level volumes key ด้วย

การประกาศ Volumes (Top-Level)

ถ้าใช้ Named Volume ต้องประกาศบอก Compose เสมอ

Important

services:

  db:

    image: postgres

    volumes:

      - pgdata:/var/lib/postgresql/data


volumes:

  pgdata: # ต้องประกาศตรงนี้ด้วย!

ลำดับการทำงาน (Dependencies)

ใช้ depends_on เพื่อบอกว่า Service ไหนต้องเริ่มก่อน

services:

  web:

    build: .

    depends_on:

      - db

      - redis


  db: ...

  redis: ...

⚠️ ข้อควรระวัง: depends_on แค่รอให้ Container "Start" แต่ไม่ได้รอให้ Database "Ready" (พร้อมรับ Connection) แอปพลิเคชันยังต้องมี Logic ในการ Retry การเชื่อมต่อเอง

Port Mapping ใน Compose

การเปิดประตูสู่โลกภายนอก (Host Machine)

ports:

- "HOST_PORT:CONTAINER_PORT"

ports:

- "8000:80"

เข้าผ่าน localhost:8000 (Host) จะทะลุไป Port 80 (Container)

expose:

- "5432"

เปิด Port 5432 เฉพาะให้ Container อื่นใน Network เดียวกันคุยได้ (คนนอกเข้าไม่ได้)

Restart Policies

จะทำอย่างไรถ้า Container พัง (Crash) หรือเครื่อง Reboot?

no ไม่ restart เลย (Default)
always Restart เสมอ ถ้าหยุดทำงาน (แม้จะสั่ง stop ไปแล้ว พอเปิด docker ใหม่ก็จะรันเอง)
on-failure Restart เฉพาะเมื่อโปรแกรม Error (Exit code != 0)
unless-stopped Restart เสมอ ยกเว้นเราสั่ง stop ไว้

ตัวอย่างไฟล์ที่สมบูรณ์

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:

The Magic Command: UP

สร้าง, เริ่ม, และเชื่อมต่อทุกอย่างในคำสั่งเดียว

$ docker-compose up

-d (Detached): รันแบบ Background (คืนค่า Terminal ให้เรา) $ docker-compose up -d
--build: บังคับให้ Build Image ใหม่ (ถ้ามีการแก้ Dockerfile) $ docker-compose up -d --build

Command: DOWN

หยุดและล้างบาง (Cleanup)

$ docker-compose down

  • หยุด Container
  • ลบ Container
  • ลบ Default Network ที่สร้างขึ้น
⚠️ Option สำคัญ: -v (Volumes)

$ docker-compose down -v จะลบ Named Volumes ด้วย (ข้อมูลหายหมด!)

Monitoring Commands

docker-compose ps

ดูสถานะของ Container ในโปรเจกต์ปัจจุบันเท่านั้น

Name        State   Ports
myapp_web_1 Up      0.0.0.0:80->80/tcp
myapp_db_1  Up      5432/tcp

docker-compose logs

ดู Output จากทุก Container รวมกัน

  • -f : Follow (ดูแบบ Real-time)
  • web : ดูเฉพาะ service ชื่อ web
$ docker-compose logs -f web

Executing Commands

การเข้าไปสั่งงานภายใน Container

docker-compose exec

รันคำสั่งใน Container ที่ กำลังทำงานอยู่ (Running)

# เข้าไปใน shell ของ db

$ docker-compose exec db psql -U user

docker-compose run

สร้าง Container ตัวใหม่ขึ้นมาเพื่อรันคำสั่ง (One-off task)

# รัน migration แล้วจบไป

$ docker-compose run web python manage.py migrate

Lifecycle Control

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 เปลี่ยน)

Scaling Services

เพิ่มจำนวน Container ของ Service ได้ง่ายๆ (แต่ต้องระวังเรื่อง Port Choc!)

$ docker-compose up -d --scale web=3

⚠️ ข้อควรระวังเรื่อง Port

ถ้าคุณ map port แบบ "8000:80" คุณจะ Scale ไม่ได้ เพราะ Port 8000 บน Host มีได้แค่หนึ่งเดียว

ทางแก้: ใช้ Load Balancer (เช่น Nginx) อยู่หน้าสุด หรือให้ Docker สุ่ม Port (ระบุแค่ target port)

Lab: Docker Compose in Action

ภารกิจวันนี้

  1. แปลงคำสั่ง docker run (จาก Lab ที่แล้ว) มาเป็น docker-compose.yml
  2. ประกอบร่าง Django + PostgreSQL เข้าด้วยกัน
  3. ทดสอบการ Persistence ข้อมูล (ลบ Container ข้อมูลต้องอยู่)
  4. ใช้ docker-compose.override.yml สำหรับ Dev Environment (Optional)

โครงสร้างโปรเจกต์ (Project Structure)

จัดระเบียบไฟล์ให้ถูกต้องก่อนเริ่มงาน

my-project/

├── docker-compose.yml

├── .env             # เก็บ Secrets

├── .gitignore

├── backend/

│   ├── Dockerfile

│   ├── requirements.txt

│   └── src/

└── frontend/

    ├── Dockerfile

    └── package.json

สรุปประโยชน์ของ Docker Compose

✅ Single Command

สร้าง, เริ่ม, หยุด ทุกอย่างด้วยคำสั่งเดียว

✅ Isolated Environment

สร้างสภาพแวดล้อมที่แยกขาดจากกันได้ง่าย (Project Name)

✅ Preservation

เก็บรักษา Volume data เมื่อ Container ถูกสร้างใหม่

✅ Portable

แค่มีไฟล์ docker-compose.yml ก็รันที่ไหนก็ได้

Compose vs Orchestrators (K8s/Swarm)

Compose ดีมาก แต่ไม่ใช่คำตอบของทุกสิ่ง

Docker Compose

  • ✅ Single Host (เครื่องเดียว)
  • ✅ Local Development
  • ✅ Testing / CI Pipelines
  • ❌ ไม่รองรับ Auto-scaling ข้ามเครื่อง
  • ❌ ไม่มี Self-healing ขั้นสูง

Kubernetes / Swarm

  • ✅ Multi-Host (Cluster)
  • ✅ Production Grade
  • ✅ High Availability / Scaling
  • ✅ Zero-downtime Deployment
  • ⚠️ ความซับซ้อนสูงกว่ามาก
💬

Q & A

มีคำถามสงสัยตรงไหนก่อนเริ่ม Lab ไหมครับ?

Let's Start the Lab! 🚀

Lab Project: Resume Hub (Full Stack)

เปลี่ยนจาก Monolith เป็น Modern Architecture แยก Frontend และ Backend ออกจากกัน

⚛️

Frontend

React + Vite + Tailwind

🐍

Backend

Django REST + uv

🚀

Cache & DB

Redis + PostgreSQL

🌐

Gateway

Caddy Web Server

🎯 Goal: สร้างระบบ Resume Platform ตาม SRS โดยใช้ Caddy เป็น Load Balancer และ Reverse Proxy

System Architecture

💻

User Browser

Caddy

Reverse Proxy / LB

Port 80

Static Files
React Build (Dist)
Backend API (x3)
Django (Scale)
⚙️
🐘 Postgres
🔥 Redis

Step 1: Backend Setup (Django API)

ใช้ 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',
  }
}

Step 2: สร้าง API Views (REST Framework)

# 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 ...
        

Step 4: Backend Dockerfile (with uv)

เราจะใช้ 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"]

Step 5: Frontend Initialization

สร้างโปรเจกต์ 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

Step 6: ตั้งค่า TailwindCSS

1. แก้ไข vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})
        
2. แก้ไข src/index.css
@import "tailwindcss";
        

Step 7: สร้าง Component แสดง Resume

ไฟล์: 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
      

Step 8: Frontend Dockerfile (Build Only)

เราจะใช้ 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"]

Step 9: Caddyfile Setup

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

}

}

Step 10: Docker Compose (The Grand Assembly)

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:
      

Step 11: Execute & Scale

สั่งรันระบบทั้งหมดด้วยคำสั่งเดียว

$ docker-compose up -d --build

สิ่งที่เกิดขึ้น:

  1. Postgres & Redis เริ่มทำงาน
  2. Backend 3 ตัวเริ่มทำงานพร้อมกัน
  3. Frontend Build สร้างไฟล์ Static ไปใส่ Volume
  4. Caddy เริ่มทำงานและ Mount Volume

การตรวจสอบ:

$ docker-compose ps

คุณจะเห็น backend-1, backend-2, backend-3 รันอยู่

Step 12: Testing Load Balancing

พิสูจน์ว่า Caddy กระจายงานไปยัง Backend ทั้ง 3 ตัว

# ดู Log ของ Backend แบบ Real-time

$ docker-compose logs -f backend

เปิด Browser เข้าเว็บ http://localhost แล้ว Refresh หน้าเว็บหลายๆ ครั้ง

สังเกตที่ Terminal: คุณจะเห็น Log สลับไปมาระหว่าง:

  • backend_1 | GET /api/resumes/ 200 OK
  • backend_2 | GET /api/resumes/ 200 OK
  • backend_3 | GET /api/resumes/ 200 OK

Step 13: ทดสอบ Redis Cache

ตรวจสอบว่าข้อมูลถูกเก็บใน 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 ทำงานถูกต้อง

Bonus: การเติมข้อมูล (Data Seeding)

เนื่องจาก Database เริ่มต้นว่างเปล่า หน้าเว็บจะโล่ง

วิธีเพิ่มข้อมูลผ่าน Django Admin:

  1. สร้าง Superuser: $ docker-compose exec backend python manage.py createsuperuser
  2. สร้าง Resumes: $ docker-compose exec backend python manage.py generate_resumes --number 100
  3. เข้า http://localhost/admin (Caddy จะ Route ไป Backend ให้ถ้า Config ถูกต้อง หรืออาจต้องเพิ่ม /admin ใน Caddyfile)
  4. เพิ่มข้อมูล Resume, Skills ผ่านหน้า Admin
  5. กลับมาหน้าแรก http://localhost เพื่อดูผลลัพธ์

*หมายเหตุ: ใน Caddyfile ที่ให้ไป เรา map แค่ /api/* ถ้าจะเข้า admin ต้องเพิ่ม `handle /admin/* { reverse_proxy backend:8000 }` ด้วย

Troubleshooting Lab

502 Bad Gateway (Caddy)

สาเหตุ: Caddy เริ่มก่อน Backend หรือ Backend พัง

แก้: รอสักพัก หรือเช็ค logs backend (`docker-compose logs backend`)

Frontend จอขาว / ไม่โหลด

สาเหตุ: Vite Build ไม่ผ่าน หรือ Volume mount ไม่ตรง

แก้: เช็ค logs frontend-build และดูว่าใน container caddy มีไฟล์ใน /srv ไหม

CORS Error

สาเหตุ: Django ปฏิเสธ Request จาก Frontend

แก้: เช็ค `CORS_ALLOW_ALL_ORIGINS = True` ใน settings.py

การส่งงาน (Project Submission)

Resume Hub Full-Stack

  • GitHub Repository: อัปโหลดโค้ดทั้งหมด (backend, frontend, docker-compose.yml, Caddyfile)
  • Screenshots:
    • หน้าเว็บที่แสดงข้อมูล Resume จาก Database
    • หน้าจอ `docker-compose ps` แสดง Services ทั้งหมด
    • หน้าจอ `docker-compose logs backend` แสดงการกระจาย Load
  • Demo Video (Optional): คลิปสั้นๆ แสดงการทำงาน

Mission Complete! 🚀 คุณได้สร้างระบบ Microservices-ready Architecture แล้ว