Medusa Commerce คืออะไรและเหมาะกับธุรกิจแบบไหน
Medusa เป็น Open Source headless commerce platform ที่พัฒนาด้วย Node.js/TypeScript ออกแบบมาให้เป็นทางเลือกแทน Shopify สำหรับธุรกิจที่ต้องการควบคุมระบบ e-commerce ของตัวเองอย่างเต็มที่ Medusa แยก backend (API) ออกจาก frontend ทำให้สามารถใช้ frontend framework อะไรก็ได้เช่น Next.js, Gatsby หรือ Nuxt
Medusa มีฟีเจอร์หลักครบถ้วนสำหรับ e-commerce ได้แก่ Product Management สำหรับจัดการสินค้าและ variants, Order Management สำหรับจัดการคำสั่งซื้อ, Cart และ Checkout flow ที่ปรับแต่งได้, Payment Integration รองรับ Stripe PayPal และอื่นๆ, Shipping Integration รองรับหลาย fulfillment providers, Multi-region support สำหรับขายหลายประเทศ และ Plugin System สำหรับเพิ่มฟังก์ชัน
GitOps เป็นแนวทางการจัดการ infrastructure และ application deployment โดยใช้ Git เป็น single source of truth ทุกการเปลี่ยนแปลงต้องผ่าน Git commit และ Pull Request ทำให้มี audit trail ที่ชัดเจน สามารถ rollback ได้ง่าย และทีมทำงานร่วมกันได้ดี
การรวม Medusa กับ GitOps ช่วยให้การ deploy e-commerce platform เป็นไปอย่างเป็นระบบ ทุก config change ถูกบันทึกใน Git สามารถ review ก่อน apply และ rollback ได้ทันทีถ้ามีปัญหา
ติดตั้ง Medusa Commerce ด้วย Docker Compose
ตั้งค่า Medusa Backend พร้อม PostgreSQL และ Redis
# docker-compose.yml — Medusa Commerce Stack
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: medusa
POSTGRES_PASSWORD: medusa_db_pass
POSTGRES_DB: medusa_db
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U medusa"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
medusa-backend:
build:
context: ./backend
dockerfile: Dockerfile
environment:
NODE_ENV: production
DATABASE_URL: postgres://medusa:medusa_db_pass@postgres:5432/medusa_db
REDIS_URL: redis://redis:6379
JWT_SECRET: your-jwt-secret-key-here
COOKIE_SECRET: your-cookie-secret-key-here
STORE_CORS: "http://localhost:8000, https://store.example.com"
ADMIN_CORS: "http://localhost:7001, https://admin.example.com"
MEDUSA_ADMIN_ONBOARDING_TYPE: default
ports:
- "9000:9000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: sh -c "npx medusa migrations run && npx medusa start"
medusa-admin:
image: node:20-alpine
working_dir: /app
volumes:
- ./admin:/app
ports:
- "7001:7001"
command: npm run dev
depends_on:
- medusa-backend
volumes:
pgdata:
redisdata:
# สร้าง Medusa Backend
# npx create-medusa-app@latest my-store
# cd my-store
# Dockerfile สำหรับ Medusa Backend
cat > backend/Dockerfile << 'DOCKERFILE'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 9000
CMD ["npx", "medusa", "start"]
DOCKERFILE
# docker compose up -d
# ทดสอบ: curl http://localhost:9000/health
# Admin: http://localhost:7001
ตั้งค่า GitOps Workflow ด้วย ArgoCD
ตั้งค่า ArgoCD สำหรับ GitOps deployment ของ Medusa Commerce
# ติดตั้ง ArgoCD บน Kubernetes
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# รอ ArgoCD พร้อม
kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=300s
# ดึง admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Kubernetes Manifests สำหรับ Medusa
# k8s/base/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: medusa-commerce
---
# k8s/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: medusa-backend
namespace: medusa-commerce
spec:
replicas: 2
selector:
matchLabels:
app: medusa-backend
template:
metadata:
labels:
app: medusa-backend
spec:
containers:
- name: medusa
image: myregistry/medusa-backend:v1.0.0
ports:
- containerPort: 9000
envFrom:
- secretRef:
name: medusa-secrets
- configMapRef:
name: medusa-config
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
readinessProbe:
httpGet:
path: /health
port: 9000
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 9000
initialDelaySeconds: 60
periodSeconds: 30
---
# k8s/base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: medusa-backend
namespace: medusa-commerce
spec:
selector:
app: medusa-backend
ports:
- port: 9000
targetPort: 9000
---
# k8s/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: medusa-config
namespace: medusa-commerce
data:
NODE_ENV: "production"
STORE_CORS: "https://store.example.com"
ADMIN_CORS: "https://admin.example.com"
---
# ArgoCD Application
# k8s/argocd/medusa-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: medusa-commerce
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/medusa-k8s.git
targetRevision: main
path: k8s/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: medusa-commerce
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 5s
maxDuration: 3m
# kubectl apply -f k8s/argocd/medusa-app.yaml
สร้าง CI/CD Pipeline สำหรับ Medusa
GitHub Actions Pipeline สำหรับ build, test และ deploy Medusa
# .github/workflows/medusa-ci-cd.yml
name: Medusa Commerce CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: }/medusa-backend
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: medusa_test
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
redis:
image: redis:7-alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Run tests
env:
DATABASE_URL: postgres://test:test@localhost:5432/medusa_test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret
COOKIE_SECRET: test-cookie
run: |
npx medusa migrations run
npm test
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
outputs:
image_tag: }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: }
username: }
password: }
- id: meta
uses: docker/metadata-action@v5
with:
images: }/}
tags: |
type=sha, prefix=
type=raw, value=latest
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: }
update-manifests:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: myorg/medusa-k8s
token: }
- name: Update image tag
run: |
SHORT_SHA=$(echo } | cut -c1-7)
cd k8s/overlays/production
kustomize edit set image myregistry/medusa-backend=}/}:
- name: Commit and push
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "Deploy medusa-backend: }"
git push
จัดการ Database Migration และ Seed Data
สร้าง migration workflow ที่ปลอดภัยสำหรับ production
#!/bin/bash
# scripts/migrate.sh — Safe Database Migration
set -euo pipefail
echo "=== Medusa Database Migration ==="
echo "Environment: "
echo "Database: @***"
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
# 1. Backup ก่อน migrate
echo "[1/4] Creating database backup..."
BACKUP_FILE="/tmp/medusa_backup_$(date +%Y%m%d_%H%M%S).sql"
pg_dump "$DATABASE_URL" > "$BACKUP_FILE"
echo " Backup saved: $BACKUP_FILE ($(du -sh $BACKUP_FILE | cut -f1))"
# 2. รัน migration (dry-run ก่อน)
echo "[2/4] Checking pending migrations..."
npx medusa migrations run --dry-run 2>&1 | tee /tmp/migration_plan.txt
PENDING=$(grep -c "migration" /tmp/migration_plan.txt || true)
echo " Pending migrations: $PENDING"
if [ "$PENDING" -eq 0 ]; then
echo "No pending migrations. Done."
exit 0
fi
# 3. รัน migration จริง
echo "[3/4] Running migrations..."
npx medusa migrations run 2>&1 | tee /tmp/migration_result.txt
if [ $? -ne 0 ]; then
echo "ERROR: Migration failed! Rolling back..."
psql "$DATABASE_URL" < "$BACKUP_FILE"
echo "Rollback complete."
exit 1
fi
# 4. Verify
echo "[4/4] Verifying migration..."
npx medusa migrations run --dry-run 2>&1 | grep -c "migration" && echo " WARNING: Still pending!" || echo " All migrations applied."
echo "=== Migration Complete ==="
# Seed data script
cat > scripts/seed.js << 'SEED'
const { MedusaContainer } = require("@medusajs/medusa");
async function seed() {
const container = await MedusaContainer.init();
const productService = container.resolve("productService");
const regionService = container.resolve("regionService");
// สร้าง Region
const region = await regionService.create({
name: "Thailand",
currency_code: "thb",
tax_rate: 7,
payment_providers: ["manual"],
fulfillment_providers: ["manual"],
countries: ["th"],
});
console.log(`Region created: `);
// สร้าง Product
const product = await productService.create({
title: "Sample T-Shirt",
description: "A comfortable cotton t-shirt",
handle: "sample-tshirt",
is_giftcard: false,
options: [{ title: "Size" }],
variants: [
{ title: "S", prices: [{ amount: 590, currency_code: "thb" }], options: [{ value: "S" }], inventory_quantity: 100 },
{ title: "M", prices: [{ amount: 590, currency_code: "thb" }], options: [{ value: "M" }], inventory_quantity: 100 },
{ title: "L", prices: [{ amount: 590, currency_code: "thb" }], options: [{ value: "L" }], inventory_quantity: 100 },
],
});
console.log(`Product created: `);
}
seed().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
SEED
Monitoring และ Rollback Strategy
ตั้งค่า monitoring และ automated rollback สำหรับ Medusa deployment
#!/usr/bin/env python3
# medusa_monitor.py — Medusa Commerce Health Monitor
import requests
import time
import json
from datetime import datetime
MEDUSA_URL = "http://localhost:9000"
ARGOCD_URL = "http://argocd-server.argocd.svc:443"
ARGOCD_TOKEN = "your-argocd-token"
SLACK_WEBHOOK = "https://hooks.slack.com/services/xxx"
def check_health():
try:
r = requests.get(f"{MEDUSA_URL}/health", timeout=10)
return r.status_code == 200
except Exception:
return False
def check_api():
endpoints = [
"/store/products?limit=1",
"/store/regions",
"/store/collections",
]
results = {}
for ep in endpoints:
try:
r = requests.get(f"{MEDUSA_URL}{ep}", timeout=10)
results[ep] = {
"status": r.status_code,
"latency": r.elapsed.total_seconds(),
"ok": r.status_code == 200
}
except Exception as e:
results[ep] = {"status": 0, "latency": 0, "ok": False, "error": str(e)}
return results
def rollback_argocd(app_name="medusa-commerce"):
headers = {"Authorization": f"Bearer {ARGOCD_TOKEN}"}
# Get current sync history
r = requests.get(
f"{ARGOCD_URL}/api/v1/applications/{app_name}",
headers=headers, verify=False
)
app = r.json()
history = app.get("status", {}).get("history", [])
if len(history) < 2:
print("No previous revision to rollback to")
return False
prev_revision = history[-2]["revision"]
print(f"Rolling back to revision: {prev_revision}")
# Rollback
r = requests.post(
f"{ARGOCD_URL}/api/v1/applications/{app_name}/rollback",
headers=headers,
json={"id": history[-2]["id"]},
verify=False
)
return r.status_code == 200
def notify(message, severity="info"):
icon = {"critical": "🔴", "warning": "🟡", "info": "🟢"}[severity]
print(f"[{icon}] {datetime.now()} {message}")
if SLACK_WEBHOOK and severity != "info":
requests.post(SLACK_WEBHOOK, json={"text": f"{icon} {message}"})
def monitor_loop():
consecutive_failures = 0
max_failures = 3
print(f"Medusa Monitor started: {MEDUSA_URL}")
while True:
healthy = check_health()
api_results = check_api()
all_ok = all(r["ok"] for r in api_results.values())
if healthy and all_ok:
consecutive_failures = 0
latencies = [r["latency"] for r in api_results.values() if r["ok"]]
avg_lat = sum(latencies) / len(latencies) if latencies else 0
if avg_lat > 2.0:
notify(f"High latency: {avg_lat:.2f}s", "warning")
else:
consecutive_failures += 1
failed = [ep for ep, r in api_results.items() if not r["ok"]]
notify(f"Health check failed ({consecutive_failures}/{max_failures}): {failed}", "warning")
if consecutive_failures >= max_failures:
notify("Triggering automatic rollback!", "critical")
if rollback_argocd():
notify("Rollback initiated successfully", "warning")
consecutive_failures = 0
else:
notify("Rollback failed! Manual intervention required", "critical")
time.sleep(30)
if __name__ == "__main__":
monitor_loop()
FAQ คำถามที่พบบ่อย
Q: Medusa กับ Shopify ต่างกันอย่างไร?
A: Medusa เป็น Open Source self-hosted ไม่มีค่าบริการรายเดือน ปรับแต่งได้ 100% เหมาะสำหรับธุรกิจที่ต้องการ full control ส่วน Shopify เป็น SaaS ใช้งานง่ายไม่ต้องจัดการ server แต่มีค่าบริการรายเดือนและ transaction fee ปรับแต่งได้จำกัด สำหรับ startup ที่มีทีม dev แนะนำ Medusa สำหรับธุรกิจที่ไม่มีทีม dev แนะนำ Shopify
Q: GitOps ดีกว่า CI/CD แบบปกติอย่างไร?
A: GitOps ใช้ Git เป็น single source of truth ทำให้ทุกการเปลี่ยนแปลงถูกบันทึกและ review ได้ สามารถ rollback ได้ด้วย git revert มี drift detection ที่ตรวจจับเมื่อ cluster state ไม่ตรงกับ Git ส่วน CI/CD แบบปกติอาจมี manual changes ที่ไม่ถูกบันทึก GitOps เหมาะสำหรับ Kubernetes environments
Q: Database migration กับ GitOps ทำอย่างไร?
A: ควรแยก migration ออกจาก application deployment โดยรัน migration เป็น Kubernetes Job หรือ init container ก่อนที่ application จะ start ต้อง backup database ก่อน migrate เสมอ ใช้ backward-compatible migrations ที่ทำงานได้กับทั้ง code เวอร์ชันเก่าและใหม่ ห้ามลบ column โดยตรงแต่ต้อง deprecate ก่อน
Q: ArgoCD กับ Flux CD ควรเลือกอันไหน?
A: ArgoCD มี Web UI ที่สวยและใช้งานง่าย มี application-centric approach เหมาะสำหรับทีมที่ต้องการ visibility ส่วน Flux CD เป็น lightweight มากกว่า ใช้ CRDs เป็นหลัก เหมาะสำหรับทีมที่ prefer CLI-first approach ทั้งสองตัวเป็น CNCF graduated projects สำหรับผู้เริ่มต้นแนะนำ ArgoCD เพราะ UI ช่วยให้เข้าใจง่าย
