เคยเจอไหม เขียนโปรเจกต์บน 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
Image: แม่แบบของแอป
Image มีอยู่ 2 แบบ
- Image สำเร็จรูป ที่มีคน build ไว้ให้แล้วบน Docker Hub เช่น
mysql,phpmyadmin,node,nginxดึงมาใช้ได้เลย - 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"] 💡 ทำไม 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 😄
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)
สรุป 5 คำที่ต้องรู้
| คำ | คืออะไร | เปรียบเทียบ |
|---|---|---|
| Dockerfile | ไฟล์สูตร บอกว่าใช้ image อะไร รันคำสั่งอะไร | สูตรอาหาร |
| Image | แม่แบบที่ build จาก Dockerfile พร้อมทุกอย่างที่แอปต้องใช้ | class |
| Container | image ที่ถูกรันแล้ว คือแอปที่ทำงานอยู่จริง | object |
| Volume | ที่เก็บข้อมูลถาวร ไม่หายแม้ลบ container | external hard disk |
| Network | วงเชื่อมให้ container เรียกหากันด้วยชื่อได้ | วง LAN ในบ้าน |
คำสั่งพื้นฐานที่ใช้บ่อย
เช็กว่าติดตั้งแล้ว
docker --version docker compose version 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 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 Volume และ Network
docker volume ls # ดู volume ทั้งหมด docker volume create db_data # สร้าง volume docker network ls # ดู network ทั้งหมด docker network create app-net # สร้าง network ปัญหา: ถ้าทำทุกอย่างด้วยมือ คำสั่งจะยาวมาก
ที่เล่ามาทั้งหมด ถ้าต้องพิมพ์ใน 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 ยาวใช่ไหม แถมพลาดนิดเดียว เช่น ลืมใส่ --network หรือพิมพ์ชื่อ volume ผิด ระบบก็พังทันที แล้วทุกครั้งที่ย้ายเครื่องก็ต้องมานั่งพิมพ์ใหม่อีก
Docker Compose: เขียนครั้งเดียว รันคำสั่งเดียว
ปัญหานี้เลยเป็นที่มาของ Docker Compose
Docker Compose คือไฟล์ตั้งค่า (YAML) ที่เราเขียนโครงสร้างระบบไว้ว่ามี service อะไรบ้าง ใช้ image ไหน เปิด port อะไร ต่อ volume กับ network ยังไง จากนั้นสั่ง คำสั่งเดียว ระบบก็ขึ้นครบทั้งชุด
ตัวอย่างโครงสร้างโปรเจกต์
my-project/ ├── api/ │ ├── src/index.js │ ├── package.json │ ├── Dockerfile │ └── .dockerignore ├── web/ │ ├── src/... │ ├── package.json │ ├── Dockerfile │ └── .dockerignore ├── .env └── compose.yaml 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 .dockerignore (ใส่ทั้งใน api และ web)
กันไม่ให้ไฟล์ไม่จำเป็นถูก copy เข้าไปใน image
node_modules dist .env .git .env
เก็บรหัสผ่านแยกไว้ อย่า commit ขึ้น Git (ใส่ไว้ใน .gitignore)
DB_ROOT_PASSWORD=changeme 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: คำสั่ง 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 ด้วย = ข้อมูลหายหมด! รันแล้วเปิดดูได้เลย
- หน้าเว็บ →
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 เวอร์ชันไหน" อีกแล้ว 🐳




