article

Docker คือเพื่อนแท้ที่ช่วยแก้ปัญหา "ในเครื่องผมทำงานนะ"

5 min read

วันแรกที่เจอ Docker

ยังจำได้เลยตอนที่เพื่อนแนะนำ Docker มาให้ครั้งแรก พอได้ยิน “Container” กับ “Image” ก็แอบคิดในใจว่า “อะไรเนี่ย ซับซ้อนจัง แค่รันโปรแกรมเองก็ได้แล้ว” 😅

แต่พอเวลาผ่านไปสักพัก แล้วมาเจอปัญหาคลาสสิคที่ทุกคนต้องเจอ:

“ในเครื่องผมทำงานนะ แต่ทำไมไปแล้วไม่ทำงานเลย?”

ตอนนั้นแหละที่เริ่มเข้าใจว่า Docker มันมีไว้แก้ปัญหาอะไร

ประสบการณ์แรกที่ใช้ Docker

โปรเจคแรกที่ใช้ Docker เป็น Node.js app ง่ายๆ เขียน Dockerfile แบบนี้:

# Dockerfile แรกของผม (ไม่ดีเลย)
FROM node:latest

COPY . /app
WORKDIR /app
RUN npm install
CMD ["npm", "start"]

ตอนนั้นคิดว่าเขียนได้แล้ว แต่พอเอาไป build มัน error ไม่รู้กี่ทีเพราะ:

  • ใช้ node:latest (ไม่ควรใช้ latest ในการทำงานจริง)
  • Copy ทุกอย่างรวม node_modules ด้วย
  • ไม่มี .dockerignore

บทเรียนจากความผิดพลาด

1. อย่าใช้ latest tags

# ❌ ไม่ดี - ไม่รู้ได้ Node version ไหน
FROM node:latest

# ✅ ดีกว่า - ระบุ version ชัดเจน
FROM node:18-alpine

เหตุผลง่ายๆ คือถ้าใช้ latest วันนี้อาจจะเป็น Node 18 แต่พรุ่งนี้อาจจะเป็น Node 20 แล้วโค้ดเราอาจจะไม่ทำงาน

2. ใช้ .dockerignore

สร้างไฟล์ .dockerignore เพื่อไม่ให้ copy ของที่ไม่จำเป็น:

node_modules
npm-debug.log
.git
.env
*.md
.DS_Store

ตอนแรกผมไม่รู้เรื่องนี้ เลยมี Docker image ขนาด 2GB เพราะ copy node_modules ขนาด 500MB ไปด้วย 🤦‍♂️

3. Multi-stage builds คือเทพ

หลังจากใช้ Docker ได้สักพัก เริ่มเรียนรู้เรื่อง Multi-stage builds:

# Stage 1: Build dependencies
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build application  
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Production image
FROM node:18-alpine AS production
WORKDIR /app

# Copy only production dependencies
COPY --from=dependencies /app/node_modules ./node_modules
# Copy built application
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./

EXPOSE 3000
CMD ["npm", "start"]

ทำแบบนี้ได้ image ขนาดเล็กลง เพราะไม่เอา dev dependencies ไปด้วย

เคสที่น่าจำ: Database Connection

เคสที่จำได้ชัดที่สุดคือตอนที่แอพใน Docker ติดต่อ database ไม่ได้ สงสัยมากว่าทำไม localhost:5432 เข้าไม่ได้

ปรากฎว่า localhost ใน container มันคือ localhost ของ container นั้น ไม่ใช่ของเครื่องเรา!

วิธีแก้:

1. ใช้ host.docker.internal (บน Windows/Mac)

// แทนที่จะใช้
const dbUrl = 'postgresql://localhost:5432/mydb';

// ให้ใช้
const dbUrl = 'postgresql://host.docker.internal:5432/mydb';

2. ใช้ Docker Compose (แนะนำ)

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/mydb
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=mydb
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

สังเกตว่าใน DATABASE_URL ใช้ db:5432 ไม่ใช่ localhost:5432 เพราะ db คือชื่อ service ใน Docker Compose

เทคนิคที่ช่วยประหยัดเวลา

1. Layer Caching

Docker มี layer caching ที่เจ๋งมาก ถ้าเรียงลำดับ Dockerfile ให้ดี:

FROM node:18-alpine

WORKDIR /app

# Copy package.json ก่อน (ไม่ค่อยเปลี่ยน)
COPY package*.json ./
RUN npm ci

# Copy source code ทีหลัง (เปลี่ยนบ่อย)
COPY . .

CMD ["npm", "start"]

ทำแบบนี้ ถ้าเปลี่ยนแค่ source code จะไม่ต้อง npm install ใหม่

2. ใช้ .env กับ Docker Compose

# docker-compose.yml
services:
  app:
    build: .
    env_file:
      - .env
    ports:
      - "${PORT:-3000}:3000"
# .env
PORT=8080
DATABASE_URL=postgresql://localhost:5432/mydb
NODE_ENV=development

3. Health Checks

เพิ่ม health check เข้าไปใน Dockerfile:

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

หรือใน Docker Compose:

services:
  app:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

ข้อผิดพลาดที่เจอบ่อยๆ

1. Permission Issues

บน Linux เจอปัญหาไฟล์ที่สร้างใน container เป็น root:

# แก้โดยสร้าง user ใหม่
FROM node:18-alpine

# สร้าง user สำหรับรันแอพ
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

USER nextjs

WORKDIR /app
COPY --chown=nextjs:nodejs . .

CMD ["npm", "start"]

2. Time Zone Issues

# ตั้ง timezone
FROM node:18-alpine

RUN apk add --no-cache tzdata
ENV TZ=Asia/Bangkok

WORKDIR /app
# ... rest of Dockerfile

3. Memory Limits

ตอนแรกไม่รู้ เลย build แล้วกิน RAM เต็มเครื่อง:

# docker-compose.yml
services:
  app:
    build: .
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

Docker ในการทำงานจริง

Development Environment

ใช้ Docker Compose เพื่อให้ทีมทำงานใน environment เดียวกัน:

# docker-compose.dev.yml
version: '3.8'
services:
  app:
    build: 
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev123
    ports:
      - "5432:5432"
    volumes:
      - postgres_dev:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_dev:

Production Deployment

สำหรับ production ใช้ multi-stage build:

# Dockerfile.prod
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS runtime
WORKDIR /app

# Security: Don't run as root
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001

COPY --from=deps --chown=nodeuser:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nodeuser:nodejs /app/dist ./dist
COPY --from=build --chown=nodeuser:nodejs /app/package.json ./

USER nodeuser

EXPOSE 3000
CMD ["node", "dist/index.js"]

เทคนิคขั้นสูงที่ใช้จริง

1. Build Arguments

ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine

ARG BUILD_DATE
ARG VERSION
LABEL build_date=$BUILD_DATE
LABEL version=$VERSION
docker build \
  --build-arg NODE_VERSION=20 \
  --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  --build-arg VERSION=1.0.0 \
  -t myapp:1.0.0 .

2. Multi-platform builds

FROM --platform=$BUILDPLATFORM node:18-alpine AS base
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

3. การ optimize image size

FROM node:18-alpine

# รวม RUN commands เพื่อลด layers
RUN apk add --no-cache \
    dumb-init \
    && npm install -g pm2 \
    && addgroup -g 1001 -S nodejs \
    && adduser -S nodeuser -u 1001

# ใช้ dumb-init สำหรับ signal handling
ENTRYPOINT ["dumb-init", "--"]

USER nodeuser
WORKDIR /app

CMD ["pm2-runtime", "start", "ecosystem.config.js"]

Docker ช่วยแก้ปัญหาอะไรบ้าง

1. “ในเครื่องผมทำงานนะ”

ก่อนใช้ Docker:

  • Dev: “ใช้ Node 16 นะ”
  • QA: “เอ๊ะ ผมใช้ Node 14 ทำไมไม่ทำงาน”
  • DevOps: “เซิร์ฟเวอร์ใช้ Node 12…”

หลังใช้ Docker: ทุกคนใช้ environment เดียวกัน ✨

2. Dependency Hell

ก่อน:

npm install
# Error: Python 2.7 required
# Error: node-gyp failed
# Error: sqlite3 compilation failed

หลัง: ทุกอย่างอยู่ใน container แล้ว

3. Microservices Development

ก่อน: ต้องเปิด 5 terminals รัน 5 services

# Terminal 1
cd user-service && npm start

# Terminal 2  
cd order-service && npm start

# Terminal 3
cd payment-service && npm start

# Terminal 4
cd notification-service && npm start

# Terminal 5
cd api-gateway && npm start

หลัง:

docker-compose up

เสร็จ! 🎉

เครื่องมือที่ช่วยให้ชีวิตง่ายขึ้น

1. Docker Desktop

UI ที่ใช้งานง่าย ดูได้ว่า container ไหนรันอยู่ ใช้ resource เท่าไหร่

2. Portainer

Web UI สำหรับจัดการ Docker ใช้ดีมาก:

docker run -d \
  --name portainer \
  -p 9000:9000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  portainer/portainer-ce

3. Dive

เช็คว่า Docker image มี layer อะไรบ้าง ใหญ่แค่ไหน:

dive myapp:latest

4. Hadolint

ตรวจสอบ Dockerfile ว่าเขียนถูกต้องไหม:

hadolint Dockerfile

สรุปประสบการณ์

Docker มันเปลี่ยนวิธีทำงานของผมไปเลย จากที่เคยมีปัญหา:

  • Environment ไม่เหมือนกัน → ตอนนี้ทุกคนใช้ environment เดียวกัน
  • Deploy ยาก → ตอนนี้ build 1 ที รัน ได้ทุกที่
  • Scale ยาก → ตอนนี้เพิ่ม container ได้เท่าที่ต้องการ

แต่ก็ต้องเรียนรู้เรื่อง:

  • Container orchestration (Kubernetes)
  • Security (ไม่รัน container เป็น root)
  • Monitoring (ดู logs, metrics)
  • Storage (volumes, bind mounts)

Docker มันเหมือนมีดในครัว ถ้าใช้เป็น มันช่วยได้เยอะมาก แต่ถ้าใช้ไม่เป็น อาจจะเจ็บตัวเองได้ 😅

สำหรับคนที่กำลังเริ่มต้น แนะนำให้ ลองเล่นก่อน แล้วค่อยๆ เรียนรู้ อย่าเอาไปใช้ใน production ตั้งแต่วันแรก เพราะมันมีหลุมพรางเยอะ

แต่พอเรียนรู้แล้ว การันตีได้เลยว่า ชีวิตจะง่ายขึ้นมาก! 🐳