Skip to content

บทที่ 5 — Deployment (Docker + Nginx + Docker Compose)

← บทที่ 4 | สารบัญ

บทสุดท้าย — เอาแอปทั้ง stack ขึ้นจริง

หลังจบบท คุณจะมี:

  • Dockerfile ของ backend (Spring Boot)
  • Dockerfile ของ frontend (React) + nginx
  • docker-compose.yml ที่ run ทุก service ด้วยกัน
  • Nginx ที่ทำ reverse proxy + serve static + handle SSL
  • เข้าใจ env var (เอ็นว์-วาร์ ย่อจาก environment variable เอน-ไว-รอน-เม้นต์ วา-ริ-เอ-เบิ้ล = ตัวแปรสภาพแวดล้อม ใช้ส่ง config/secret เข้าโปรแกรม) management ใน production
  • รู้ว่า deploy ที่ไหนได้บ้าง — ตัวเลือกหลักในปี 2026:
    • VPS (วี-พี-เอส = Virtual Private Server เวอร์-ชวล ไพร-เวท เซิร์ฟ-เวอร์) — เซิร์ฟเวอร์เสมือนที่เราเช่ามาคุมเองทั้งเครื่อง เช่น Hetzner, DigitalOcean, Linode
    • Render (เรน-เดอร์) — PaaS แบบ git push deploy
    • Railway (เรล-เวย์) — PaaS แบบ pay-as-you-go
    • Fly.io (ฟลาย-ดอท-ไอ-โอ) — deploy ใกล้ user ทั่วโลก
    • Vercel (เวอร์-เซลล์) / Netlify (เน็ต-ลิ-ฟาย) / Cloudflare Pages — frontend + serverless functions
    • Coolify (คูล-ลิ-ฟาย) / Dokploy — self-host แบบ Vercel-alternative บน VPS เราเอง
  • CI/CD ด้วย GitHub Actions

⚠️ อ่านก่อน — บทนี้กว้างมาก ตั้งความคาดหวังให้ถูก

Deployment แตะ "หลายโดเมนใหญ่" พร้อมกัน (Docker, Nginx, CI/CD, cloud, monitoring, backup) แต่ละอันมีหมวด/หนังสือของตัวเอง — บทนี้ไม่ได้สอนแต่ละอันตั้งแต่ศูนย์ แต่ "ร้อยทุกอย่างเข้าด้วยกัน" ให้เห็นว่า fullstack app ขึ้น production ยังไง

วิธีอ่านตามระดับ:

  • ทำตามได้จริงบนเครื่องตัวเอง (Part 1–4: Dockerfile + Nginx + Docker Compose) — เป้าหมายรอบแรกคือ "รัน fullstack app ด้วย docker compose up ให้ขึ้นครบทุก service"
  • ทำได้ถ้ามีของ (Part 5 HTTPS, Part 6 Deploy บน VPS/Railway, Part 7 CI/CD) — ต้องมี server/domain/GitHub repo
  • เป็น reference ไว้กลับมาทีหลัง (Part 8 Monitoring, Part 9 Backup) — มือใหม่อ่านผ่าน ๆ เอาว่ามีอะไรบ้างก็พอ

📋 Prerequisite

  • ✅ ผ่าน Full-stack บท 1–4 (มี React + Spring Boot app ที่รันได้)
  • ✅ พื้นฐาน Docker — บทนี้ใช้ Docker/Compose หนัก ถ้ายังไม่เคยแตะเลยจะงงตั้งแต่ Part 1
    • แนะนำอ่าน Docker บท 0–4 ก่อน
    • หรือถ้าจะลุยเลย ติดตั้ง Docker Desktop (Windows/Mac) / Docker Engine (Linux) จาก https://docs.docker.com/get-docker/
    • ทดสอบว่าใช้งานได้: docker run hello-world ต้องผ่าน
  • 🌱 ถ้าเพิ่งเริ่ม: ทำให้ถึง Part 4 (Docker Compose รันครบ) ก่อน — cloud/CI/CD/monitoring ค่อยกลับมา

1. Production Architecture (Recap)

บทสุดท้ายจะ deploy ทั้ง stack ขึ้น production จริง — ก่อนลงมือ ทบทวนภาพรวมสถาปัตยกรรม: nginx เป็นด่านหน้า (static + reverse proxy + SSL), Spring Boot เป็น backend, PostgreSQL เป็น DB ทุกอย่างห่อใน container เพื่อให้ deploy ที่ไหนก็เหมือนกัน:

📌 DB ไม่ควรเชื่อมตรงจาก edge nginx — PostgreSQL อยู่บน internal Docker network เท่านั้น เข้าถึงผ่าน backend เป็น single entry point ลด attack surface


Part 1: Backend Dockerfile

2. Multi-stage Build (มัล-ติ-สเตจ บิวด์)

📚 Dockerfile syntax พื้นฐาน (สำหรับมือใหม่)

ถ้าไม่เคยเขียน Dockerfile มาก่อน อ่านสรุปคำสั่งหลักก่อน:

  • FROM <image> — เริ่มจาก base image (เช่น eclipse-temurin:21-jdk = Linux ที่มี Java 21 ติดตั้งแล้ว)
  • WORKDIR /path — เซ็ตโฟลเดอร์ทำงาน (เหมือน cd)
  • COPY src dest — copy ไฟล์จากเครื่องเรา (build context) เข้าไปใน image
  • RUN <cmd> — รันคำสั่ง shell ตอน build image (เช่น RUN ./mvnw package)
  • EXPOSE <port> — บอกว่า container จะเปิด port อะไร (เป็น documentation เท่านั้น)
  • USER <name> — เปลี่ยน user ที่ใช้รัน process
  • ENTRYPOINT [...] — คำสั่งที่รันตอน docker run (entry point ของ container)
  • AS <name> ต่อท้าย FROM — ตั้งชื่อ stage เพื่อ reference ทีหลัง (--from=build)

Reference: https://docs.docker.com/reference/dockerfile/

dockerfile
# backend/Dockerfile

# ─── Stage 1: Build ────────────────────────────────────────
FROM eclipse-temurin:21-jdk AS build

WORKDIR /workspace

# โหลด dependency ของ Maven แยกเป็น layer เดียว เพื่อให้ Docker cache ไว้ (build ครั้งหน้าเร็วขึ้น)
COPY .mvn .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -B

# build โปรเจกต์เป็นไฟล์ jar
COPY src src
RUN ./mvnw clean package -DskipTests -B

# แยก jar ออกเป็น layer ย่อย ๆ (Spring Boot layered JAR — ดูคำอธิบายข้างล่าง)
# Reference: https://docs.spring.io/spring-boot/reference/packaging/efficient.html
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted


# ─── Stage 2: Runtime ──────────────────────────────────────
FROM eclipse-temurin:21-jre

WORKDIR /app

# Copy layers ทีละชั้น (เปลี่ยน code → invalidate แค่ layer สุดท้าย)
COPY --from=build /workspace/target/extracted/dependencies/ ./
COPY --from=build /workspace/target/extracted/spring-boot-loader/ ./
COPY --from=build /workspace/target/extracted/snapshot-dependencies/ ./
COPY --from=build /workspace/target/extracted/application/ ./

# สร้าง user ที่ไม่ใช่ root แล้วรันด้วย user นี้ (เพื่อความปลอดภัย — ถ้าโดนเจาะจะเสียหายจำกัด)
RUN groupadd -r app && useradd -r -g app app
USER app

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "org.springframework.boot.loader.launch.JarLauncher"]

📌 TL;DR สำหรับมือใหม่ — บรรทัด ENTRYPOINT คือคำสั่งที่ container รันตอนเริ่มทำงาน เรียก Java โหลด Spring Boot app ส่วน -XX:MaxRAMPercentage=75 บอกให้ JVM ใช้ memory ได้ 75% ของ container limit รายละเอียดเวอร์ชัน Spring Boot ดูในกล่องด้านล่าง (อ่านรอบสองก็ได้)

🔎 รายละเอียดเชิงลึก — JarLauncher (อ่านรอบสอง)

เมื่อ extract layered JAR แล้วโฟลเดอร์ที่ได้คือ spring-boot-loader/ (มี class files) ไม่ใช่ไฟล์ .jar ที่รันด้วย -jar ตรง ๆ ได้ ต้องเรียก class JarLauncher ที่ Spring Boot ทำมาเพื่อโหลด nested jars

  • Spring Boot 3.2+org.springframework.boot.loader.launch.JarLauncher
  • Spring Boot 3.0–3.1 หรือ 2.x → org.springframework.boot.loader.JarLauncher (ไม่มี .launch.)

ดูเวอร์ชันใน pom.xml แล้วเลือกให้ตรง ถ้าเลือกผิดจะเจอ Error: Could not find or load main class

เกี่ยวกับ JVM flag-XX:+UseContainerSupport (ให้ JVM อ่าน container memory limit) เปิด default ตั้งแต่ JDK 10+ จึงไม่ต้องใส่ซ้ำ ตัวที่ควรใส่จริงคือ -XX:MaxRAMPercentage=75 (กำหนดสัดส่วน heap ต่อ container memory) — สำหรับ container เล็กมาก (256-512 MB) ค่า 75% อาจสูงเกินไปทำให้ metaspace/OS ขาด memory ปรับเป็น 50-65% หรือเซ็ต -Xmx ตรงๆ แทน

🆕 Java 25 LTS + Distroless (สำหรับ production)

JDK 25 LTS ออก ก.ย. 2025 — สามารถเปลี่ยน base image เป็น eclipse-temurin:25-jdk / 25-jre ได้

สำหรับ production ที่ต้องการ attack surface เล็กที่สุด ใช้ distroless image (มีแค่ Java runtime ไม่มี shell/apt) หรือ Chainguard / Wolfi images:

dockerfile
# Distroless example (Google)
FROM gcr.io/distroless/java25-debian12
# หรือ Chainguard (zero-CVE focus)
# FROM cgr.dev/chainguard/jre

ข้อดี: ภาพเล็กกว่า (50-80 MB), ไม่มี shell ให้ attacker exploit, CVE น้อย
ข้อเสีย: debug ยากขึ้น (ไม่มี sh), HEALTHCHECK ที่ใช้ curl/wget รันไม่ได้ — ต้องใช้ exec ตรงเข้า Java app หรือใช้ Kubernetes livenessProbe แทน

อีกทางเลือก: jlink custom runtime — สร้าง JRE เฉพาะ module ที่ app ใช้ ขนาดเล็กที่สุด

ทำไม Multi-stage

  • Stage 1 (build) = ต้องการ JDK + Maven (~500 MB)
  • Stage 2 (runtime) = ต้องการแค่ JRE (~200 MB)
  • Final image = แค่ stage 2

ทำไม Layered JAR

Spring Boot 2.3+ แยก fat jar เป็น layer:

  • dependencies — library (เปลี่ยนน้อยที่สุด)
  • spring-boot-loader — loader
  • snapshot-dependencies — snapshot
  • application — code เรา (เปลี่ยนบ่อยที่สุด)

Docker cache แต่ละ layer → เปลี่ยน code → rebuild เร็ว


3. .dockerignore

.dockerignore บอก Docker ว่าไฟล์ไหนไม่ต้องส่งเข้า build context — ตัด target/, .git/, IDE config ออก ทำให้ build เร็วขึ้นและกัน secret หลุดเข้า image:

# backend/.dockerignore
target/
.idea/
.vscode/
*.iml
.git/
.gitignore
Dockerfile
*.md

ทำให้ build context เล็กลง → upload ไป Docker daemon เร็ว


4. Build + Run

ลอง build image แล้วรันในเครื่องก่อน deploy จริง — ส่ง config ผ่าน env var (DATABASE_URL, JWT_SECRET) ไม่ hardcode ลง image เพื่อให้ image เดียวรันได้ทุก environment:

bash
cd backend
docker build -t myapp-backend:latest .

docker run -p 8080:8080 \
    -e DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/appdb \
    -e DATABASE_USER=app \
    -e DATABASE_PASSWORD=secret \
    -e JWT_SECRET=mysecret \
    myapp-backend:latest

5. Spring Boot — Build Image (alternative)

ถ้าไม่อยากเขียน Dockerfile เอง Spring Boot มี build-image ที่ใช้ Cloud Native Buildpacks สร้าง image ที่ optimize + secure ให้อัตโนมัติ:

แทน Dockerfile — ใช้ spring-boot:build-image:

bash
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp-backend:latest

ใช้ Cloud Native Buildpacks → image optimized + secure + ไม่ต้องเขียน Dockerfile


Part 2: Frontend Dockerfile

6. React + Nginx

React เป็น static site หลัง build (HTML/JS/CSS) จึงไม่ต้องมี Node ใน production — multi-stage build: stage แรกใช้ Node build, stage สองก๊อปไฟล์ static ลง nginx:

dockerfile
# frontend/Dockerfile

# ─── Stage 1: Build ────────────────────────────────────────
FROM node:22-alpine AS build

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build


# ─── Stage 2: Nginx ────────────────────────────────────────
FROM nginx:1.27-alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

# รันด้วย non-root user (nginx image มี user `nginx` อยู่แล้ว)
# หมายเหตุ: ถ้าใช้ port < 1024 ต้อง bind capability — nginx:alpine 1.25+ มี config รองรับแล้ว
EXPOSE 80

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1

CMD ["nginx", "-g", "daemon off;"]

💡 Node 22 LTS เป็นค่า default ในปี 2026 — Node 20 เข้า maintenance mode ปลายปี 2026
ทางเลือก package manager: pnpm (เร็ว+ประหยัดพื้นที่ disk) หรือ bun (runtime+package manager รวดเร็ว) ถ้า project ตั้งค่าไว้


7. nginx.conf (Frontend container)

📚 nginx config syntax พื้นฐาน (สำหรับมือใหม่)

ถ้าไม่เคยเขียน nginx config:

  • server { ... } = หนึ่ง virtual host (ฟัง 1 port + 1 domain)
  • location <path> { ... } = match URL path (เช่น location /api/ match ทุก URL ที่ขึ้นด้วย /api/)
  • location ~* regex = match แบบ regex (case-insensitive)
  • try_files = หาไฟล์ตามลำดับ ถ้าไม่เจอใช้ตัวสุดท้าย
  • add_header ... always = ส่ง header กลับเสมอ (รวม error response)
  • expires 1y = ตั้ง Cache-Control ให้ browser cache 1 ปี

Reference: https://nginx.org/en/docs/beginners_guide.html

nginx
# frontend/nginx.conf
server {
    listen 80;
    server_name _;
    
    root /usr/share/nginx/html;
    index index.html;
    
    # gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_min_length 1000;
    
    # Cache hashed assets ยาว ๆ — Vite ใส่ hash ในชื่อไฟล์ (เช่น app-a1b2c3.js)
    # เปลี่ยน code → hash เปลี่ยน → URL ใหม่ → ไม่ stale แน่นอน
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
    }
    
    # ⚠️ ไฟล์ใน public/ (favicon.ico, robots.txt, ฯลฯ) Vite "ไม่" hash
    # cache สั้น ๆ พอ ไม่ต้อง immutable — ไม่งั้นเปลี่ยน favicon แล้ว user เห็นของเก่า 1 ปี
    location = /favicon.ico { expires 1d; add_header Cache-Control "public"; }
    location = /robots.txt  { expires 1d; add_header Cache-Control "public"; }
    
    # SPA fallback — ทุก route → index.html
    # index.html ห้าม cache เด็ดขาด ไม่งั้น deploy ใหม่แล้ว user ค้างหน้าเก่า
    location / {
        try_files $uri $uri/ /index.html;
    }
    location = /index.html {
        add_header Cache-Control "no-store, must-revalidate";
    }
    
    # ─── Security headers (2026 baseline) ─────────────────────
    # X-Frame-Options: ป้องกัน clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;
    # ห้าม browser เดา MIME type
    add_header X-Content-Type-Options "nosniff" always;
    # ⚠️ ไม่ใส่ X-XSS-Protection — header นี้ deprecated แล้ว (Chrome เลิก XSS auditor ปี 2019, MDN/OWASP บอกไม่ให้ตั้ง) ในบาง edge case ทำให้เกิด XSS เพิ่มด้วยซ้ำ ใช้ CSP แทน
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    # Permissions-Policy: ปิด feature ที่ไม่ได้ใช้
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
    # CSP — ตัวอย่าง strict (ปรับ source ให้ตรงกับ app)
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
    # HSTS ตั้งใน edge nginx (server ที่มี TLS) ไม่ใช่ตรงนี้
}

💡 CSP เริ่มแบบ Report-Only ก่อน — ถ้าเปิด Content-Security-Policy แบบ enforce ทันที อาจพัง inline script/style ที่มี ใช้ Content-Security-Policy-Report-Only ก่อนเพื่อดู violation ใน DevTools → console แล้วค่อยปรับ → enforce

ทำไม SPA Fallback?

User เข้า /users/123 ตรง ๆ → nginx หา file /users/123 → ไม่เจอ → ต้อง return index.html แทน (React Router handle URL ที่ client)


8. .dockerignore (Frontend)

node_modules/
dist/
.git/
.idea/
.vscode/
*.log
.env
.env.local
Dockerfile

Part 3: Edge Nginx — Reverse Proxy

มี 2 approach:

A. แต่ละ container มี nginx ของตัวเอง + 1 nginx ภายนอกB. 1 nginx ภายนอก จัดการทุกอย่าง

ใช้ A สำหรับ flexibility — frontend container = nginx ที่ serve static, edge nginx ทำ TLS + routing


9. Edge nginx.conf

💡 edge nginx = nginx ตัวที่อยู่ขอบนอกสุด รับ request จากอินเทอร์เน็ตก่อนใคร จัดการ TLS (Transport Layer Security — โพรโทคอลเข้ารหัส HTTPS; ชื่อเก่าคือ SSL), redirect HTTP→HTTPS, route /api ไป backend และอื่น ๆ ไป frontend ทำให้ทั้งระบบอยู่ใต้ domain เดียว ไม่มี CORS

💡 มือใหม่อ่านเป็น template — config ข้างล่างมีรายละเอียดเยอะ (upstream, TLS, X-Forwarded-*) รอบแรกใช้ตามที่เห็นได้เลย แล้วกลับมาอ่านทีหลังเมื่อต้องปรับ

Config ที่ใช้งานได้จริง (nginx 1.25+):

nginx
# edge/nginx.conf
upstream frontend {
    server frontend:80;
}

upstream backend {
    server backend:8080;
}

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;
    
    # ACME challenge สำหรับ Let's Encrypt renew (ต้องผ่าน port 80)
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    
    # HTTP → HTTPS redirect ที่เหลือทั้งหมด
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    # ⚠️ nginx 1.25+ : `listen 443 ssl http2;` deprecated — แยก `http2 on;` ออกมาเป็น directive ของตัวเอง
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    # http3 on;                       # ถ้าใช้ nginx 1.25.3+ และ build รองรับ QUIC
    # listen 443 quic reuseport;      # HTTP/3
    server_name app.example.com;
    
    # ─── TLS (Mozilla Intermediate profile, 2026) ──────────────
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    # ECDHE only — drop legacy ciphers
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;
    
    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/app.example.com/chain.pem;
    resolver 1.1.1.1 9.9.9.9 valid=60s;
    resolver_timeout 5s;
    
    # HSTS (เปิดที่ edge เพราะมี TLS) — 1 ปี + subdomain + preload
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    
    # Upload limit
    client_max_body_size 20M;
    
    # ─── Backend API ───────────────────────────────────────────
    location /api/ {
        proxy_pass http://backend;
        # ครบชุด X-Forwarded-* — Spring Boot ForwardedHeaderFilter ใช้สร้าง URL ที่ถูกต้อง
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Port  $server_port;
        
        # Timeout สำหรับ slow endpoint
        proxy_read_timeout 60s;
        proxy_connect_timeout 60s;
    }
    
    # Server-sent events (เผื่อใช้)
    location /api/events {
        proxy_pass http://backend;
        proxy_set_header Connection '';
        proxy_http_version 1.1;
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 24h;
    }
    
    # Frontend (default)
    location / {
        proxy_pass http://frontend;
        proxy_set_header Host             $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

📌 ssl_ciphers + ssl_prefer_server_ciphers — ใช้ profile จาก Mozilla SSL Configuration Generator แทนที่จะแก้เอง (อัปเดตให้ตามมาตรฐานใหม่อัตโนมัติ) — profile "Intermediate" รองรับ browser ส่วนใหญ่ "Modern" ใช้ TLS 1.3 อย่างเดียว (ดรอป browser เก่า)

HSTS preloadmax-age=31536000; includeSubDomains; preload ส่งเข้า hstspreload.org ได้ ⚠️ ส่งแล้ว rollback ยาก (domain ติด preload list ของ browser) ทดสอบให้แน่ก่อน

HTTP/3 (QUIC) — nginx 1.25.3+ มี http3 on; + listen 443 quic (ต้อง build รองรับ QUIC) เปิดได้ถ้า browser ผู้ใช้ยุคปัจจุบันรองรับ (Chrome/Edge/Safari ส่วนใหญ่ทำได้แล้วในปี 2026)

💡 ทางเลือก: Caddy auto-TLS — ถ้าไม่อยากจัดการ certbot/nginx เอง Caddy ออก cert + renew ให้อัตโนมัติด้วย config 3 บรรทัด:

caddy
app.example.com {
    reverse_proxy /api/* backend:8080
    reverse_proxy frontend:80
}

เหมาะกับ side project ที่ไม่ต้องการ tune nginx ละเอียด


Part 4: Docker Compose

10. docker-compose.yml

docker-compose ผูกทุก service (db, backend, frontend, edge, certbot) เข้าด้วยกันในไฟล์เดียว รันคำสั่งเดียวก็ได้ทั้ง stack:

💡 healthcheck = คำสั่งเช็คว่า service ยังทำงานปกติไหม · depends_on = ให้ backend รอ db พร้อมก่อน · volume = พื้นที่เก็บข้อมูลถาวรนอก container (ไฟล์ DB ไม่หายตอนลบ container) · restart policy = นโยบาย restart อัตโนมัติเมื่อ service ล่ม

📌 $$ คืออะไร — ฉบับสั้น: docker-compose แทน $VAR ของตัวเอง (จาก .env) ตอน parse ไฟล์ ถ้าอยากให้ shell ใน container เห็น $VAR (ไม่ใช่ compose) ให้ escape เป็น $$VAR (compose แทน $$$ แล้ว shell ค่อยอ่านต่อ)

ใช้กรณีไหน: เมื่อเรียกตัวแปรที่ตั้งโดย container เอง (เช่น POSTGRES_USER ที่ image postgres ตั้งให้) หรือตัวแปรของ shell ($! = PID ของ background process ล่าสุด)

yaml
# docker-compose.yml

# 2026 best practice: ใส่ name แทนที่ version (`version:` ถูก deprecated ใน Compose v2)
name: myapp

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  backend:
    build: ./backend
    image: myapp-backend:latest
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DATABASE_URL: jdbc:postgresql://db:5432/${DB_NAME}
      DATABASE_USER: ${DB_USER}
      DATABASE_PASSWORD: ${DB_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}
      CORS_ORIGINS: ${CORS_ORIGINS}
    volumes:
      - uploads:/app/uploads
    depends_on:
      db:
        condition: service_healthy
    # ⚠️ ใช้ wget แทน curl — `eclipse-temurin:21-jre` ไม่ติด curl มาให้
    # ถ้าใช้ distroless image ใส่ HEALTHCHECK ตรงใน Dockerfile (CMD รัน Java ตรง) แทน
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/actuator/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s
    restart: unless-stopped

  frontend:
    build: ./frontend
    image: myapp-frontend:latest
    restart: unless-stopped

  edge:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./edge/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
      - uploads:/var/www/uploads:ro          # serve uploaded file
    depends_on:
      - backend
      - frontend
    restart: unless-stopped

  # SSL renewal (Let's Encrypt) — primary pattern: ใช้ --deploy-hook reload nginx อัตโนมัติ
  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
      # ⚠️ docker socket mount = certbot สามารถ control docker บน host ได้ทั้งหมด
      # ถ้ารับความเสี่ยงนี้ไม่ได้ ใช้วิธี cron บน host แทน (ดูคำอธิบายใน Part 5)
      - /var/run/docker.sock:/var/run/docker.sock:ro
    # ติดตั้ง docker CLI ใน certbot container แล้วใช้ --deploy-hook สั่ง reload edge
    entrypoint: "/bin/sh -c 'apk add --no-cache docker-cli && trap exit TERM; while :; do certbot renew --deploy-hook \"docker kill -s HUP myapp-edge-1\" || true; sleep 12h & wait $${!}; done;'"

volumes:
  pgdata:
  uploads:

💡 ชื่อ container myapp-edge-1 — Docker Compose ตั้งชื่อ container ตามรูปแบบ <project>-<service>-<index> เมื่อใส่ name: myapp + service ชื่อ edge + instance ที่ 1 ก็จะได้ myapp-edge-1 ปรับให้ตรงกับชื่อ project ของคุณ


11. .env (Production Secrets)

compose ดึง secret (DB password, JWT secret) จากไฟล์ .env — ⚠️ ไฟล์นี้ห้าม commit เด็ดขาด (ใส่ .gitignore) แต่ commit .env.example ที่มีแค่ชื่อ key ไว้สอนคนใหม่:

bash
# .env (อย่า commit!)
DB_NAME=appdb
DB_USER=app
DB_PASSWORD=very-secret-password-here
JWT_SECRET=super-long-random-string-at-least-256-bits
CORS_ORIGINS=https://app.example.com
bash
# .gitignore
.env
.env.*
!.env.example
bash
# .env.example (commit ตัวนี้แทน)
DB_NAME=appdb
DB_USER=app
DB_PASSWORD=changeme
JWT_SECRET=changeme
CORS_ORIGINS=https://app.example.com

12. Run Stack

เมื่อ compose + .env พร้อม — up -d --build สร้างและรัน, logs -f ดู log, down หยุด ⚠️ ระวัง down -v ที่ลบ volume (ข้อมูล DB หาย!):

bash
# Build + start
docker compose up -d --build

# Logs
docker compose logs -f backend
docker compose logs -f

# Stop
docker compose down

# Stop + remove data (⚠️ ลบ DB)
docker compose down -v

# Rebuild เฉพาะ backend
docker compose up -d --build backend

Part 5: HTTPS — Let's Encrypt

13. Setup ครั้งแรก

HTTPS ฟรีด้วย Let's Encrypt แต่ครั้งแรกต้อง bootstrap ก่อน — รัน nginx แบบ HTTP, ขอ certificate ผ่าน certbot (ต้องชี้ DNS มาที่ server แล้ว) แล้วค่อยเปิด HTTPS หลังจากนั้น certbot service จะ auto-renew ทุก 90 วันให้เอง:

📘 DNS A record คืออะไร (สำหรับมือใหม่)

DNS A record = mapping จากชื่อโดเมน → IP address (IPv4) เช่น app.example.com → 203.0.113.10
เพิ่มได้ในหน้า DNS ของ registrar (ที่ซื้อ domain) — ค้นคำว่า "DNS management" หรือ "Add A record"

ตัวอย่างค่าที่ต้องกรอก:

FieldValue
TypeA
Name / Hostapp (จะได้ app.example.com) หรือ @ (root domain)
Value / TargetIP ของ server (จาก VPS provider)
TTL300 (5 นาที — ตั้งสั้นๆ ตอนทดสอบ)

ตรวจว่า DNS propagate แล้ว: nslookup app.example.com หรือ dig app.example.com ต้องคืน IP ที่ตั้งไว้
(Cloudflare/Route53 propagate ภายในไม่กี่นาที, registrar เก่าบางเจ้าใช้เวลาถึง 24 ชั่วโมง)

bash
# 1. ตั้ง DNS A record: app.example.com → IP server

# 2. Run nginx แบบ HTTP เท่านั้น (comment block 443 ออกก่อน)
docker compose up -d edge

# 3. Generate certificate
docker run --rm \
    -v ./certbot/conf:/etc/letsencrypt \
    -v ./certbot/www:/var/www/certbot \
    certbot/certbot certonly --webroot \
    -w /var/www/certbot \
    -d app.example.com \
    --email you@example.com \
    --agree-tos --non-interactive

# 4. Uncomment block 443 + reload
docker compose restart edge

ทุก 90 วัน — auto renew ผ่าน certbot service ใน docker-compose (entrypoint ใน §10 ใช้ --deploy-hook reload edge nginx ให้แล้ว)

⚠️ certbot renew ≠ nginx ใช้ cert ใหม่ทันที — ต้อง reload

certbot renew แค่เขียนไฟล์ cert ใหม่ลง volume แต่ nginx ยังคงโหลด cert เก่าใน memory จนกว่าจะ reload — ถ้าไม่ทำอะไรเพิ่ม → 90 วันต่อมาผู้ใช้เจอ "certificate expired" ทั้งที่ renew สำเร็จมาตลอด

entrypoint ของ certbot ใน §10 ใช้ --deploy-hook "docker kill -s HUP myapp-edge-1" ที่จะรัน เฉพาะตอน renew สำเร็จ (ปลอดภัยกว่า reload ตลอด)

🔒 ข้อควรระวัง — docker socket mount

วิธีนี้ต้อง mount /var/run/docker.sock เข้า certbot container = certbot สามารถ control docker ทั้งหมดบน host ได้ (รัน container ใหม่, mount path ไหนก็ได้ → escape เป็น root ของ host)

ทางเลือกที่ปลอดภัยกว่า:

  1. Cron บน host — ตั้ง cron job บน host เรียก docker compose exec edge nginx -s reload หลัง renew (certbot รันโดยไม่ต้อง mount socket)
    cron
    0 3 * * * docker compose -f /opt/myapp/docker-compose.yml run --rm certbot renew --deploy-hook "docker compose -f /opt/myapp/docker-compose.yml exec edge nginx -s reload"
  2. systemd timer บน host (Linux modern) — เหมือน cron แต่จัดการ log/dependency ได้ดีกว่า
  3. เปลี่ยนไปใช้ Caddy — auto-TLS ในตัว ไม่ต้องจัดการ certbot+nginx reload เลย

Part 6: Deploy ที่ไหน

14. ตัวเลือก

⚠️ ราคา/free tier ของ PaaS เปลี่ยนบ่อย — ตรวจจากเว็บจริงก่อนเลือก

Railway ยกเลิก free plan ปี 2023, Fly.io ปรับ pricing ปี 2024, Render ปรับนโยบาย free tier หลายครั้ง — ตารางด้านล่างเป็น โครงเปรียบเทียบเชิงคุณภาพเท่านั้น ไม่มี hard number ราคา ต้องเปิดหน้า pricing ของแต่ละเจ้าตอนตัดสินใจ (ตรวจครั้งสุดท้าย: 2026-06)

ต้องการ source — ตรวจครั้งสุดท้าย 2026-06

Cost levelEasyScaleTech
VPS (Hetzner, Linode, DigitalOcean)ต่ำ (flat monthly)⚪ ต้อง setup เอง⚪ manualfull control
Railway (เรล-เวย์)pay-as-go✅ ง่ายมาก✅ autogit push deploy
Render (เรน-เดอร์)free tier เปลี่ยนบ่อย + paid✅ ง่าย✅ autosimilar to Railway
Fly.io (ฟลาย-ดอท-ไอ-โอ)pay-as-go✅ deploy ใกล้ user✅ edgeglobal, Firecracker VM
Vercel (เวอร์-เซลล์)free + paid✅ ง่ายสุดfrontend + serverless/edge functions (full-stack ได้ผ่าน serverless)
Netlify (เน็ต-ลิ-ฟาย)free + paid✅ ง่ายคล้าย Vercel — frontend + functions
Cloudflare Pages + Workersfree generous✅ edge globalstatic + edge runtime
Coolify (คูล-ลิ-ฟาย)ฟรี (self-host)⚪ ต้อง VPS เองopen-source Vercel alternative — UI สวย รัน Docker stack
Dokployฟรี (self-host)⚪ ต้อง VPS เองคล้าย Coolify — UI deploy บน VPS ของเรา
AWS / GCP / Azurevaries, ซับซ้อน⚫ ซับซ้อน✅ enterpriseECS / EKS / Cloud Run / Container Apps

Database hosting ปี 2026 (ถ้า deploy frontend บน PaaS แบบ serverless ที่ไม่มี DB):

ServiceTypeจุดเด่น
NeonPostgres serverlessbranching เหมือน git, free tier ใจดี
SupabasePostgres + Auth + Storageopen-source, all-in-one BaaS
TursoSQLite (libSQL) edgereplicate ทั่วโลก ใกล้ user
CockroachDB Serverlessdistributed SQLscale แนวนอน global
Railway / Render Postgresmanaged Postgresรวมกับ app deploy ได้ในที่เดียว

คำแนะนำมือใหม่ (เช็คราคา/free tier official ก่อนตัดสินใจ):

  • Frontend อย่างเดียว (SPA + API call ไป backend อื่น) → Vercel / Netlify / Cloudflare Pages
  • Side project full-stack → Railway / Render (deploy + DB ในที่เดียว)
  • Self-host บน VPS ที่อยากมี UI สวยเหมือน Vercel → Coolify / Dokploy
  • Production (budget) → Hetzner / DigitalOcean VPS + docker-compose
  • Production (scale) → AWS ECS / Cloud Run / Fly.io

15. Deploy บน VPS (ง่ายสุด)

deploy บน VPS ให้ control เต็มที่และถูก ($5/เดือน) — SSH เข้า server, ติดตั้ง Docker, clone repo, ตั้ง .env แล้ว docker compose up:

bash
# SSH เข้า server
ssh user@your-server

# Install Docker
curl -fsSL https://get.docker.com | sh

# Clone repo
git clone https://github.com/you/myapp.git
cd myapp

# Copy + edit .env
cp .env.example .env
nano .env

# Run
docker compose up -d --build

# ดู status
docker compose ps

Update (เมื่อ push code ใหม่)

bash
cd myapp
git pull
docker compose up -d --build

16. Deploy บน Railway (ง่ายที่สุด)

ถ้าไม่อยากดูแล server เอง Railway คือทางที่ง่ายสุด — push code ขึ้น GitHub, ต่อ repo, มันตรวจ Dockerfile + deploy ให้เอง พร้อม managed PostgreSQL:

  1. Push code ขึ้น GitHub
  2. ไป https://railway.app → New Project → Deploy from GitHub
  3. เลือก repo → Railway detect ภาษา + Dockerfile อัตโนมัติ
  4. Add PostgreSQL service
  5. Set environment variables
  6. Done — Railway provide URL

Part 7: CI/CD — GitHub Actions

17. Workflow

deploy ด้วยมือทุกครั้งเสี่ยงพลาดและช้า — CI/CD ทำให้ทุก push เข้า main จะ test → build image → deploy อัตโนมัติ:

💡 มือใหม่อ่านเป็น template — YAML ข้างล่างมี syntax เฉพาะ GitHub Actions เยอะ (${{ ... }}, secrets.*, needs:) รอบแรกใช้ตามที่เห็นก่อน แล้วอ่านเพิ่ม:

  • Documentation: https://docs.github.com/en/actions/learn-github-actions
  • ${{ secrets.X }} = ค่าจาก Settings → Secrets and variables → Actions ของ repo
  • ${{ github.repository }} = ชื่อ repo อัตโนมัติ (เช่น you/myapp)
  • needs: test = job นี้รอ job test เสร็จก่อน
yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

# 2026 best practice: ระบุ permission ขั้นต่ำที่จำเป็น (least privilege)
# packages: write ต้องเปิดเพื่อ push image ขึ้น ghcr.io
permissions:
  contents: read
  packages: write
  id-token: write       # สำหรับ OIDC federation (ดูข้างล่าง)

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      # ⭐ Supply-chain best practice: pin action เป็น commit SHA (ไม่ใช่ tag)
      # tag เปลี่ยน owner ได้ (เคยมี case actions ถูก compromise) — SHA fix ตลอดไป
      # ดู SHA ที่ถูกต้องจากหน้า release ของ action นั้น ๆ
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      
      - uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
        with:
          java-version: '25'           # JDK 25 LTS (2026)
          distribution: 'temurin'
          cache: maven
      
      - name: Run backend tests
        working-directory: ./backend
        run: ./mvnw test
      
      - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
        with:
          node-version: '22'           # Node 22 LTS (2026)
          cache: 'npm'
          cache-dependency-path: 'frontend/package-lock.json'
      
      - name: Install frontend deps
        working-directory: ./frontend
        run: npm ci
      
      - name: Run frontend tests
        working-directory: ./frontend
        run: npm test -- --run
      
      - name: Type check
        working-directory: ./frontend
        run: npm run type-check
  
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      
      - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
      
      - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      # ⭐ ใช้ SHA-pinned tag (ไม่ใช่ :latest) — แต่ละ deploy ติด commit SHA ที่ trace ได้
      # ถ้า rollback แค่ point compose ไป SHA เก่า ไม่ต้อง rebuild
      - name: Build + push backend
        uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0
        with:
          context: ./backend
          push: true
          tags: |
            ghcr.io/${{ github.repository }}/backend:${{ github.sha }}
            ghcr.io/${{ github.repository }}/backend:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
      
      - name: Build + push frontend
        uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0
        with:
          context: ./frontend
          push: true
          tags: |
            ghcr.io/${{ github.repository }}/frontend:${{ github.sha }}
            ghcr.io/${{ github.repository }}/frontend:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      # ⚠️ SSH key ใน secrets เป็น long-lived credential
      # ทางเลือก modern (ดู §callout ข้างล่าง): GitHub OIDC + Tailscale Action
      - uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.0.3
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          envs: GITHUB_SHA
          script: |
            cd /opt/myapp
            # set image tag เป็น SHA ของ commit นี้ (ผ่าน .env)
            sed -i "s|^IMAGE_TAG=.*|IMAGE_TAG=${GITHUB_SHA}|" .env
            docker compose pull
            docker compose up -d
            docker image prune -f

📦 Prod docker-compose.yml ใช้ image (ไม่ใช่ build)

Compose ใน §10 มีทั้ง build: (สำหรับ dev) และ image: ตรงกันให้ compose build local — บน server prod ลบ build: ออก ใช้แค่ image: ที่ pull จาก ghcr.io ไม่งั้น docker compose pull จะข้าม service ที่มี build:

yaml
# docker-compose.prod.yml (บน server)
services:
  backend:
    image: ghcr.io/you/myapp/backend:${IMAGE_TAG:-latest}
    # ไม่มี build: — pull จาก registry เท่านั้น
    environment: ...
  frontend:
    image: ghcr.io/you/myapp/frontend:${IMAGE_TAG:-latest}

🔐 Modern alternative to long-lived SSH key

  • GitHub OIDC federation — สำหรับ AWS/GCP/Azure ไม่ต้องเก็บ access key ใน secrets เลย workflow แลก temporary token ผ่าน OIDC trust
    • AWS: aws-actions/configure-aws-credentials
    • GCP: google-github-actions/auth
    • Azure: azure/login
  • Tailscale GitHub Action — สำหรับ VPS: workflow join Tailscale network ชั่วคราว → SSH เข้า server ผ่าน Tailscale (ไม่ต้องเปิด SSH ออก public)
  • SSH cert with short TTL — issue SSH cert ผ่าน CA (Smallstep, Vault) อายุ 1 ชั่วโมง แทน key ถาวร

สำหรับการพัฒนาเริ่มต้น SSH key + secrets ใช้ได้ — แต่ Production จริงควรไปทางใดทางหนึ่งข้างต้น

📌 Dependabot สำหรับ pin action updates

เมื่อ pin action เป็น SHA ต้อง manual update เมื่อมี version ใหม่ — ใช้ Dependabot ช่วย:

yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Dependabot จะเปิด PR อัปเดต SHA + เปลี่ยน comment เป็น version ใหม่ให้อัตโนมัติ

⚠️ Deploy แบบนี้มี downtime (~10-30 วินาที)

docker compose up -d จะหยุด container เก่าก่อนสตาร์ทตัวใหม่ ระหว่างนั้น user เจอ 502/503 — สำหรับ side project รับได้ แต่ production ที่ต้อง zero-downtime ต้องใช้:

  • Rolling update บน Docker Swarm/Kubernetes (เปลี่ยนทีละ replica พร้อม readiness probe)
  • Blue-green deploy (เปิด stack ใหม่ข้าง ๆ → switch nginx upstream → ปิดของเก่า)
  • PaaS แบบ Railway/Render ที่ทำ rolling deploy ให้ในตัว

สำหรับ Spring Boot ควรเปิด server.shutdown=graceful (รอ in-flight request เสร็จก่อน shutdown) ช่วยลด request ที่ตกระหว่าง deploy


Part 8: Monitoring + Logging

reference — ไว้กลับมาเมื่อ app ขึ้น production จริง · เจาะลึกในหมวด Observability

18. Logs

หลัง deploy แล้วต้องมองเห็นว่าระบบทำงานยังไง — log คือด่านแรก docker compose logs ดูได้ในเครื่อง แต่ production ควรส่งไปที่เก็บกลาง (Loki/Datadog/CloudWatch) เพื่อค้นและ alert ได้:

bash
# Stream logs
docker compose logs -f backend

# Last 100 lines
docker compose logs --tail=100 backend

# Filter
docker compose logs backend 2>&1 | grep ERROR

ใน production — ส่ง log ไป:

  • Loki + Grafana (self-host) — labels-based, รวมกับ Prometheus/Tempo ใน Grafana
  • OpenTelemetry Collector — collector กลางที่ส่งต่อไป backend อะไรก็ได้ (Loki, Tempo, Datadog) — มาตรฐานของ 2025+
  • Vector (Datadog open-source) — collector ที่เร็วและ config ง่าย
  • OpenObserve — log/metric/trace ใน stack เดียว S3 storage
  • Datadog, New Relic, Honeycomb (SaaS)
  • CloudWatch (AWS), Cloud Logging (GCP), Azure Monitor

19. Health Check + Uptime Monitoring

Backend มี /actuator/health (จาก Spring Boot บทที่ 05)

bash
curl https://app.example.com/api/actuator/health
# {"status":"UP"}

Setup external monitoring:

  • UptimeRobot (ฟรี — ping ทุก 5 นาที)
  • Better Stack (ฟรีเริ่มต้น)
  • Pingdom

20. Metrics — Prometheus + Grafana

log บอกเหตุการณ์ แต่ metrics บอก "แนวโน้ม" (request rate, latency, error rate) — เพิ่ม Prometheus + Grafana ลง compose แล้ว scrape /actuator/prometheus ของ Spring Boot:

💡 scrape คืออะไร — Prometheus ทำงานแบบ "pull": ทุก N วินาที (default 15s) Prometheus วิ่งไป HTTP GET ที่ endpoint ของแต่ละ service (เรียกว่า "scrape") เก็บ metric กลับมา ต่างจาก StatsD/Datadog ที่ app ส่งข้อมูล "push" ออกไปเอง
ข้อดี: Prometheus รู้สถานะ "service ยัง up ไหม" จาก scrape สำเร็จ/ล้มเหลว · ข้อเสีย: ต้องเข้าถึง endpoint ของทุก service ได้ (มักรันใน internal network เดียวกัน)

yaml
# docker-compose.yml — เพิ่ม
prometheus:
  image: prom/prometheus
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
  ports:
    - "9090:9090"

grafana:
  image: grafana/grafana
  ports:
    - "3000:3000"
  volumes:
    - grafana-data:/var/lib/grafana
yaml
# prometheus.yml
scrape_configs:
  - job_name: 'spring-boot'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['backend:8080']

→ Grafana dashboard (ใน Grafana กด Import แล้วใส่เลข 4701 = id ของ dashboard สำเร็จรูปสำหรับ Spring Boot ที่คนทำไว้แชร์ในคลังของ Grafana)

⚠️ Community dashboard อาจล้าสมัย

Dashboard id 4701 ออกแบบสำหรับ Spring Boot + Micrometer Prometheus registry — Micrometer มีการเปลี่ยนชื่อ metric หลายครั้ง (เช่น http.server.requests tag rename) ถ้า import แล้วเจอ "No data" ทุก panel แสดงว่า metric name ไม่ตรงกับ dashboard

ทางเลือก:

  • ลอง dashboard id 11378 (JVM Micrometer) ที่ generic กว่า
  • หรือใช้ Spring Boot Admin / built-in actuator UI
  • หรือสร้าง panel เองจาก metric ใน /actuator/prometheus

ต้องการ source — ตรวจครั้งสุดท้าย 2026-06 (Spring Boot 3.x)


Part 9: Backup

reference — สำคัญมากตอนมี data จริง แต่ยังไม่ต้องทำตอนเรียน

21. PostgreSQL Backup

ข้อมูลใน DB คือสิ่งที่กู้คืนไม่ได้ถ้าหาย — ต้องมี backup อัตโนมัติ ⚠️ อย่าลืม "ทดสอบ restore" จริง ไม่ใช่แค่ backup:

bash
# Manual
docker compose exec db pg_dump -U app appdb > backup-$(date +%Y%m%d).sql

# Restore
cat backup.sql | docker compose exec -T db psql -U app appdb

Automated daily backup

bash
#!/bin/bash
# /opt/myapp/backup.sh

DATE=$(date +%Y%m%d-%H%M)
BACKUP_DIR=/var/backups/myapp
mkdir -p $BACKUP_DIR

cd /opt/myapp
docker compose exec -T db pg_dump -U app appdb | gzip > $BACKUP_DIR/db-$DATE.sql.gz

# Keep last 7 days
find $BACKUP_DIR -name "db-*.sql.gz" -mtime +7 -delete

# Upload to S3 (optional)
aws s3 cp $BACKUP_DIR/db-$DATE.sql.gz s3://my-backups/db/
bash
# Cron
0 3 * * * /opt/myapp/backup.sh

Part 10: ⚠️ Production Checklist

□ HTTPS enabled (Let's Encrypt + auto-renew)
□ HTTP → HTTPS redirect
□ Strong passwords (≥ 32 chars) สำหรับ DB + JWT
□ Secrets via env variable, ไม่ commit
□ Non-root user ใน container
□ Image scanned (trivy, snyk)
□ Health check endpoint working
□ Backup automation (DB + uploads)
□ Log aggregation
□ Monitoring + alerts (uptime, error rate)
□ Rate limiting (nginx limit_req)
□ CORS allowed origins ระบุชัด (ไม่ใช่ *)
□ CSP header
□ File upload validation
□ Database connection pool sized correctly
□ JVM heap sized to container memory — ใช้ `-XX:MaxRAMPercentage=75` สำหรับ container ≥1 GB (เหลือ ~25% สำหรับ metaspace + direct buffer + thread stack) · container เล็กกว่า (256-512 MB) ลดเป็น 50-65% เพราะ metaspace/OS ต้องการ memory คงที่ไม่ scale ลงตาม heap · หรือเซ็ต `-Xmx` ตรงๆ · JDK 10+ auto-detect container limit ผ่าน `-XX:+UseContainerSupport` (default เปิดอยู่แล้ว — ไม่ต้องใส่ flag) · พื้นฐาน JVM memory model: https://docs.oracle.com/en/java/javase/21/gctuning/
□ Graceful shutdown (Spring Boot รองรับ default)
□ Rolling update plan

22. Rate Limiting ใน Nginx

ป้องกัน abuse/brute-force ตั้งแต่ชั้น edge ก่อนถึง backend — nginx limit_req จำกัด request ต่อ IP โดย endpoint อ่อนไหวอย่าง login ตั้งเข้มกว่า (5/นาที) ส่วน API ทั่วไปหลวมกว่า (10/วินาที):

nginx
# Edge nginx
http {
    # ⭐ ถ้าอยู่หลัง Cloudflare/LB → ต้อง trust X-Forwarded-For ไม่งั้น $binary_remote_addr
    #    จะกลายเป็น IP ของ proxy เดียวกันทุก request → rate limit ใช้ไม่ได้/บล็อกทุกคนพร้อมกัน
    set_real_ip_from 10.0.0.0/8;              # subnet ของ LB/proxy ภายใน
    # ⚠️ ห้าม hardcode IP ของ Cloudflare/cloud provider ในไฟล์นี้ — เขาเปลี่ยน range เรื่อยๆ
    # ดึงรายการล่าสุดจาก https://www.cloudflare.com/ips/ แล้วใส่ผ่าน include หรือ template
    real_ip_header X-Forwarded-For;
    real_ip_recursive on;

    # rate=10r/s = 10 req/วินาที ต่อ IP
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    # rate=5r/m = 5 req/นาที ต่อ IP (เข้มสำหรับ login กัน brute force)
    # ⚠️ NAT consideration: corporate/mobile users อาจ share IP เดียวกัน (NAT/CGNAT)
    # 5r/m อาจกระทบ legitimate users — ผ่อนเป็น 10-20r/m หรือใช้ key อื่น (เช่น username) สำหรับ critical endpoint
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
}

server {
    location /api/auth/login {
        limit_req zone=login burst=2 nodelay;
        proxy_pass http://backend;
    }

    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://backend;
    }
}

📌 ทำไม real_ip สำคัญ — เมื่อ traffic ผ่าน Cloudflare/AWS ALB/Nginx ตัวหน้า ทุก request จะมี $remote_addr = IP ของ proxy ตัวสุดท้าย (เดียวกันหมด) → rate limit ที่ตั้งเป็น 5r/m จะถูกชนใน 5 request แรกของทั้งระบบ ผลคือ DoS ตัวเอง วิธีแก้คือ trust X-Forwarded-For ที่ proxy ตั้งให้ — แต่ต้อง trust เฉพาะ subnet ของ proxy ของเรา (ไม่งั้นผู้ใช้ใส่ header เองได้ → bypass)


23. Checkpoint

🛠️ Checkpoint 5.1 — Local Compose
รัน full stack ผ่าน docker compose up:

  • DB + Backend + Frontend + Edge nginx
  • เข้า http://localhost → เห็น app ทำงาน
  • API call จาก frontend ผ่าน /api → backend

🛠️ Checkpoint 5.2 — Deploy ไป VPS

  • เช่า Hetzner / DigitalOcean VPS ($5/month)
  • ตั้ง DNS A record
  • SSH + setup Docker
  • Deploy + HTTPS

🛠️ Checkpoint 5.3 — CI/CD

  • ทำ GitHub Action ที่ test + build + deploy auto เมื่อ push main
  • เพิ่ม PR check: type-check + lint + test

🛠️ Checkpoint 5.4 — Monitoring

  • ติด UptimeRobot ping ทุก 5 นาที
  • ติด Grafana + Prometheus → ดู metrics ของ Spring Boot
  • ดู alert ตอน service down

24. สรุปบท

ทบทวนแก่นของบท deployment — Docker multi-stage, edge nginx (proxy+TLS+rate limit), docker-compose orchestration, Let's Encrypt, ตัวเลือก deploy (VPS/Railway/AWS), CI/CD, monitoring และ backup:

✅ Docker multi-stage = image เล็ก + secure
✅ Spring Boot layered JAR → cache efficient
✅ Frontend = nginx serve static + SPA fallback
✅ Edge nginx = reverse proxy + TLS + rate limit
docker-compose.yml orchestrate ทุก service + healthcheck + depends_on
✅ Env variable ใน .env — อย่า commit
✅ Let's Encrypt + certbot = HTTPS ฟรี + auto-renew
✅ Deploy options: VPS (controls) → Railway/Render (easy) → AWS (scale)
✅ CI/CD = GitHub Actions test + build + ssh deploy
✅ Monitoring = uptime + health + metrics + log
✅ Backup = automated + offsite (S3)


📖 Glossary (ศัพท์เฉพาะที่ใช้ในบทนี้)

ศัพท์ที่ผ่านมาทั้งบท พร้อมคำอ่าน (สำหรับ pronunciation) และความหมายสั้น ๆ:

ศัพท์คำอ่านความหมาย
nginxเอน-จิน-เอ็กซ์web server + reverse proxy ที่นิยมที่สุดในวงการ
Certbotเซิร์ต-บอทtool ของ Let's Encrypt สำหรับขอ/ต่ออายุ TLS certificate
Let's Encryptเล็ตส์-เอ็น-คริปต์CA ที่ออก TLS cert ฟรี มี renewal ทุก 90 วัน
VPSวี-พี-เอสVirtual Private Server — เซิร์ฟเวอร์เสมือนที่เช่ามาคุมเอง
Renderเรน-เดอร์PaaS แบบ git push deploy
Railwayเรล-เวย์PaaS แบบ pay-as-you-go
Fly.ioฟลาย-ดอท-ไอ-โอPaaS ที่ deploy ใกล้ user ทั่วโลก
Vercelเวอร์-เซลล์platform สำหรับ frontend + serverless functions
Netlifyเน็ต-ลิ-ฟายคล้าย Vercel — frontend + edge functions
Coolifyคูล-ลิ-ฟายopen-source Vercel alternative ที่ self-host บน VPS ของเราเอง
multi-stage buildมัล-ติ-สเตจ บิวด์Dockerfile หลาย FROM — แยก build env ออกจาก runtime ทำให้ image เล็ก
distrolessดิส-โทร-เลสbase image ที่ตัด shell/package manager ออก เหลือแค่ runtime ลด attack surface
healthcheckเฮลธ์-เช็คคำสั่งเช็คว่า container ยังทำงานปกติไหม
scrapeสเครปPrometheus pull metric จาก endpoint ทุก N วินาที
HSTSเอช-เอส-ที-เอสHTTP Strict Transport Security — บอก browser ให้ใช้ HTTPS เท่านั้น
CSPซี-เอส-พีContent Security Policy — header ป้องกัน XSS
OCSP staplingโอ-ซี-เอส-พี สเตเปิ้ลลิ่งเซิร์ฟเวอร์แนบ proof ว่า cert ไม่ revoke ลดการเช็คของ browser
OIDCโอ-ไอ-ดี-ซีOpenID Connect — auth federation แบบไม่ต้องเก็บ long-lived credential
OTel / OpenTelemetryโอ-เทล / โอ-เพ่น เทเลม-เม-ทรีมาตรฐานเปิดสำหรับ trace/metric/log

🎉 จบ Full-stack Book

หลังจากบทที่ 0-5 คุณ:

  • ✅ ต่อ React กับ Spring Boot ได้
  • ✅ ทำ auth ครบ flow ที่ secure
  • ✅ ทำ CRUD พร้อม optimistic UI
  • ✅ Handle file upload ทั้ง local และ S3
  • ✅ Deploy ขึ้นจริงด้วย Docker + Nginx + HTTPS
  • ✅ มี CI/CD + monitoring

หัวข้อต่อยอด

ทำอะไร
WebSocket / SSEreal-time (chat, notification)
Kafka / RabbitMQevent-driven, decouple service
gRPCRPC ที่เร็วกว่า REST
Microservicesแยก service ตาม domain
Kubernetesorchestration ที่ scale
Observability stackLoki + Tempo + Prometheus + Grafana
Feature flagsLaunchDarkly, GrowthBook
A/B testingdata-driven product

← บทที่ 4 | ← สารบัญ Full-stack | ← สารบัญหลัก


Glossary: ../glossary.md · Style guide: ../CONTRIBUTING.md last_verified: 2026-06-09 · review report: ../REVIEW-2026-06-03.md