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

สัปดาห์ที่ 4

การจัดการ Docker Images และ Volumes

เจาะลึกการทำงานของ Image Layers, การฝากไฟล์ไว้บน Registry และการเก็บข้อมูลให้อยู่ถาวร

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

  • Image Anatomy: เข้าใจเรื่อง Layers และ Caching
  • Registry: การใช้งาน Docker Hub (Tag & Push)
  • Data Persistence: การใช้ Volumes และ Bind Mounts
  • Networking Basics: การเชื่อมต่อระหว่าง Containers
DevOps Course: Week 4

ทบทวน: Image vs Container

ก่อนจะไปต่อ ต้องแม่นเรื่องข้อแตกต่าง

💿

Docker Image

"แม่พิมพ์" หรือ "ต้นแบบ"

  • Read-only (แก้ไขไม่ได้)
  • ประกอบด้วย Code, Libs, OS Filesystem
  • สร้างจาก Dockerfile
📦

Docker Container

"สิ่งมีชีวิต" ที่รันอยู่จริง

  • Read-Write Layer (เขียนทับได้ชั่วคราว)
  • เป็น Instance ของ Image
  • สร้าง ลบ หรือเริ่มใหม่ได้ตลอดเวลา

เบื้องหลัง Image

Union File System

ทำไม Docker Image ถึงมีขนาดเล็กและสร้างได้เร็ว? คำตอบคือ UnionFS

หลักการทำงาน

  • 🥞 Layering: Docker มองไฟล์ระบบเป็น "ชั้นๆ" ซ้อนทับกัน
  • 🔍 Unified View: เวลาเรามองเข้าไปใน Container เราจะเห็นไฟล์ทั้งหมดรวมกันเสมือนเป็นไฟล์ระบบเดียว
  • ♻️ Copy-on-Write (CoW): ถ้าจะแก้ไขไฟล์ที่อยู่ในชั้นล่าง Docker จะไม่แก้ที่เดิม แต่จะ "Copy" ไฟล์นั้นขึ้นมาที่ชั้นบนสุดแล้วค่อยแก้ (ประหยัดพื้นที่)

โครงสร้างของ Image Layers

ทุกคำสั่งใน Dockerfile สร้าง Layer ใหม่

# Dockerfile

FROM ubuntu:20.04

COPY . /app

RUN make /app

CMD python app.py

Container Layer (Read/Write) Writable
Layer 3: CMD python app.py
Layer 2: RUN make /app
Layer 1: COPY . /app
Base Image: Ubuntu 20.04

💡 ในกรณีที่หลาย App ใช้ Base Image อันเดียวกัน จะโหลด Base Image แค่ครั้งเดียวและแชร์กัน (ประหยัด Disk)

Layer Caching: หัวใจของความเร็ว

Docker จะจำผลลัพธ์ของแต่ละ Layer ไว้ ถ้า Input ไม่เปลี่ยน Docker จะใช้ "ของเดิม" (Cache) แทนการสร้างใหม่

❌ แบบที่ช้า (Bad Practice)

COPY . .
RUN pip install -r requirements.txt

ถ้าแก้ Code แค่บรรทัดเดียว Layer COPY . . จะเปลี่ยน ทำให้ Layer ถัดไป (pip install) ต้องรันใหม่ทั้งหมด ทั้งที่ dependency ไม่เปลี่ยน

✅ แบบที่เร็ว (Good Practice)

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

แยก Copy dependency มาก่อน ถ้าแก้ Code หลัก Docker จะยังใช้ Cache ของ Layer pip install ได้ เพราะ requirements.txt ไม่เปลี่ยน

ตรวจสอบประวัติด้วย `docker history`

เราสามารถดูว่า Image นี้ถูกสร้างมาจากคำสั่งอะไรบ้างและแต่ละชั้นมีขนาดเท่าไหร่

$ docker history python:3.9-slim

IMAGE CREATED CREATED BY SIZE
a8734b... 2 weeks ago CMD ["python3"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENTRYPOINT... 0B
<missing> 2 weeks ago /bin/sh -c apt-get update... 10MB

ประโยชน์:

  • ตรวจสอบว่า Image บวมเพราะ Layer ไหน
  • ดูว่าใครเป็นคนสร้าง Image หรือใช้คำสั่งอะไรที่อาจไม่ปลอดภัย

Docker Registry คืออะไร?

☁️

คือ "โกดัง" สำหรับจัดเก็บและแจกจ่าย Docker Images

Docker Hub

Public Registry ที่ใหญ่ที่สุด (Default ของ Docker) มี Official Images มากมาย (Nginx, Python, Node)

Private Registry

สำหรับเก็บ Image ของบริษัทที่ไม่ต้องการเปิดเผย (เช่น AWS ECR, Google GCR, หรือ Self-hosted)

โครงสร้างชื่อของ Image

ชื่อ Image บอกที่มาและเวอร์ชันเสมอ

docker.io/ Registry (Optional)
wichit2s/ User/Org
my-app Image Name
:v1.0 Tag
  • 🔴 Registry: ถ้าไม่ใส่ จะถือว่าเป็น docker.io (Docker Hub)
  • 🔵 User: ชื่อบัญชีเจ้าของ ถ้าเป็น Official Image (เช่น ubuntu) จะไม่มีส่วนนี้
  • 🟡 Tag: ระบุเวอร์ชัน ถ้าไม่ใส่ จะถือว่าเป็น :latest

ขั้นตอนการนำ Image ขึ้น Cloud

# 1. Login เข้า Docker Hub

$ docker login

# 2. แปะป้ายชื่อ (Tag) ให้ตรงกับ User ของเรา

รูปแบบ: docker tag <local-image> <user>/<image>:<tag>

$ docker tag my-app:latest wichit2s/my-app:v1

# 3. อัปโหลด (Push)

$ docker push wichit2s/my-app:v1

⚠️ สำคัญ: คุณต้องมีสิทธิ์เขียน (Write Access) ใน Repository ปลายทาง

ปัญหา: ข้อมูลหายไปไหน?

Containers are Ephemeral (เกิดขึ้นแล้วดับไปในเวลาอันสั้น)

🗑️

เมื่อ Container ถูกลบ (Remove)

➡️
🔥

ไฟล์ที่สร้างใหม่ใน Container จะหายไปทันที!

ทางแก้คือการใช้ Volumes หรือ Bind Mounts เพื่อเก็บข้อมูลไว้นอก Container

ทางเลือกในการจัดเก็บข้อมูล (Storage Options)

Docker มีวิธีหลักๆ ในการเก็บข้อมูล (Persist) แม้ Container จะถูกลบไปแล้ว

1. Volumes

เก็บข้อมูลไว้ในพื้นที่ส่วนตัวของ Docker บน Host (เช่น /var/lib/docker/volumes/)

Best Practice

2. Bind Mounts

เชื่อมโยงไฟล์หรือโฟลเดอร์จาก ที่ไหนก็ได้ บนเครื่อง Host เข้าไปใน Container

Great for Dev

3. tmpfs Mounts

เก็บข้อมูลใน RAM ของ Host เท่านั้น ข้อมูลจะหายไปเมื่อ Container หยุดทำงาน

For Secrets/Speed

เจาะลึก Docker Volumes

วิธีมาตรฐานและปลอดภัยที่สุดในการเก็บข้อมูล

ทำไมต้องใช้ Volumes?

  • Managed by Docker: ดูแลโดย Docker โดยตรง ไม่ขึ้นกับโครงสร้างไฟล์ของ OS
  • Easy Backup: ง่ายต่อการสำรองข้อมูลหรือย้ายข้อมูล (Migration)
  • Sharing: แชร์ข้อมูลระหว่าง Container ได้อย่างปลอดภัย
  • Driver Support: รองรับการเก็บข้อมูลบน Cloud หรือ Remote Storage ได้

# ตัวอย่างการสร้างและใช้งาน

$ docker volume create my-db-data

$ docker run -v my-db-data:/var/lib/mysql mysql

เจาะลึก Bind Mounts

สะพานเชื่อมระหว่าง Code ในเครื่องเรากับ Container

Use Case หลัก

เหมาะมากสำหรับ Development Environment เราแก้ Code ในเครื่อง (Host) แล้วเห็นผลลัพธ์ใน Container ทันทีโดยไม่ต้อง Build Image ใหม่

# รูปแบบคำสั่ง

-v /path/on/host:/path/in/container

# ตัวอย่างจริง (Windows/Mac)

$ docker run -v $(pwd):/app -p 80:80 my-web-server

⚠️ ข้อควรระวัง: ประสิทธิภาพอาจต่ำกว่า Volume และขึ้นอยู่กับ File System ของ Host

คำสั่งจัดการ Volume (Volume CLI)

docker volume create <name>
สร้าง Volume ใหม่
docker volume ls
ดูรายชื่อ Volume ทั้งหมด
docker volume inspect <name>
ดูรายละเอียด (เช่น path จริงบนเครื่อง)
docker volume rm <name>
ลบ Volume (ต้องไม่มี Container ใช้งานอยู่)
docker volume prune
ลบ Volume ที่ไม่ได้ใช้ทั้งหมด (Clean up)

วิธีการใช้งาน: -v vs --mount

เราสามารถ Mount Volume ได้ 2 วิธีหลัก

1. -v (Short Syntax)

นิยมใช้มากที่สุด สั้น กระชับ แต่อาจสับสนระหว่าง Volume กับ Bind Mount

-v my-vol:/app

ถ้าไม่มี / นำหน้า = Named Volume

-v /home/user/data:/app

ถ้ามี / นำหน้า = Bind Mount (Host path)

2. --mount (Long Syntax)

ชัดเจน อ่านง่าย (Explicit) แนะนำสำหรับการใช้งานใน Docker Service / Swarm

--mount type=volume,src=my-vol,dst=/app

--- OR ---

--mount type=bind,src=/host/path,dst=/app

Docker Networking คืออะไร?

Container ไม่ได้อยู่โดดเดี่ยว ต้องสื่อสารกันเองและสื่อสารกับโลกภายนอกด้วย

🌐

World

↔️
Docker Host
🐳

Container

เป้าหมายของ Networking:

  • แยกการสื่อสารของแต่ละ App ออกจากกัน (Isolation)
  • เชื่อมต่อ Database กับ Backend อย่างปลอดภัย (Communication)
  • เปิดให้บริการ Web Server สู่ภายนอก (Exposing)

1. Bridge Network (Default)

เมื่อคุณรัน Container โดยไม่ระบุจะใช้ Network นี้

🌉

Software Bridge (docker0)

Docker สร้าง Virtual Switch ภายใน Host เพื่อให้ Container เชื่อมต่อกันได้

  • Container จะได้รับ IP Address ภายใน (เช่น 172.17.0.2)
  • Container ใน Bridge เดียวกันคุยกันได้ผ่าน IP Address
  • ข้อจำกัด: ใน Default Bridge, Container ไม่สามารถ คุยกันผ่าน "ชื่อ" (DNS Name) ได้ ต้องใช้ IP เท่านั้น

# ตรวจสอบ Network ทั้งหมด

$ docker network ls

Special Drivers: Host & None

Host Network

--network host
  • ยกเลิกการแยก Network: Container ใช้ Network Stack เดียวกับเครื่อง Host เลย
  • Performance สูงสุด: ไม่มีการทำ NAT
  • ข้อเสีย: Port ชนกันได้ง่าย (เช่น รัน Nginx 2 ตัวที่ Port 80 ไม่ได้)

None Network

--network none
  • ตัดขาดจากโลกภายนอก: Container ไม่มี Network Interface (นอกจาก Loopback)
  • Security สูงสุด: เหมาะสำหรับงานที่ไม่ต้องใช้อินเทอร์เน็ต เช่น การคำนวณข้อมูลแบบ Offline หรือ Batch Job

User-defined Bridge Network

★ พระเอกตัวจริง: สิ่งที่เราควรใช้แทน Default Bridge

Automatic Service Discovery

เมื่อเราสร้าง Network เอง Container จะสามารถเรียกหากันด้วย "ชื่อ Container" ได้เลย (Docker มี DNS Server ภายในให้)

# 1. สร้าง Network ใหม่

$ docker network create my-net

# 2. รัน Database (ตั้งชื่อว่า db)

$ docker run -d --name db --network my-net postgres

# 3. รัน Web App ให้ต่อกับ db

$ docker run -d --network my-net my-web-app

# Web App สามารถ connect string ไปที่ host: "db" ได้เลย!

การเผยแพร่พอร์ต (Port Publishing)

การอนุญาตให้คนภายนอกเข้าถึง Container ได้

-p <Host_Port>:<Container_Port>

รูปแบบ: -p [พอร์ตเครื่องเรา]:[พอร์ตภายในคอนเทนเนอร์]

$ docker run -p 8080:80 nginx

เข้าเว็บผ่าน http://localhost:8080

$ docker run -p 3000:3000 nodeapp

เข้าเว็บผ่าน http://localhost:3000

⚠️ ถ้าไม่ใส่ -p Container จะคุยกันเองได้ แต่เราจะเข้าผ่าน Browser ไม่ได้

การสื่อสารระหว่าง Container (Container Communication)

เมื่อเราสร้าง Network เอง (User-defined Bridge) สิ่งมหัศจรรย์จะเกิดขึ้น

Default Bridge

Container ต้องคุยกันผ่าน IP Address เท่านั้น

$ ping 172.17.0.2
$ ping db-server ✘ (Unknown Host)

ยากในการจัดการ เพราะ IP เปลี่ยนทุกครั้งที่ Restart

User-defined Bridge

Container คุยกันผ่าน Container Name ได้เลย

$ ping 172.18.0.2
$ ping db-server ✔ (Resolved!)

Docker มี Internal DNS Server คอยจัดการให้

Service Discovery & DNS

📱
Web App
name: my-web
"Connect to db:5432"
Docker DNS resolves IP
🗄️
Database
name: db

ประโยชน์ของการใช้ชื่อ (DNS):

  • ไม่ต้อง Hardcode IP Address ใน Config file
  • เมื่อ Container ตายแล้วเกิดใหม่ (ได้ IP ใหม่) แอปฯ ยังทำงานได้ปกติเพราะชื่อเดิม
  • เป็นพื้นฐานสำคัญก่อนไปสู่ Docker Compose และ Kubernetes

สรุปคำสั่งสำคัญ (Cheat Sheet)

Images

docker build -t .

docker images

docker tag

docker push

Volumes

docker volume create

docker volume ls

docker run -v vol:/path

docker run -v $(pwd):/path

Network

docker network create

docker network ls

docker run --network

docker network inspect

Cleanup (ใช้ระวัง!)

docker system prune -a

ลบทุกอย่างที่ไม่ได้รันอยู่ (Stopped containers, unused networks, dangling images)
🧪

Ready for Laboratory?

เราจะลงมือทำจริง: ตั้งแต่เขียน Code, สร้าง Image, Push ขึ้น Cloud, จนถึงการจัดการ Data และ Network

เปิด VS Code และ Docker Desktop รอไว้เลย!

ภารกิจวันนี้ (Mission: Django & Postgres)

เราจะจำลองสภาพแวดล้อม Production โดยแยก Database และ Web Server ออกจากกัน

🌐 Network: สร้าง Docker Network ให้ Container คุยกันได้
🗄️ Database: รัน PostgreSQL Container แยกต่างหาก
Optimization: เขียน Dockerfile ให้ Build เร็วด้วย Layer Caching
💾 Volumes: ใช้ Bind Mount เพื่อแก้โค้ดแล้วเห็นผลทันที

Step 1: เตรียม Source Code

ดาวน์โหลดโค้ดต้นฉบับและสลับไปยัง Branch ที่ถูกต้อง

# 1. Clone Repository

$ git clone https://github.com/wichitpaulsombat/djrepo.git

# 2. เข้าโฟลเดอร์และ Checkout branch wk04

$ cd djrepo

$ git checkout wk04

# 3. ตรวจสอบไฟล์ (ต้องมี manage.py และ requirements.txt)

$ ls -l

Step 2: สร้าง Docker Network

เพื่อให้ Web App (Django) มองเห็น Database (Postgres) ผ่านชื่อ Container

# สร้าง Bridge Network ชื่อ 'dj-net'

$ docker network create dj-net

💡 ทำไมต้องทำ?
ถ้าไม่สร้าง Network เอง Container จะใช้ Default Bridge ซึ่งไม่สามารถคุยกันด้วยชื่อ (DNS Resolution) ได้สะดวก

Step 3: รัน Database Container

# รัน Postgres โดยกำหนดชื่อเป็น 'db' และอยู่ใน Network เดียวกัน

$ docker run -d \
  --name db \
  --network dj-net \
  -e POSTGRES_DB=postgres \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:15-alpine

--name db: ชื่อนี้สำคัญ! Django จะใช้เชื่อมต่อ
-v pgdata:...: ใช้ Named Volume เก็บข้อมูลถาวร

Step 4: ตรวจสอบการตั้งค่า Django

เปิดไฟล์ mysite/settings.py ดูส่วน DATABASES

import os

...

DATABASES = {

  'default': {

    'ENGINE': 'django.db.backends.postgresql',

    'NAME': os.environ.get('DB_NAME', 'postgres'),

    'USER': os.environ.get('DB_USER', 'postgres'),

    'PASSWORD': os.environ.get('DB_PASSWORD', 'secret'),

    'HOST': os.environ.get('DB_HOST', 'db'), # ชื่อ Container DB

    'PORT': '5432',

  }

}

สังเกตว่าโค้ดรองรับการอ่านค่าจาก Environment Variables (os.environ.get) ทำให้เราปรับแก้ค่าได้ตอนสั่ง Docker Run

Step 5: เขียน Dockerfile (Optimized)

สร้างไฟล์ Dockerfile ใหม่ โดยเรียงลำดับ Layer ให้ถูกต้อง

FROM python:3.10-slim

# ติดตั้ง dependencies ที่จำเป็นสำหรับ Postgres

RUN apt-get update && apt-get install -y libpq-dev gcc

WORKDIR /app

# 🚀 Optimization: Copy requirements ก่อน Code

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

# จากนั้นค่อย Copy Code ที่เหลือ

COPY . .

EXPOSE 8000

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Step 5: เขียน Dockerfile (Optimized with uv)

ปรับปรุง Dockerfile โดยใช้ uv แทน pip เพื่อลดเวลา Build (เร็วกว่า 10-100 เท่า)

FROM python:3.13-slim

# 1. นำเข้า uv จาก official image

COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# 2. ติดตั้ง OS dependencies ที่จำเป็นสำหรับ Postgres

RUN apt-get update && apt-get install -y libpq-dev gcc

WORKDIR /app

# 3. Copy requirements และติดตั้งด้วย uv

COPY pyproject.toml uv.lock ./

RUN uv sync --frozen --no-dev

# 4. Copy Code ส่วนที่เหลือ

COPY . .

# 5. Expose port

EXPOSE 8000

CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]

💡 Key Changes:

  • COPY --from=.../uv: ดึงตัวโปรแกรม uv เข้ามาใช้ใน image
  • uv pip install --system: สั่ง uv ให้ลง package ใส่ระบบหลักของ Python ใน container เลย (ไม่ต้องสร้าง venv ซ้อน) ซึ่งเร็วกว่า pip มาก

Step 6: Build Image

# ตั้งชื่อ Image ว่า 'dj-app'

$ docker build -t dj-app .

ลองทดสอบ Caching:

  1. ลอง Build ครั้งแรก (ใช้เวลาโหลด pip นาน)
  2. แก้ไฟล์ในโฟลเดอร์ mysite เล็กน้อย (เช่นแก้ README)
  3. Build อีกครั้ง ... สังเกตว่าขั้นตอน RUN pip install จะเขียนว่า Using cache (เร็วมาก!)

Step 7: Start Django Container

เชื่อม Network, กำหนด Env Vars, และ Mount Volume

# Windows (PowerShell) ใช้ ${PWD} แทน $(pwd)

$ docker run -d \
  --name web \
  --network dj-net \
  -p 8000:8000 \
  -v $(pwd):/app \
  -e DB_HOST=db \
  -e DB_PASSWORD=secret \
  dj-app

--network dj-net: เพื่อให้เจอ container ชื่อ 'db'

-v $(pwd):/app: เพื่อให้แก้โค้ดในเครื่องแล้ว container อัปเดตตาม (Hot Reload)

-e DB_HOST=db: บอก Django ให้ต่อ Database ที่เครื่องชื่อ 'db'

Step 8: Initialize Database

เมื่อรันครั้งแรก Database ยังว่างเปล่า เราต้องสั่ง Migrate เพื่อสร้างตาราง

# สั่งรันคำสั่งภายใน container ชื่อ 'web'

$ docker exec -it web python manage.py migrate

# (Optional) สร้าง Admin User

$ docker exec -it web python manage.py createsuperuser

ถ้าขึ้น OK แสดงว่า Django ต่อกับ Postgres สำเร็จแล้ว!

Step 9: ทดสอบหน้าเว็บ

🌐 Open Browser

http://localhost:8000

คุณควรเห็นหน้า "The install worked successfully!" หรือหน้าแอปพลิเคชันของคุณ

ลองเข้า http://localhost:8000/admin และ Login ด้วย Superuser ที่สร้างเมื่อสักครู่

Step 10: ทดสอบการแก้โค้ด (Bind Mount)

พิสูจน์ว่าเราไม่ต้อง Build Image ใหม่เมื่อแก้โค้ด

  1. เปิด VS Code ในเครื่องของคุณ (Host)
  2. แก้ไขไฟล์ mysite/urls.py หรือสร้างไฟล์ views ง่ายๆ
  3. ลองเปลี่ยนข้อความที่แสดงผล หรือทำให้ Code Error เล็กน้อย
  4. Refresh Browser ทันที (ไม่ต้อง stop/start container)
  5. ถ้าหน้าเว็บเปลี่ยนตาม แสดงว่า Bind Mount (-v $(pwd):/app) ทำงานสมบูรณ์

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

Connection Refused (DB)

สาเหตุ: Container 'web' หา 'db' ไม่เจอ หรือ DB ยังรันไม่เสร็จ

แก้: เช็คว่าอยู่ Network เดียวกันไหม (`docker network inspect dj-net`)

Relation "auth_user" does not exist

สาเหตุ: ยังไม่ได้รัน Migration

แก้: รันคำสั่ง `migrate` ใน Step 8 อีกครั้ง

DisallowedHost

สาเหตุ: ALLOWED_HOSTS ใน settings.py ไม่รองรับ

แก้: เพิ่ม '*' หรือ 'localhost' ใน settings.py แล้ว Save (Bind mount จะอัปเดตให้)

Step 11: การลบและคืนพื้นที่

เมื่อทำ Lab เสร็จแล้ว ควรลบ Container เพื่อไม่ให้หนักเครื่อง

# ลบ Containers (ทั้ง web และ db)

$ docker rm -f web db


# ลบ Network

$ docker network rm dj-net


# (Optional) ลบ Volume หากไม่ต้องการข้อมูลแล้ว

$ docker volume rm pgdata

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

สิ่งที่ต้องส่ง:

📸
1. ส่ง Video Link:
แสดงการทำ lab เพื่อให้ได้ ResumeHub Website รันผ่าน docker ได้ สำเร็จ