DEV Community

Cover image for Docker ฉบับเข้าใจง่าย: Image, Container, Volume, Network และ Docker Compose
reerii code
reerii code

Posted on

Docker ฉบับเข้าใจง่าย: Image, Container, Volume, Network และ Docker Compose

เคยเจอไหม เขียนโปรเจกต์บน Windows มาอย่างดี รันในเครื่องตัวเองผ่านหมด แต่พอเอาไป deploy บนเซิร์ฟเวอร์ Linux ดันรันไม่ขึ้น เพราะ library บางตัวทำงานต่างกันในคนละสภาพแวดล้อม

หรือบางที ส่งโปรเจกต์ให้เพื่อนไปรัน เพื่อนก็รันไม่ได้อีก เพราะเวอร์ชันที่ติดตั้งไม่ตรงกัน หรือเวอร์ชันตรงกันแล้วแต่ดันอยู่คนละ OS

นี่แหละคือปัญหาคลาสสิก "It works on my machine" และเป็นเหตุผลที่ Docker เกิดขึ้นมา


Docker ต่างจาก VM ยังไง

ถ้ามองแบบหยาบ ๆ Docker ก็คล้าย VM นะ คือแยกแอปออกไปรันในพื้นที่ของตัวเอง แต่ต่างกันตรงนี้

  • VM (Virtual Machine) คือการจำลองคอมพิวเตอร์ขึ้นมาทั้งเครื่อง มี OS ของตัวเองครบชุด เลยกินทรัพยากรเยอะ และบูตนาน
  • Docker เอาแค่สิ่งที่แอปต้องใช้จริง ๆ เช่น โค้ด runtime library และ config มัดรวมเป็นก้อนเดียว แล้วใช้ kernel ร่วมกับเครื่องหลัก เลยเบาและเปิดขึ้นได้ในไม่กี่วินาที

ก้อนที่มัดรวมทุกอย่างไว้นั่นแหละ เรียกว่า Image

เปรียบเทียบ VM กับ Docker


Image: แม่แบบของแอป

Image มีอยู่ 2 แบบ

  1. Image สำเร็จรูป ที่มีคน build ไว้ให้แล้วบน Docker Hub เช่น mysql, phpmyadmin, node, nginx ดึงมาใช้ได้เลย
  2. Image ของแอปเราเอง อันนี้ต้อง build เอง ถ้าโปรเจกต์มีหน้าเว็บ 1 ตัว กับ API 1 ตัว ก็ต้อง build ออกมาเป็นอย่างละ 1 image

จะ build image ได้ ต้องมีไฟล์ชื่อ Dockerfile อยู่ในแต่ละโปรเจกต์ (web กับ api แยกกันคนละไฟล์) ข้างในจะบอกว่า

  • ใช้ image ตั้งต้นตัวไหน เช่น node:22-alpine
  • ตอน build ต้องรันคำสั่งอะไร เช่น npm install
  • ตอนเปิดแอปให้รันคำสั่งอะไร เช่น npm start หรือ node dist/index.js

ตัวอย่าง Dockerfile ของ API (Node.js) หน้าตาประมาณนี้

# 1) ใช้ image ตั้งต้นเป็น Node.js 22 แบบ alpine (ขนาดเล็ก)
FROM node:22-alpine

# 2) กำหนดโฟลเดอร์ทำงานใน container
WORKDIR /app

# 3) copy ไฟล์ package ก่อน แล้วติดตั้ง dependency
COPY package*.json ./
RUN npm ci --omit=dev

# 4) copy โค้ดที่เหลือเข้าไป
COPY . .

# 5) บอกว่าแอปใช้ port 3000
EXPOSE 3000

# 6) คำสั่งที่รันตอน container เริ่มทำงาน
CMD ["node", "src/index.js"]
Enter fullscreen mode Exit fullscreen mode

💡 ทำไม copy package.json ก่อนโค้ด? เพราะ Docker build เป็นชั้น ๆ (layer) และจำ cache ไว้ ถ้าแก้แค่โค้ดแต่ไม่ได้แก้ dependency รอบหน้าจะไม่ต้อง npm ci ใหม่ build เร็วขึ้นเยอะ


Container: Image ที่ถูกรันแล้ว

แต่ image เฉย ๆ ทำอะไรไม่ได้นะ มันเป็นแค่แม่แบบ จะใช้งานได้ต้องเอาไป run ก่อน ถึงจะกลายเป็น Container ซึ่งก็คือแอปที่กำลังทำงานอยู่จริง เปิดใช้งานได้ตามปกติ

ถ้าเป็นสายเขียนโค้ด ให้นึกว่า image คือ class ส่วน container คือ object ที่ new ออกมา จาก image เดียวจะรันกี่ container ก็ได้

ดังนั้นถ้าโปรเจกต์มี web + api + database ก็จะมี 3 image และพอรันก็จะได้ 3 container

ไม่ยากใช่ไหมล่ะ แค่ build image แล้วรันเป็น container 😄

จาก Dockerfile สู่ Image และ Container


Volume: ที่เก็บข้อมูลที่ไม่หายไปกับ Container

นอกจาก image กับ container แล้ว ยังมีอีก 2 ตัวที่ต้องรู้จัก ตัวแรกคือ Volume

container ถูกออกแบบมาให้ลบทิ้งแล้วสร้างใหม่ได้ตลอด ถ้าเก็บข้อมูลไว้ข้างใน container พอลบ ข้อมูลก็หายไปด้วย

Volume คือพื้นที่เก็บข้อมูลที่แยกออกมาไว้ข้างนอก container เช่น ไฟล์ฐานข้อมูลของ MySQL หรือรูปที่ผู้ใช้อัปโหลด สมมติวันหนึ่งเราลบ container หรือลบ image ไปเลย แต่ถ้าไม่ได้ลบ volume ข้อมูลที่ใช้งานมาก็ยังอยู่ครบ สร้าง container ใหม่แล้วผูกกับ volume เดิม ข้อมูลก็กลับมาเหมือนเดิม

📌 Volume ไม่ใช่ตัวฐานข้อมูลนะ ตัวฐานข้อมูลคือ container ของ MySQL ส่วน volume คือ "ที่เก็บไฟล์" ของฐานข้อมูลนั้น

Network: ทางเชื่อมระหว่าง Container

ตัวที่สองคือ Network ทำหน้าที่เชื่อม web, api และ database ให้อยู่วงเดียวกัน เรียกใช้งานกันได้เหมือนอยู่ในเครื่องเดียวกัน

ข้อดีคือ container ใน network เดียวกันเรียกหากันด้วย ชื่อ service ได้เลย เช่น API ต่อฐานข้อมูลด้วย host db ไม่ต้องไปจำ IP

⚠️ จุดที่มือใหม่พลาดบ่อย: ชื่อ db หรือ api ใช้ได้เฉพาะตอน container คุยกันเองเท่านั้น หน้าเว็บที่รันอยู่บน browser ของผู้ใช้ไม่ได้อยู่ใน network นี้ เวลาหน้าเว็บเรียก API ต้องเรียกผ่าน port ที่เปิดออกมา เช่น http://localhost:3000 (หรือโดเมนจริงตอน deploy)

การเชื่อมต่อ container ผ่าน network และ volume


สรุป 5 คำที่ต้องรู้

คำ คืออะไร เปรียบเทียบ
Dockerfile ไฟล์สูตร บอกว่าใช้ image อะไร รันคำสั่งอะไร สูตรอาหาร
Image แม่แบบที่ build จาก Dockerfile พร้อมทุกอย่างที่แอปต้องใช้ class
Container image ที่ถูกรันแล้ว คือแอปที่ทำงานอยู่จริง object
Volume ที่เก็บข้อมูลถาวร ไม่หายแม้ลบ container external hard disk
Network วงเชื่อมให้ container เรียกหากันด้วยชื่อได้ วง LAN ในบ้าน

คำสั่งพื้นฐานที่ใช้บ่อย

เช็กว่าติดตั้งแล้ว

docker --version
docker compose version
Enter fullscreen mode Exit fullscreen mode

Image

docker pull mysql:8.4              # ดึง image สำเร็จรูปจาก Docker Hub
docker images                      # ดู image ที่มีในเครื่อง
docker build -t my-api ./api       # build image จาก Dockerfile ในโฟลเดอร์ api
docker rmi my-api                  # ลบ image
Enter fullscreen mode Exit fullscreen mode

Container

docker run -d --name api -p 3000:3000 my-api   # รัน container (-d = รันเบื้องหลัง, -p = เปิด port เครื่อง:container)
docker ps                          # ดู container ที่กำลังรัน
docker ps -a                       # ดูทั้งหมด รวมตัวที่หยุดแล้ว
docker logs -f api                 # ดู log แบบ real-time
docker exec -it api sh             # เข้าไปใน container
docker stop api                    # หยุด
docker start api                   # เริ่มใหม่
docker rm api                      # ลบ container
Enter fullscreen mode Exit fullscreen mode

Volume และ Network

docker volume ls                   # ดู volume ทั้งหมด
docker volume create db_data       # สร้าง volume
docker network ls                  # ดู network ทั้งหมด
docker network create app-net      # สร้าง network
Enter fullscreen mode Exit fullscreen mode

ปัญหา: ถ้าทำทุกอย่างด้วยมือ คำสั่งจะยาวมาก

ที่เล่ามาทั้งหมด ถ้าต้องพิมพ์ใน terminal เองทีละคำสั่ง ระบบที่มี database, phpMyAdmin, api และ web จะออกมาหน้าตาแบบนี้

docker network create app-net
docker volume create db_data

docker run -d --name db --network app-net \
  -e MYSQL_ROOT_PASSWORD=changeme -e MYSQL_DATABASE=myapp \
  -v db_data:/var/lib/mysql mysql:8.4

docker run -d --name phpmyadmin --network app-net \
  -e PMA_HOST=db -p 8080:80 phpmyadmin

docker build -t my-api ./api
docker run -d --name api --network app-net \
  -e DB_HOST=db -e DB_USER=root -e DB_PASSWORD=changeme -e DB_NAME=myapp \
  -p 3000:3000 my-api

docker build -t my-web ./web
docker run -d --name web --network app-net -p 80:80 my-web
Enter fullscreen mode Exit fullscreen mode

ยาวใช่ไหม แถมพลาดนิดเดียว เช่น ลืมใส่ --network หรือพิมพ์ชื่อ volume ผิด ระบบก็พังทันที แล้วทุกครั้งที่ย้ายเครื่องก็ต้องมานั่งพิมพ์ใหม่อีก


Docker Compose: เขียนครั้งเดียว รันคำสั่งเดียว

ปัญหานี้เลยเป็นที่มาของ Docker Compose

Docker Compose คือไฟล์ตั้งค่า (YAML) ที่เราเขียนโครงสร้างระบบไว้ว่ามี service อะไรบ้าง ใช้ image ไหน เปิด port อะไร ต่อ volume กับ network ยังไง จากนั้นสั่ง คำสั่งเดียว ระบบก็ขึ้นครบทั้งชุด

Docker Compose: ไฟล์เดียว คำสั่งเดียว

ตัวอย่างโครงสร้างโปรเจกต์

my-project/
├── api/
│   ├── src/index.js
│   ├── package.json
│   ├── Dockerfile
│   └── .dockerignore
├── web/
│   ├── src/...
│   ├── package.json
│   ├── Dockerfile
│   └── .dockerignore
├── .env
└── compose.yaml
Enter fullscreen mode Exit fullscreen mode

api/Dockerfile

ใช้ไฟล์เดียวกับตัวอย่างด้านบนได้เลย

web/Dockerfile (เช่น React/Vue ที่ build ด้วย Vite)

หน้าเว็บแบบนี้ใช้ Node แค่ตอน build พอได้ไฟล์ static แล้วก็ส่งต่อให้ Nginx เสิร์ฟ เรียกว่า multi-stage build ทำให้ image สุดท้ายเล็กมาก

# Stage 1: build หน้าเว็บ
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: เอาเฉพาะไฟล์ที่ build แล้วไปเสิร์ฟด้วย Nginx
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
Enter fullscreen mode Exit fullscreen mode

.dockerignore (ใส่ทั้งใน api และ web)

กันไม่ให้ไฟล์ไม่จำเป็นถูก copy เข้าไปใน image

node_modules
dist
.env
.git
Enter fullscreen mode Exit fullscreen mode

.env

เก็บรหัสผ่านแยกไว้ อย่า commit ขึ้น Git (ใส่ไว้ใน .gitignore)

DB_ROOT_PASSWORD=changeme
Enter fullscreen mode Exit fullscreen mode

compose.yaml

services:
  db:
    image: mysql:8.4
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql        # เก็บข้อมูลไว้ใน volume
    healthcheck:                       # เช็กว่า MySQL พร้อมรับ connection แล้ว
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
      interval: 5s
      retries: 10
    networks:
      - app-net

  phpmyadmin:
    image: phpmyadmin
    environment:
      PMA_HOST: db                     # เรียก database ด้วยชื่อ service
    ports:
      - "8080:80"
    depends_on:
      - db
    networks:
      - app-net

  api:
    build: ./api                       # build จาก api/Dockerfile
    restart: unless-stopped
    environment:
      DB_HOST: db
      DB_USER: root
      DB_PASSWORD: ${DB_ROOT_PASSWORD}
      DB_NAME: myapp
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy     # รอจน db พร้อมจริง ๆ ค่อยเริ่ม
    networks:
      - app-net

  web:
    build: ./web                       # build จาก web/Dockerfile
    restart: unless-stopped
    ports:
      - "80:80"
    depends_on:
      - api
    networks:
      - app-net

volumes:
  db_data:

networks:
  app-net:
Enter fullscreen mode Exit fullscreen mode

คำสั่ง Docker Compose ที่ใช้บ่อย

docker compose up -d               # build (ถ้ายังไม่มี) + รันทั้งระบบ
docker compose up -d --build       # build image ใหม่ แล้วรัน (ใช้หลังแก้โค้ด)
docker compose ps                  # ดูสถานะทุก service
docker compose logs -f api         # ดู log ของ api
docker compose exec db mysql -u root -p   # เข้า MySQL ใน container
docker compose restart api         # restart เฉพาะ api
docker compose down                # หยุดและลบ container + network (ข้อมูลใน volume ยังอยู่)
docker compose down -v             # ⚠️ ลบ volume ด้วย = ข้อมูลหายหมด!
Enter fullscreen mode Exit fullscreen mode

รันแล้วเปิดดูได้เลย

  • หน้าเว็บ → http://localhost
  • API → http://localhost:3000
  • phpMyAdmin → http://localhost:8080

ข้อควรระวังเล็ก ๆ ที่ช่วยได้เยอะ

  • docker compose down -v จะลบ volume ข้อมูลในฐานข้อมูลหายหมด ใช้ตอนอยากล้างระบบจริง ๆ เท่านั้น
  • depends_on เฉย ๆ แค่รอให้ container เริ่ม ไม่ได้รอให้ฐานข้อมูลพร้อมรับ connection เลยต้องใช้ healthcheck คู่กับ condition: service_healthy แบบในตัวอย่าง
  • ใช้ docker compose (มีเว้นวรรค) เป็นเวอร์ชันปัจจุบัน ส่วน docker-compose (มีขีด) เป็นตัวเก่า และไม่ต้องใส่ version: บนหัวไฟล์แล้ว
  • บน Windows ใช้ Docker Desktop (WSL 2) container ที่รันอยู่ก็คือ Linux container เหมือนบนเซิร์ฟเวอร์ ปัญหา "Windows รันได้ Linux รันไม่ได้" เลยหายไปเกือบหมด
  • ระวังเรื่องสถาปัตยกรรม CPU ถ้า build บนเครื่อง ARM (เช่น Mac ชิป M) แล้วจะเอา image ไปรันบนเซิร์ฟเวอร์ x86 ให้ build ด้วย docker buildx build --platform linux/amd64 ...

สรุป

  • Dockerfile → build ได้ Image → run ได้ Container
  • Volume เก็บข้อมูลไว้ไม่ให้หายไปกับ container
  • Network เชื่อม container ให้เรียกหากันด้วยชื่อได้
  • Docker Compose รวมทุกอย่างไว้ในไฟล์เดียว สั่ง docker compose up -d คำสั่งเดียวจบ

ครั้งหน้าส่งโปรเจกต์ให้เพื่อน แค่แนบ compose.yaml ไปด้วย เพื่อนก็รันได้เหมือนเครื่องเราเป๊ะ ไม่ต้องมานั่งถามว่า "ใช้ Node เวอร์ชันไหน" อีกแล้ว 🐳

Top comments (0)