เจาะลึกการทำงานของ Image Layers, การฝากไฟล์ไว้บน Registry และการเก็บข้อมูลให้อยู่ถาวร
ก่อนจะไปต่อ ต้องแม่นเรื่องข้อแตกต่าง
"แม่พิมพ์" หรือ "ต้นแบบ"
"สิ่งมีชีวิต" ที่รันอยู่จริง
ทำไม Docker Image ถึงมีขนาดเล็กและสร้างได้เร็ว? คำตอบคือ UnionFS
ทุกคำสั่งใน Dockerfile สร้าง Layer ใหม่
# Dockerfile
FROM ubuntu:20.04
COPY . /app
RUN make /app
CMD python app.py
💡 ในกรณีที่หลาย App ใช้ Base Image อันเดียวกัน จะโหลด Base Image แค่ครั้งเดียวและแชร์กัน (ประหยัด Disk)
Docker จะจำผลลัพธ์ของแต่ละ Layer ไว้ ถ้า Input ไม่เปลี่ยน Docker จะใช้ "ของเดิม" (Cache) แทนการสร้างใหม่
COPY . .
RUN pip install -r requirements.txt
ถ้าแก้ Code แค่บรรทัดเดียว Layer COPY . . จะเปลี่ยน ทำให้ Layer ถัดไป (pip install) ต้องรันใหม่ทั้งหมด ทั้งที่ dependency ไม่เปลี่ยน
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
แยก Copy dependency มาก่อน ถ้าแก้ Code หลัก Docker จะยังใช้ Cache ของ Layer pip install ได้ เพราะ requirements.txt ไม่เปลี่ยน
เราสามารถดูว่า 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 |
คือ "โกดัง" สำหรับจัดเก็บและแจกจ่าย Docker Images
Public Registry ที่ใหญ่ที่สุด (Default ของ Docker) มี Official Images มากมาย (Nginx, Python, Node)
สำหรับเก็บ Image ของบริษัทที่ไม่ต้องการเปิดเผย (เช่น AWS ECR, Google GCR, หรือ Self-hosted)
ชื่อ Image บอกที่มาและเวอร์ชันเสมอ
docker.io (Docker Hub)ubuntu) จะไม่มีส่วนนี้:latest# 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
Docker มีวิธีหลักๆ ในการเก็บข้อมูล (Persist) แม้ Container จะถูกลบไปแล้ว
เก็บข้อมูลไว้ในพื้นที่ส่วนตัวของ Docker บน Host (เช่น /var/lib/docker/volumes/)
เชื่อมโยงไฟล์หรือโฟลเดอร์จาก ที่ไหนก็ได้ บนเครื่อง Host เข้าไปใน Container
Great for Devเก็บข้อมูลใน RAM ของ Host เท่านั้น ข้อมูลจะหายไปเมื่อ Container หยุดทำงาน
For Secrets/Speedวิธีมาตรฐานและปลอดภัยที่สุดในการเก็บข้อมูล
# ตัวอย่างการสร้างและใช้งาน
$ docker volume create my-db-data
$ docker run -v my-db-data:/var/lib/mysql mysql
สะพานเชื่อมระหว่าง Code ในเครื่องเรากับ Container
เหมาะมากสำหรับ 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
docker volume create <name>
docker volume ls
docker volume inspect <name>
docker volume rm <name>
docker volume prune
เราสามารถ Mount Volume ได้ 2 วิธีหลัก
นิยมใช้มากที่สุด สั้น กระชับ แต่อาจสับสนระหว่าง Volume กับ Bind Mount
-v my-vol:/app
ถ้าไม่มี / นำหน้า = Named Volume
-v /home/user/data:/app
ถ้ามี / นำหน้า = Bind Mount (Host path)
ชัดเจน อ่านง่าย (Explicit) แนะนำสำหรับการใช้งานใน Docker Service / Swarm
--mount type=volume,src=my-vol,dst=/app
--- OR ---
--mount type=bind,src=/host/path,dst=/app
Container ไม่ได้อยู่โดดเดี่ยว ต้องสื่อสารกันเองและสื่อสารกับโลกภายนอกด้วย
World
Container
เมื่อคุณรัน Container โดยไม่ระบุจะใช้ Network นี้
Docker สร้าง Virtual Switch ภายใน Host เพื่อให้ Container เชื่อมต่อกันได้
# ตรวจสอบ Network ทั้งหมด
$ docker network ls
--network host
--network none
★ พระเอกตัวจริง: สิ่งที่เราควรใช้แทน Default Bridge
เมื่อเราสร้าง 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" ได้เลย!
การอนุญาตให้คนภายนอกเข้าถึง Container ได้
-p <Host_Port>:<Container_Port>
รูปแบบ: -p [พอร์ตเครื่องเรา]:[พอร์ตภายในคอนเทนเนอร์]
เข้าเว็บผ่าน http://localhost:8080
เข้าเว็บผ่าน http://localhost:3000
⚠️ ถ้าไม่ใส่ -p Container จะคุยกันเองได้ แต่เราจะเข้าผ่าน Browser ไม่ได้
เมื่อเราสร้าง Network เอง (User-defined Bridge) สิ่งมหัศจรรย์จะเกิดขึ้น
Container ต้องคุยกันผ่าน IP Address เท่านั้น
ยากในการจัดการ เพราะ IP เปลี่ยนทุกครั้งที่ Restart
Container คุยกันผ่าน Container Name ได้เลย
Docker มี Internal DNS Server คอยจัดการให้
name: my-web
name: db
docker build -t .
docker images
docker tag
docker push
docker volume create
docker volume ls
docker run -v vol:/path
docker run -v $(pwd):/path
docker network create
docker network ls
docker run --network
docker network inspect
docker system prune -a
ลบทุกอย่างที่ไม่ได้รันอยู่ (Stopped containers, unused networks, dangling images)เราจะลงมือทำจริง: ตั้งแต่เขียน Code, สร้าง Image, Push ขึ้น Cloud, จนถึงการจัดการ Data และ Network
เราจะจำลองสภาพแวดล้อม Production โดยแยก Database และ Web Server ออกจากกัน
ดาวน์โหลดโค้ดต้นฉบับและสลับไปยัง 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
เพื่อให้ Web App (Django) มองเห็น Database (Postgres) ผ่านชื่อ Container
# สร้าง Bridge Network ชื่อ 'dj-net'
$ docker network create dj-net
💡 ทำไมต้องทำ?
ถ้าไม่สร้าง Network เอง Container จะใช้ Default Bridge ซึ่งไม่สามารถคุยกันด้วยชื่อ (DNS Resolution) ได้สะดวก
# รัน 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
เปิดไฟล์ 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
สร้างไฟล์ 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"]
ปรับปรุง 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"]
# ตั้งชื่อ Image ว่า 'dj-app'
$ docker build -t dj-app .
RUN pip install จะเขียนว่า Using cache (เร็วมาก!)เชื่อม 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'
เมื่อรันครั้งแรก 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 สำเร็จแล้ว!
http://localhost:8000
คุณควรเห็นหน้า "The install worked successfully!" หรือหน้าแอปพลิเคชันของคุณ
ลองเข้า http://localhost:8000/admin และ Login ด้วย Superuser ที่สร้างเมื่อสักครู่
พิสูจน์ว่าเราไม่ต้อง Build Image ใหม่เมื่อแก้โค้ด
mysite/urls.py หรือสร้างไฟล์ views ง่ายๆ-v $(pwd):/app) ทำงานสมบูรณ์สาเหตุ: Container 'web' หา 'db' ไม่เจอ หรือ DB ยังรันไม่เสร็จ
แก้: เช็คว่าอยู่ Network เดียวกันไหม (`docker network inspect dj-net`)
สาเหตุ: ยังไม่ได้รัน Migration
แก้: รันคำสั่ง `migrate` ใน Step 8 อีกครั้ง
สาเหตุ: ALLOWED_HOSTS ใน settings.py ไม่รองรับ
แก้: เพิ่ม '*' หรือ 'localhost' ใน settings.py แล้ว Save (Bind mount จะอัปเดตให้)
เมื่อทำ Lab เสร็จแล้ว ควรลบ Container เพื่อไม่ให้หนักเครื่อง
# ลบ Containers (ทั้ง web และ db)
$ docker rm -f web db
# ลบ Network
$ docker network rm dj-net
# (Optional) ลบ Volume หากไม่ต้องการข้อมูลแล้ว
$ docker volume rm pgdata