agentsclimarketplace

Deploy

Skill sonhaicoder/haiclaudeskill/skills/deploy

14 auto-triggering Claude Code skills that inject production engineering rules into your AI agent — trigger precision + deterministic option-spaces, distilled from a ~60K-line FastAPI/Next.js/Flutter codebase

Install
npx -y skills add sonhaicoder/haiclaudeskill --skill deploy

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Auto-trigger khi: "deploy", "push to production", "Railway", "Vercel", "Render", "Heroku", "VPS", "Hetzner", "Fly.io", "production environment", "env vars", "environment variables", ".env", "Dockerfile", "docker-compose", "CI/CD", "GitHub Actions", "port", "CORS production", "DATABASE_URL production", "build failed", "deployment failed", "502 Bad Gateway", "cold start", "health check", "rollback". Rule: KHÔNG deploy khi build fail local. KHÔNG hardcode secrets. Pre-deploy checklist mandatory.

SKILL.md

17.3 KB, as published. Nobody here has run it

Deploy Skill — Production Deployment Patterns

Triết lý: If it doesn't work locally, it won't work in production. Local build pass + env check + migration ready = minimum bar before touching production. Source: distilled từ Commerce Platform production patterns (Railway + Vercel + Neon).


1. AUTO-TRIGGER

DÙNG khi:
  ✓ User nói: deploy, release, ship, push to production, go live
  ✓ User mention platform: Railway, Vercel, Render, Heroku, Fly.io, Hetzner, VPS
  ✓ User mention artifact: Dockerfile, docker-compose.yml, .env, vercel.json
  ✓ User mention problem: "build failed", "deployment failed", "502", "cold start", "rollback"
  ✓ User mention config: DATABASE_URL, CORS production, environment variables, port conflict
  ✓ User nói: "CI/CD", "GitHub Actions", "health check", "env vars"

KHÔNG dùng khi:
  ✗ Chỉ local dev setup (dùng feature-dev skill)
  ✗ Code review (dùng code-review skill)
  ✗ Database schema design (dùng backend-fastapi skill)

2. Deploy Philosophy

Nguyên tắc cốt lõi:

LOCAL FIRST. ALWAYS.

Trước khi deploy bất cứ thứ gì:
  □ npm run build (hoặc tsc --noEmit) PASS
  □ python -m py_compile main.py PASS
  □ Alembic migration đã commit
  □ .env.example cập nhật
  □ CORS origins updated
  □ Health check endpoint hoạt động

"Works on my machine" không phải excuse. Fix local trước.

Deploy = 3 gates:

GATE 1: LOCAL BUILD PASS
  Backend: python -c "from app.api.v1 import api_router" + py_compile
  Admin:   cd web-admin && npm run build
  Store:   cd web-storefront && npm run build
  → Tất cả pass → mới được sang gate 2

GATE 2: ENV & CONFIG CHECK
  □ Tất cả env vars trong dashboard (Railway/Vercel)
  □ CORS_ORIGINS include production frontend URLs
  □ DATABASE_URL trỏ production (KHÔNG local)
  □ Secret keys là production values (không test keys)

GATE 3: MIGRATION READY
  □ alembic upgrade head chạy được
  □ Migration không phá data hiện tại
  □ Backup taken nếu migration có DROP/ALTER COLUMN

3. Environment Variables Rules

NEVER hardcode. ALWAYS via env vars.

# SAI — hardcode secret
SECRET_KEY = "my-secret-key-123"
DATABASE_URL = "postgresql://user:pass@localhost/db"
GROQ_API_KEY = "gsk_abc123"

# ĐÚNG — Pydantic Settings pattern
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    SECRET_KEY: str
    DATABASE_URL: str
    REDIS_URL: str = "redis://localhost:6379"
    GROQ_API_KEY: str = ""
    GEMINI_API_KEY: str = ""
    CORS_ORIGINS: list[str] = ["http://localhost:3001"]

    model_config = SettingsConfig(env_file=".env")

settings = Settings()

.env.example — document EVERYTHING:

# Core
SECRET_KEY=your-secret-key-here-min-32-chars
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname
DATABASE_URL_SYNC=postgresql://user:pass@host:5432/dbname

# Cache
REDIS_URL=redis://localhost:6379

# AI providers
GROQ_API_KEY=gsk_...
GEMINI_API_KEY=AIza...
DEEPSEEK_API_KEY=sk-...

# Storage
CLOUDINARY_CLOUD_NAME=your-cloud
CLOUDINARY_API_KEY=your-key
CLOUDINARY_API_SECRET=your-secret

# Payments
VNPAY_TMN_CODE=your-tmn
VNPAY_HASH_SECRET=your-secret
MOMO_PARTNER_CODE=your-code
MOMO_ACCESS_KEY=your-key
MOMO_SECRET_KEY=your-secret

# Shipping
GHN_TOKEN=your-token
GHN_SHOP_ID=your-shop-id

# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASS=your-app-password

# CORS (comma-separated)
CORS_ORIGINS=https://sonhai-admin.vercel.app,https://sonhai-store.vercel.app

Secrets rotation strategy:

Khi nào rotate secrets:
  □ Developer rời team
  □ Suspected leak (check git history: git log --all -p | grep "SECRET")
  □ Platform breach notification
  □ Quarterly rotation cho payment secrets

Cách rotate không downtime:
  1. Tạo secret mới trong platform dashboard
  2. Update code để accept BOTH old + new (grace period)
  3. Deploy
  4. Revoke old secret
  5. Remove old secret fallback code
  6. Deploy lại

4. Port Management

Commerce Platform port map (KHÔNG conflict):

Dynamic Pricing (project khác):  8000  ← KHÔNG ĐƯỢC DÙNG
Commerce Backend (FastAPI):       8001
Commerce Admin (React):           3001
Commerce Storefront (React):      3002
MONII Backend:                    8002
MONII Frontend:                   3003
Vite default (tránh dùng):        5173

Lý do quan trọng:

# Chạy local đúng port
uvicorn main:app --reload --port 8001
npm run dev -- --port 3001   # web-admin
npm run dev -- --port 3002   # web-storefront

# Kiểm tra port có đang dùng không
lsof -i :8001
lsof -i :3001

# Kill process đang giữ port
kill -9 $(lsof -t -i:8001)

Production không có port conflict — Railway/Vercel tự assign từ $PORT env var:

# Backend: dùng $PORT (Railway set tự động)
CMD = ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT}"]

5. Railway Deployment

Setup lần đầu:

1. railway.app → New Project → Deploy from GitHub
2. Connect repo → chọn branch main
3. Set Root Directory: backend
4. Add PostgreSQL plugin: + New → Database → Add PostgreSQL
5. Add Redis plugin: + New → Database → Add Redis
6. Set env vars (Variables tab):
   - DATABASE_URL → ${{Postgres.DATABASE_URL}} (internal, Railway injects)
   - DATABASE_URL_SYNC → dùng external URL cho Alembic
   - SECRET_KEY → generate: openssl rand -hex 32
   - REDIS_URL → ${{Redis.REDIS_URL}}
   - CORS_ORIGINS → https://sonhai-admin.vercel.app,https://sonhai-store.vercel.app
   - + tất cả AI keys, payment keys
7. Settings → Health Check Path: /health
8. Settings → Restart Policy: Always

Dockerfile pattern cho Railway:

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Railway injects $PORT
CMD sh -c "uvicorn main:app --host 0.0.0.0 --port ${PORT}"

DATABASE_URL — internal vs external:

# Internal (dùng trong Railway app — nhanh hơn, không tính bandwidth)
DATABASE_URL=postgresql+asyncpg://postgres:[email protected]:5432/railway

# External (dùng local để chạy Alembic migration hoặc debug)
DATABASE_URL=postgresql+asyncpg://postgres:[email protected]:PORT/railway

# asyncpg (async SQLAlchemy) vs psycopg2 (sync Alembic)
# → asyncpg: postgresql+asyncpg://...
# → psycopg2/Alembic: postgresql://... (KHÔNG có +asyncpg)
# Set DATABASE_URL_SYNC riêng cho Alembic

Connection pool settings cho Railway Postgres:

engine = create_async_engine(
    settings.DATABASE_URL,
    pool_size=5,
    max_overflow=10,
    pool_recycle=180,
    pool_pre_ping=True,
    connect_args={
        "statement_cache_size": 0,  # BẮT BUỘC cho Railway connection pooler
        "server_settings": {"application_name": "commerce-backend"},
    },
)

Rollback Railway deployment:

Railway dashboard → Deployments tab
→ Click deployment muốn rollback về
→ "Redeploy" button
→ Confirm
→ Previous deployment active trong ~30s

Auto-deploy from GitHub:

Railway tự deploy khi push to connected branch.
Để deploy manually: railway up (CLI)
Để deploy specific commit: không support — dùng git push với commit đó là HEAD

6. Vercel Deployment

Setup lần đầu:

1. vercel.com → New Project → Import Git Repository
2. Framework Preset: Vite
3. Root Directory: web-admin (hoặc web-storefront)
4. Build Command: npm run build
5. Output Directory: dist
6. Set env vars: VITE_API_URL=https://your-railway-backend.up.railway.app
7. Deploy

vercel.json — bắt buộc cho SPA + API proxy:

{
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "https://commerce-platform-production-f1e1.up.railway.app/api/:path*"
    },
    {
      "source": "/((?!api/).*)",
      "destination": "/index.html"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" }
      ]
    }
  ]
}

Deploy commands:

# Preview deploy (không affect production)
npx vercel

# Production deploy
npx vercel --prod --yes

# Assign custom alias sau khi deploy
npx vercel alias <deployment-url> sonhai-admin.vercel.app

Deployment Protection — phải disable nếu muốn public:

Vercel Dashboard → Settings → Deployment Protection
→ Vercel Authentication: Disabled
→ Password Protection: Disabled
→ Save

Env vars tại Vercel:

# Không dùng .env trong Vercel — set trong dashboard hoặc CLI
vercel env add VITE_API_URL production
vercel env add VITE_API_URL preview

# Pull về local để debug
vercel env pull .env.local

7. Database Migrations on Deploy

LUÔN LUÔN: migration TRƯỚC deploy, KHÔNG sau.

Thứ tự bắt buộc:
  1. Deploy migration (alembic upgrade head)
  2. Verify migration thành công
  3. Deploy new application code
  4. Verify health check pass

Sai:
  1. Deploy new code  ← code mới expect column chưa có → 500 errors
  2. Run migration

Tạo migration đúng cách:

# Generate migration từ model changes
alembic revision --autogenerate -m "add shipping_tracking to orders"

# Kiểm tra migration file TRƯỚC khi commit
cat alembic/versions/<generated_file>.py

# Test locally
alembic upgrade head
alembic downgrade -1  # verify rollback works
alembic upgrade head  # go back up

# Commit migration file
git add alembic/versions/<file>.py
git commit -m "migration: add shipping_tracking to orders"

2-step NOT NULL column pattern (KHÔNG phá production):

# SAIIII — phá migration nếu table có data
op.add_column('products', sa.Column('weight_kg', sa.Numeric(8,2), nullable=False))

# ĐÚNG — 2 bước
# Step 1: add nullable, với server_default
op.add_column('products', sa.Column('weight_kg', sa.Numeric(8,2), nullable=True, server_default='0'))
# Step 2 (migration riêng, sau khi deploy step 1):
op.alter_column('products', 'weight_kg', nullable=False)
op.alter_column('products', 'weight_kg', server_default=None)

Chạy migration on Railway:

# Cách 1: Railway CLI (recommended)
railway run alembic upgrade head

# Cách 2: Local với external DATABASE_URL
DATABASE_URL_SYNC=postgresql://user:pass@external-host:port/db alembic upgrade head

# Cách 3: Trong Dockerfile startup (rủi ro — nếu fail thì app không start)
CMD sh -c "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port ${PORT}"

8. CORS in Production

Whitelist explicit origins. KHÔNG dùng * cho production.

# SAI — accept mọi origin
app.add_middleware(CORSMiddleware, allow_origins=["*"])

# ĐÚNG — explicit whitelist
from app.core.config import settings

origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",")]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Env var:

# KHÔNG có trailing slash — common mistake!
CORS_ORIGINS=https://sonhai-admin.vercel.app,https://sonhai-store.vercel.app

# SAI — trailing slash gây CORS fail
CORS_ORIGINS=https://sonhai-admin.vercel.app/,https://sonhai-store.vercel.app/

Checklist CORS:

□ Admin frontend URL (web-admin Vercel deployment)
□ Storefront frontend URL (web-storefront Vercel deployment)
□ Mobile app (nếu có — thường không cần CORS cho native)
□ localhost:3001, localhost:3002 cho local dev (dev env only)
□ Staging URL nếu có (staging.sonhai-admin.vercel.app)

9. Health Checks

Implement /health endpoint — Railway dùng nó để biết app ready:

from fastapi import APIRouter
from sqlalchemy import text

router = APIRouter()

@router.get("/health")
async def health_check(db: AsyncSession = Depends(get_db)):
    """Railway health check. Must return 200 for app to receive traffic."""
    try:
        # Check DB connectivity
        await db.execute(text("SELECT 1"))

        # Check Redis connectivity (optional)
        # await redis.ping()

        return {
            "status": "healthy",
            "db": "connected",
            "timestamp": datetime.utcnow().isoformat(),
        }
    except Exception as e:
        raise HTTPException(status_code=503, detail=f"Unhealthy: {str(e)}")


@router.get("/health/detailed")
async def health_detailed():
    """Detailed metrics — không expose tới public, dùng Railway internal."""
    return {
        "version": settings.APP_VERSION,
        "db_pool_size": engine.pool.size(),
        "db_checked_out": engine.pool.checkedout(),
        ...
    }

Railway health check config:

Railway Dashboard → Service Settings → Health Check
  Path: /health
  Timeout: 30s (đủ để DB connection khởi động)
  
Railway chờ 200 response trước khi route traffic sang deployment mới.
Nếu không config → Railway deploy ngay (risky khi DB chưa ready).

10. Zero-Downtime Deploy Pattern

Railway làm gì khi deploy:

1. Build new Docker image
2. Start new container (chạy song song với old)
3. Health check new container (GET /health → expect 200)
4. Khi new container healthy → route traffic sang
5. Send SIGTERM to old container (graceful shutdown)
6. Old container có 30s để finish in-flight requests
7. Old container killed

SIGTERM handler — drain connections gracefully:

import signal
import asyncio

shutdown_event = asyncio.Event()

def handle_sigterm(signum, frame):
    """Railway sends SIGTERM before killing container."""
    shutdown_event.set()

signal.signal(signal.SIGTERM, handle_sigterm)

@app.on_event("shutdown")
async def shutdown():
    """Wait for in-flight requests before closing DB connections."""
    await asyncio.sleep(2)  # drain in-flight requests
    await engine.dispose()

Connection draining:

# uvicorn timeout-graceful-shutdown (seconds)
# Set trong Railway: uvicorn --timeout-graceful-shutdown 30
CMD sh -c "uvicorn main:app --host 0.0.0.0 --port ${PORT} --timeout-graceful-shutdown 30"

11. Rollback

Khi nào rollback ngay:

□ Error rate > 5% sau deploy (Railway logs → filter 5xx)
□ Health check failing (app restart loop)
□ Auth endpoints returning 500
□ Payment endpoints broken (rollback NGAY, không chờ)
□ DB migration gây data corruption

Rollback Railway (application):

Railway Dashboard → Deployments tab
→ Tìm deployment trước đó (màu xanh = active)
→ Click previous successful deployment
→ "Redeploy" button
→ ~30s để route traffic sang

Rollback DB migration:

# Xem current revision
alembic current

# Xem history
alembic history --verbose

# Downgrade 1 step
alembic downgrade -1

# Downgrade về specific revision
alembic downgrade abc123de

# KHÔNG thể rollback nếu migration drop column + data đã mất
# → Đó là lý do phải backup trước mọi destructive migration

Backup trước destructive migration:

# Railway PostgreSQL backup
railway run pg_dump -Fc > backup_$(date +%Y%m%d_%H%M%S).dump

# Restore nếu cần
railway run pg_restore -d $DATABASE_URL backup_file.dump

12. Post-Deploy Verification

Smoke test ngay sau deploy — không chờ user report:

□ Open production URL → page load không trắng
□ Login với test account → success
□ List products → data hiện
□ Create test product → tạo được, hiện trong list
□ Create test order → tạo được
□ Health check URL trả 200: curl https://backend-url/health
□ Check Railway logs: không có ERROR 500
□ Check Vercel function logs: không có exception

Monitor sau deploy:

# Watch Railway logs real-time
railway logs --follow

# Filter chỉ errors
railway logs | grep -E "ERROR|Exception|Traceback"

# Check 5xx rate (Railway Metrics tab → HTTP → Status Codes)
# Nếu 5xx > 1% → investigate ngay

Alert nếu 5xx spike:

# Middleware log 5xx với đủ context để debug
@app.middleware("http")
async def log_5xx(request: Request, call_next):
    response = await call_next(request)
    if response.status_code >= 500:
        logger.error(
            f"5xx: {request.method} {request.url.path} → {response.status_code}",
            extra={"shop_id": request.state.shop_id if hasattr(request.state, "shop_id") else None}
        )
    return response

References

  • references/railway-vercel.md — Railway + Vercel setup chi tiết
  • references/pre-deploy-checklist.md — Checklist đầy đủ trước mỗi deploy
  • Commerce Platform CLAUDE.md → PRODUCTION DEPLOYMENT section
  • Railway docs: docs.railway.app
  • Vercel docs: vercel.com/docs

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.