โหมดมืด
บทที่ 5 — Deployment (Docker + Nginx + Docker Compose)
บทสุดท้าย — เอาแอปทั้ง 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) เข้าไปใน imageRUN <cmd>— รันคำสั่ง shell ตอน build image (เช่นRUN ./mvnw package)EXPOSE <port>— บอกว่า container จะเปิด port อะไร (เป็น documentation เท่านั้น)USER <name>— เปลี่ยน user ที่ใช้รัน processENTRYPOINT [...]— คำสั่งที่รันตอนdocker run(entry point ของ container)AS <name>ต่อท้ายFROM— ตั้งชื่อ stage เพื่อ reference ทีหลัง (--from=build)
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ตรง ๆ ได้ ต้องเรียก classJarLauncherที่ 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 หรือใช้ KuberneteslivenessProbeแทนอีกทางเลือก: 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— loadersnapshot-dependencies— snapshotapplication— 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:latest5. 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 ปี
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
DockerfilePart 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 preload —
max-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 บรรทัด:
caddyapp.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ที่ imagepostgresตั้งให้) หรือตัวแปรของ 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.combash
# .gitignore
.env
.env.*
!.env.examplebash
# .env.example (commit ตัวนี้แทน)
DB_NAME=appdb
DB_USER=app
DB_PASSWORD=changeme
JWT_SECRET=changeme
CORS_ORIGINS=https://app.example.com12. 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 backendPart 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"ตัวอย่างค่าที่ต้องกรอก:
Field Value Type AName / Host app(จะได้app.example.com) หรือ@(root domain)Value / Target IP ของ server (จาก VPS provider) TTL 300 (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)ทางเลือกที่ปลอดภัยกว่า:
- Cron บน host — ตั้ง cron job บน host เรียก
docker compose exec edge nginx -s reloadหลัง renew (certbot รันโดยไม่ต้อง mount socket)cron0 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"- systemd timer บน host (Linux modern) — เหมือน cron แต่จัดการ log/dependency ได้ดีกว่า
- เปลี่ยนไปใช้ 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 level | Easy | Scale | Tech | |
|---|---|---|---|---|
| VPS (Hetzner, Linode, DigitalOcean) | ต่ำ (flat monthly) | ⚪ ต้อง setup เอง | ⚪ manual | full control |
| Railway (เรล-เวย์) | pay-as-go | ✅ ง่ายมาก | ✅ auto | git push deploy |
| Render (เรน-เดอร์) | free tier เปลี่ยนบ่อย + paid | ✅ ง่าย | ✅ auto | similar to Railway |
| Fly.io (ฟลาย-ดอท-ไอ-โอ) | pay-as-go | ✅ deploy ใกล้ user | ✅ edge | global, Firecracker VM |
| Vercel (เวอร์-เซลล์) | free + paid | ✅ ง่ายสุด | ✅ | frontend + serverless/edge functions (full-stack ได้ผ่าน serverless) |
| Netlify (เน็ต-ลิ-ฟาย) | free + paid | ✅ ง่าย | ✅ | คล้าย Vercel — frontend + functions |
| Cloudflare Pages + Workers | free generous | ✅ | ✅ edge global | static + edge runtime |
| Coolify (คูล-ลิ-ฟาย) | ฟรี (self-host) | ⚪ ต้อง VPS เอง | ⚪ | open-source Vercel alternative — UI สวย รัน Docker stack |
| Dokploy | ฟรี (self-host) | ⚪ ต้อง VPS เอง | ⚪ | คล้าย Coolify — UI deploy บน VPS ของเรา |
| AWS / GCP / Azure | varies, ซับซ้อน | ⚫ ซับซ้อน | ✅ enterprise | ECS / EKS / Cloud Run / Container Apps |
Database hosting ปี 2026 (ถ้า deploy frontend บน PaaS แบบ serverless ที่ไม่มี DB):
| Service | Type | จุดเด่น |
|---|---|---|
| Neon | Postgres serverless | branching เหมือน git, free tier ใจดี |
| Supabase | Postgres + Auth + Storage | open-source, all-in-one BaaS |
| Turso | SQLite (libSQL) edge | replicate ทั่วโลก ใกล้ user |
| CockroachDB Serverless | distributed SQL | scale แนวนอน global |
| Railway / Render Postgres | managed 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 psUpdate (เมื่อ push code ใหม่)
bash
cd myapp
git pull
docker compose up -d --build16. Deploy บน Railway (ง่ายที่สุด)
ถ้าไม่อยากดูแล server เอง Railway คือทางที่ง่ายสุด — push code ขึ้น GitHub, ต่อ repo, มันตรวจ Dockerfile + deploy ให้เอง พร้อม managed PostgreSQL:
- Push code ขึ้น GitHub
- ไป https://railway.app → New Project → Deploy from GitHub
- เลือก repo → Railway detect ภาษา + Dockerfile อัตโนมัติ
- Add PostgreSQL service
- Set environment variables
- 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 นี้รอ jobtestเสร็จก่อน
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/grafanayaml
# 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.requeststag 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 appdbAutomated 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.shPart 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 plan22. 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 ตัวเอง วิธีแก้คือ trustX-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 / SSE | real-time (chat, notification) |
| Kafka / RabbitMQ | event-driven, decouple service |
| gRPC | RPC ที่เร็วกว่า REST |
| Microservices | แยก service ตาม domain |
| Kubernetes | orchestration ที่ scale |
| Observability stack | Loki + Tempo + Prometheus + Grafana |
| Feature flags | LaunchDarkly, GrowthBook |
| A/B testing | data-driven product |
← บทที่ 4 | ← สารบัญ Full-stack | ← สารบัญหลัก
Glossary: ../glossary.md · Style guide: ../CONTRIBUTING.md last_verified: 2026-06-09 · review report: ../REVIEW-2026-06-03.md