Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Flujos de trabajo de despliegue, patrones de pipeline CI/CD, contenedorización Docker, health checks, estrategias de rollback y listas de verificación de preparación para producción de aplicaciones web.
.claude/skills/affaan-m-deployment-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 119% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 347% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 194% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 131% | 0% |
生产环境部署工作流和 CI/CD 最佳实践。
逐步替换实例——在发布过程中,新旧版本同时运行。
实例 1: v1 → v2 (首次更新)
实例 2: v1 (仍在运行 v1)
实例 3: v1 (仍在运行 v1)
实例 1: v2
实例 2: v1 → v2 (第二次更新)
实例 3: v1
实例 1: v2
实例 2: v2
实例 3: v1 → v2 (最后更新)优点: 零停机时间,渐进式发布 缺点: 两个版本同时运行——需要向后兼容的更改 适用场景: 标准部署,向后兼容的更改
运行两个相同的环境。原子化地切换流量。
Blue (v1) ← 流量
Green (v2) 空闲,运行新版本
# 验证后:
Blue (v1) 空闲(转为备用状态)
Green (v2) ← 流量优点: 即时回滚(切换回蓝色环境),切换干净利落 缺点: 部署期间需要双倍的基础设施 适用场景: 关键服务,对问题零容忍
首先将一小部分流量路由到新版本。
v1:95% 的流量
v2:5% 的流量(金丝雀)
# 如果指标表现良好:
v1:50% 的流量
v2:50% 的流量
# 最终:
v2:100% 的流量优点: 在全量发布前,通过真实流量发现问题 缺点: 需要流量分割基础设施和监控 适用场景: 高流量服务,风险性更改,功能标志
dockerfile# Stage 1: Install dependencies FROM node:22-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --production=false # Stage 2: Build FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build RUN npm prune --production # Stage 3: Production image FROM node:22-alpine AS runner WORKDIR /app RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 USER appuser COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules COPY --from=builder --chown=appuser:appgroup /app/dist ./dist COPY --from=builder --chown=appuser:appgroup /app/package.json ./ ENV NODE_ENV=production EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 CMD ["node", "dist/server.js"]
dockerfileFROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server FROM alpine:3.19 AS runner RUN apk --no-cache add ca-certificates RUN adduser -D -u 1001 appuser USER appuser COPY --from=builder /server /server EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:8080/health || exit 1 CMD ["/server"]
dockerfileFROM python:3.12-slim AS builder WORKDIR /app RUN pip install --no-cache-dir uv COPY requirements.txt . RUN uv pip install --system --no-cache -r requirements.txt FROM python:3.12-slim AS runner WORKDIR /app RUN useradd -r -u 1001 appuser USER appuser COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --from=builder /usr/local/bin /usr/local/bin COPY . . ENV PYTHONUNBUFFERED=1 EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')" || exit 1 CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]
# 良好实践
- 使用特定版本标签(node:22-alpine,而非 node:latest)
- 采用多阶段构建以最小化镜像体积
- 以非 root 用户身份运行
- 优先复制依赖文件(利用分层缓存)
- 使用 .dockerignore 排除 node_modules、.git、tests 等文件
- 添加 HEALTHCHECK 指令
- 在 docker-compose 或 k8s 中设置资源限制
# 不良实践
- 以 root 身份运行
- 使用 :latest 标签
- 在单个 COPY 层中复制整个仓库
- 在生产镜像中安装开发依赖
- 在镜像中存储密钥(应使用环境变量或密钥管理器)yamlname: CI/CD on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm test -- --coverage - uses: actions/upload-artifact@v4 if: always() with: name: coverage path: coverage/ build: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v5 with: push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max deploy: needs: build runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' environment: production steps: - name: Deploy to production run: | # Platform-specific deployment command # Railway: railway up # Vercel: vercel --prod # K8s: kubectl set image deployment/app app=ghcr.io/${{ github.repository }}:${{ github.sha }} echo "Deploying ${{ github.sha }}"
PR 已开启:
lint → typecheck → 单元测试 → 集成测试 → 预览部署
合并到 main:
lint → typecheck → 单元测试 → 集成测试 → 构建镜像 → 部署到 staging → 冒烟测试 → 部署到 productiontypescript// Simple health check app.get("/health", (req, res) => { res.status(200).json({ status: "ok" }); }); // Detailed health check (for internal monitoring) app.get("/health/detailed", async (req, res) => { const checks = { database: await checkDatabase(), redis: await checkRedis(), externalApi: await checkExternalApi(), }; const allHealthy = Object.values(checks).every(c => c.status === "ok"); res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? "ok" : "degraded", timestamp: new Date().toISOString(), version: process.env.APP_VERSION || "unknown", uptime: process.uptime(), checks, }); }); async function checkDatabase(): Promise<HealthCheck> { try { await db.query("SELECT 1"); return { status: "ok", latency_ms: 2 }; } catch (err) { return { status: "error", message: "Database unreachable" }; } }
yamllivenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 10 periodSeconds: 30 failureThreshold: 3 readinessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 2 startupProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 0 periodSeconds: 5 failureThreshold: 30 # 30 * 5s = 150s max startup time
bash# All config via environment variables — never in code DATABASE_URL=postgres://user:pass@host:5432/db REDIS_URL=redis://host:6379/0 API_KEY=${API_KEY} # injected by secrets manager LOG_LEVEL=info PORT=3000 # Environment-specific behavior NODE_ENV=production # or staging, development APP_ENV=production # explicit app environment
typescriptimport { z } from "zod"; const envSchema = z.object({ NODE_ENV: z.enum(["development", "staging", "production"]), PORT: z.coerce.number().default(3000), DATABASE_URL: z.string().url(), REDIS_URL: z.string().url(), JWT_SECRET: z.string().min(32), LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), }); // Validate at startup — fail fast if config is wrong export const env = envSchema.parse(process.env);
bash# Docker/Kubernetes: point to previous image kubectl rollout undo deployment/app # Vercel: promote previous deployment vercel rollback # Railway: redeploy previous commit railway up --commit <previous-sha> # Database: rollback migration (if reversible) npx prisma migrate resolve --rolled-back <migration-name>
在任何生产部署之前:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-07 | pass→pass | 19,174 | 30,679 | +60% | 1 | 1 | 0% | 3,370 | 6,147 | +82% | 0 | 0 | — |
case-01 | fail→pass | 12,470 | 11,727 | -6% | 1 | 1 | 0% | 2,540 | 4,916 | +94% | 0 | 0 | — |
case-02 | fail→fail | 12,872 | 10,727 | -17% | 1 | 1 | 0% | 2,729 | 5,486 | +101% | 0 | 0 | — |
case-03 | pass→pass | 6,071 | 4,573 | -25% | 1 | 1 | 0% | 1,252 | 4,216 | +237% | 0 | 0 | — |
case-04 | pass→pass | 11,357 | 11,863 | +4% | 1 | 1 | 0% | 2,416 | 5,856 | +142% | 0 | 0 | — |
case-05 | pass→pass | 20,864 | 8,465 | -59% | 1 | 1 | 0% | 1,706 | 5,051 | +196% | 0 | 0 | — |
case-06 | fail→pass | 8,729 | 6,451 | -26% | 1 | 1 | 0% | 2,042 | 4,472 | +119% | 0 | 0 | — |
case-08 | fail→pass | 4,229 | 3,074 | -27% | 1 | 1 | 0% | 844 | 3,773 | +347% | 0 | 0 | — |
case-09 | pass→pass | 3,281 | 2,409 | -27% | 1 | 1 | 0% | 626 | 3,691 | +490% | 0 | 0 | — |
case-10 | fail→pass | 6,655 | 2,441 | -63% | 1 | 1 | 0% | 1,244 | 3,663 | +194% | 0 | 0 | — |
case-11 | pass→pass | 7,833 | 15,642 | +100% | 1 | 1 | 0% | 1,311 | 4,578 | +249% | 0 | 0 | — |
case-12 | pass→pass | 11,009 | 6,301 | -43% | 1 | 1 | 0% | 1,657 | 4,278 | +158% | 0 | 0 | — |
case-13 | pass→pass | 5,583 | 3,975 | -29% | 1 | 1 | 0% | 1,027 | 3,950 | +285% | 0 | 0 | — |
case-14 | pass→pass | 11,885 | 11,261 | -5% | 1 | 1 | 0% | 2,228 | 5,382 | +142% | 0 | 0 | — |
case-15 | pass→pass | 10,522 | 9,656 | -8% | 1 | 1 | 0% | 2,027 | 4,949 | +144% | 0 | 0 | — |
case-16 | fail→pass | 8,830 | 6,471 | -27% | 1 | 1 | 0% | 1,932 | 4,463 | +131% | 0 | 0 | — |
case-17 | pass→pass | 13,938 | 4,455 | -68% | 1 | 1 | 0% | 2,594 | 4,143 | +60% | 0 | 0 | — |
case-18 | pass→pass | 16,413 | 13,640 | -17% | 1 | 1 | 0% | 2,705 | 5,537 | +105% | 0 | 0 | — |
case-19 | pass→pass | 12,065 | 10,088 | -16% | 1 | 1 | 0% | 2,748 | 5,498 | +100% | 0 | 0 | — |
case-20 | pass→pass | 8,267 | 9,287 | +12% | 1 | 1 | 0% | 1,852 | 5,306 | +187% | 0 | 0 | — |
case-21 | pass→pass | 11,360 | 14,456 | +27% | 1 | 1 | 0% | 2,397 | 6,539 | +173% | 0 | 0 | — |
case-22 | pass→pass | 10,152 | 7,106 | -30% | 1 | 1 | 0% | 2,016 | 4,576 | +127% | 0 | 0 | — |
case-23 | pass→pass | 15,093 | 11,619 | -23% | 1 | 1 | 0% | 2,572 | 5,256 | +104% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.