上线部署与运维保障
SaaS 产品从开发到上线有一道"生死线"——生产环境的稳定性。功能做完只是 50%,剩下 50% 是安全加固、性能优化、监控告警、灰度发布。本章是 SaaS 上线前的终极 Checklist。
1. 生产环境 Checklist
1.1 安全清单
| 检查项 | 状态 | 说明 |
|---|
| HTTPS 强制 | ☐ | 所有流量走 HTTPS |
| 环境变量分离 | ☐ | 生产密钥不在代码仓库 |
| CSRF 保护 | ☐ | Server Actions 自动保护 |
| CSP 头 | ☐ | 防 XSS 注入 |
| Rate Limiting | ☐ | API + 登录限流 |
| SQL 注入防护 | ☐ | Drizzle ORM 参数化查询 |
| Cookie 安全 | ☐ | HttpOnly + Secure + SameSite |
| 敏感日志脱敏 | ☐ | 不打印密码、Token |
1.2 性能清单
| 检查项 | 状态 | 目标 |
|---|
| Lighthouse Performance | ☐ | ≥ 90 |
| LCP | ☐ | < 2.5s |
| FID/INP | ☐ | < 200ms |
| CLS | ☐ | < 0.1 |
| Bundle Size | ☐ | 首页 JS < 100KB |
| 图片优化 | ☐ | next/image + WebP |
| 字体优化 | ☐ | next/font 预加载 |
1.3 基础设施清单
| 检查项 | 状态 | 说明 |
|---|
| 数据库备份 | ☐ | 自动每日备份 |
| 数据库连接池 | ☐ | Serverless 用连接池 |
| CDN 配置 | ☐ | 静态资源走 CDN |
| DNS 配置 | ☐ | 域名 + SSL 证书 |
| 错误监控 | ☐ | Sentry 集成 |
| 日志收集 | ☐ | 结构化日志 |
| Stripe Webhook | ☐ | 生产环境 Webhook URL |
1.4 业务流程清单
| 检查项 | 状态 | 说明 |
|---|
| 注册流程 | ☐ | 邮箱验证 + 欢迎邮件 |
| 登录流程 | ☐ | OAuth + 密码 + MFA |
| 支付流程 | ☐ | Stripe 测试卡验证通过 |
| Webhook | ☐ | Stripe Webhook 生产 URL 配置 |
| 邮件发送 | ☐ | 生产域名 SPF/DKIM 配置 |
| 错误页面 | ☐ | 自定义 404/500 页面 |
| 法律页面 | ☐ | Terms of Service + Privacy Policy |
| SEO | ☐ | sitemap.xml + robots.txt + OG 标签 |
1.5 Next.js 安全头配置
// next.config.ts
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
]
const nextConfig = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }]
},
}
1.6 CSP(Content Security Policy)配置
// middleware.ts
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' blob: data: https://*.r2.cloudflarestorage.com`,
`font-src 'self'`,
`connect-src 'self' https://*.posthog.com https://*.sentry.io https://*.stripe.com`,
`frame-src https://js.stripe.com`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join('; ')
const headers = new Headers(request.headers)
headers.set('x-nonce', nonce)
const response = NextResponse.next({ request: { headers } })
response.headers.set('Content-Security-Policy', csp)
return response
}
1.7 环境变量管理
// lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
// 数据库
DATABASE_URL: z.string().url(),
// 认证
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
// Stripe
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith('pk_'),
// 邮件
RESEND_API_KEY: z.string().startsWith('re_'),
// 存储
S3_ENDPOINT: z.string().url(),
S3_ACCESS_KEY: z.string(),
S3_SECRET_KEY: z.string(),
S3_BUCKET: z.string(),
// 监控
SENTRY_DSN: z.string().url().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
// Admin
SUPER_ADMIN_EMAILS: z.string(),
})
// 启动时验证——缺少环境变量立即报错
export const env = envSchema.parse(process.env)
2. 灰度发布
2.1 发布策略
| 策略 | 说明 | 风险 |
|---|
| 全量发布 | 一次性全部更新 | 高 |
| 灰度发布 | 按比例/条件逐步放量 | 低 |
| 蓝绿部署 | 两套环境切换 | 低(成本高) |
| 金丝雀发布 | 先给少量用户 | 最低 |
2.2 Vercel 灰度方案
// middleware.ts
export function middleware(request: NextRequest) {
// 基于 Cookie 的灰度分流
const bucket = request.cookies.get('ab-bucket')?.value
if (!bucket) {
// 首次访问,随机分配
const newBucket = Math.random() < 0.1 ? 'canary' : 'stable'
const response = NextResponse.next()
response.cookies.set('ab-bucket', newBucket, { maxAge: 86400 * 30 })
if (newBucket === 'canary') {
// 重写到金丝雀部署
return NextResponse.rewrite(new URL(request.url, process.env.CANARY_URL))
}
return response
}
if (bucket === 'canary') {
return NextResponse.rewrite(new URL(request.url, process.env.CANARY_URL))
}
return NextResponse.next()
}
2.3 Feature Flag 灰度
// 基于 PostHog Feature Flag(见第 69 章)
const showNewDashboard = await isFeatureEnabled('new-dashboard', tenantId)
if (showNewDashboard) {
return <NewDashboard />
}
return <OldDashboard />
3. 监控告警
3.1 监控层级
| 层级 | 工具 | 指标 |
|---|
| 应用错误 | Sentry | 错误率、未捕获异常 |
| 性能 | Vercel Analytics | Web Vitals、路由耗时 |
| 业务 | PostHog | 注册转化、付费转化 |
| 基础设施 | Vercel / Provider | CPU、内存、数据库连接数 |
| 可用性 | BetterUptime | 站点可达性 |
3.2 Sentry 生产配置
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // 生产 10% 采样
profilesSampleRate: 0.1,
beforeSend(event) {
// 过滤已知的非关键错误
if (event.exception?.values?.[0]?.type === 'AbortError') return null
return event
},
})
3.3 告警规则
| 指标 | 阈值 | 通知方式 |
|---|
| 错误率 > 1% | 5 分钟窗口 | Slack + 邮件 |
| P95 延迟 > 3s | 10 分钟窗口 | Slack |
| 支付失败 | 任何一次 | 邮件(立即) |
| 站点不可达 | 连续 2 次检查 | 电话 + Slack |
| 数据库连接数 > 80% | 实时 | Slack |
3.4 健康检查端点
// app/api/health/route.ts
export async function GET() {
try {
await db.execute(sql`SELECT 1`)
return Response.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.NEXT_PUBLIC_APP_VERSION || 'unknown',
})
} catch (error) {
return Response.json({ status: 'unhealthy', error: String(error) }, { status: 503 })
}
}
3.5 结构化日志
// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
interface LogContext {
userId?: string
tenantId?: string
requestId?: string
[key: string]: any
}
function log(level: LogLevel, message: string, context?: LogContext) {
const entry = {
level,
message,
timestamp: new Date().toISOString(),
...context,
}
// 生产环境输出 JSON(便于日志聚合工具解析)
if (process.env.NODE_ENV === 'production') {
console[level === 'error' ? 'error' : 'log'](JSON.stringify(entry))
} else {
console[level === 'error' ? 'error' : 'log'](`[${level.toUpperCase()}] ${message}`, context)
}
}
export const logger = {
debug: (msg: string, ctx?: LogContext) => log('debug', msg, ctx),
info: (msg: string, ctx?: LogContext) => log('info', msg, ctx),
warn: (msg: string, ctx?: LogContext) => log('warn', msg, ctx),
error: (msg: string, ctx?: LogContext) => log('error', msg, ctx),
}
3.6 Sentry 错误边界
// app/global-error.tsx
'use client'
import * as Sentry from '@sentry/nextjs'
import { useEffect } from 'react'
export default function GlobalError({
error, reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
Sentry.captureException(error)
}, [error])
return (
<html>
<body>
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold">Something went wrong</h2>
<p className="mt-2 text-muted-foreground">
We've been notified and are working on a fix.
</p>
<button
onClick={reset}
className="mt-4 rounded bg-primary px-4 py-2 text-primary-foreground"
>
Try Again
</button>
</div>
</div>
</body>
</html>
)
}
3.7 自定义 404 页面
// app/not-found.tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-bold">404</h1>
<p className="mt-4 text-lg text-muted-foreground">Page not found</p>
<Link href="/" className="mt-6 inline-block rounded bg-primary px-6 py-2 text-primary-foreground">
Go Home
</Link>
</div>
</div>
)
}
4. 数据库生产配置
4.1 连接池
// lib/db/index.ts
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
// Serverless 环境用 HTTP 连接(无连接池开销)
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql)
4.2 迁移策略
| 阶段 | 操作 |
|---|
| 开发 | drizzle-kit push 快速同步 |
| 预发布 | drizzle-kit generate 生成迁移文件 |
| 生产 | drizzle-kit migrate 执行迁移(CI/CD 中) |
4.3 备份
| 策略 | 频率 | 保留期 |
|---|
| 自动快照 | 每日 | 7 天 |
| WAL 归档 | 持续 | 30 天 |
| 手动快照 | 大变更前 | 永久 |
5. 上线流程
5.1 标准流程
1. 功能冻结(Code Freeze)
2. 跑完整测试套件
3. 生产环境迁移(数据库)
4. 灰度发布(10% 流量)
5. 监控 30 分钟
6. 全量发布
7. 验证核心流程(注册/登录/支付)
8. 更新 Status Page
5.2 CI/CD Pipeline
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm run test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Database Migrations
run: npx drizzle-kit migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
5.3 数据库迁移安全
// 生产迁移原则
// 1. 只做加法(加列/加表),不做减法
// 2. 新列先 nullable,后续再改为 NOT NULL
// 3. 列重命名分两步:加新列 → 迁移数据 → 删旧列
// 4. 大表加索引用 CONCURRENTLY
// drizzle.config.ts
export default {
schema: './lib/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
}
5.4 回滚策略
| 情况 | 操作 | 时效 |
|---|
| UI 问题 | Vercel 即时回滚到上一个部署 | < 1 分钟 |
| 数据库迁移问题 | 执行回滚迁移脚本 | 5-30 分钟 |
| 第三方服务故障 | Feature Flag 关闭相关功能 | < 1 分钟 |
| 严重安全漏洞 | 立即回滚 + 通知用户 | < 5 分钟 |
| 数据损坏 | 从备份恢复 | 30-60 分钟 |
5.5 上线验证脚本
// scripts/verify-production.ts
async function verifyProduction() {
const baseUrl = process.env.PRODUCTION_URL!
const checks = [
{ name: 'Health Check', url: `${baseUrl}/api/health` },
{ name: 'Homepage', url: baseUrl },
{ name: 'Login Page', url: `${baseUrl}/login` },
{ name: 'Stripe Webhook', url: `${baseUrl}/api/webhooks/stripe` },
]
for (const check of checks) {
try {
const res = await fetch(check.url, { method: check.name.includes('Webhook') ? 'POST' : 'GET' })
const status = res.status
const ok = check.name.includes('Webhook') ? status === 400 : status === 200
console.log(`${ok ? '✅' : '❌'} ${check.name}: ${status}`)
} catch (err) {
console.log(`❌ ${check.name}: ${err}`)
}
}
}
verifyProduction()
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
default: 'YourSaaS - Project Management for Teams',
template: '%s | YourSaaS',
},
description: 'The modern project management platform for growing teams.',
metadataBase: new URL('https://yoursaas.com'),
openGraph: {
type: 'website',
locale: 'en_US',
siteName: 'YourSaaS',
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
creator: '@yoursaas',
},
robots: {
index: true,
follow: true,
},
}
6.2 Sitemap 生成
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
const staticPages = [
'', '/pricing', '/features', '/about', '/blog',
'/terms', '/privacy',
].map((path) => ({
url: `https://yoursaas.com${path}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: path === '' ? 1 : 0.8,
}))
return staticPages
}
6.3 Robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin', '/api/', '/dashboard'],
},
],
sitemap: 'https://yoursaas.com/sitemap.xml',
}
}
7. 性能优化
7.1 Bundle 分析
// package.json
{
"scripts": {
"analyze": "ANALYZE=true next build"
}
}
// next.config.ts
import withBundleAnalyzer from '@next/bundle-analyzer'
const nextConfig = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
})({
// ...其他配置
})
7.2 关键性能优化
| 优化项 | 实现方式 | 效果 |
|---|
| 动态导入 | next/dynamic 懒加载重组件 | 首屏 JS 减少 |
| 字体优化 | next/font 预加载 | 消除 FOIT |
| 图片优化 | next/image 自动 WebP + 懒加载 | LCP 改善 |
| Server Components | 默认 RSC,减少客户端 JS | Bundle 减小 |
| 路由预取 | <Link> 自动预取 | 导航秒开 |
| 数据库查询 | Promise.all 并行查询 | TTFB 减少 |
7.3 缓存策略
// 静态页面缓存(ISR)
export const revalidate = 3600 // 1 小时
// 动态数据缓存
import { unstable_cache } from 'next/cache'
export const getCachedPricing = unstable_cache(
async () => {
return db.select().from(plans).where(eq(plans.active, true))
},
['pricing-plans'],
{ revalidate: 3600, tags: ['pricing'] }
)
// 手动失效
import { revalidateTag } from 'next/cache'
revalidateTag('pricing')
本章小结
- 安全清单:HTTPS、CSP(nonce)、Rate Limiting、Cookie 安全、日志脱敏
- 环境变量:Zod schema 启动时验证,缺少变量立即报错
- 性能清单:Lighthouse ≥ 90、LCP < 2.5s、首页 JS < 100KB
- 灰度发布:Middleware Cookie 分流 / Feature Flag 按租户发布
- 监控告警:Sentry 错误 + Vercel 性能 + BetterUptime 可用性 + 结构化日志
- 错误处理:全局 error.tsx 自动上报 Sentry,自定义 404 页面
- CI/CD:GitHub Actions → lint + type-check + test → 迁移 → Vercel 部署
- 迁移安全:只做加法、先 nullable 再 NOT NULL、大表 CONCURRENTLY
- 回滚:Vercel 秒级回滚 + 数据库回滚脚本 + Feature Flag Kill Switch
- SEO:Metadata API + sitemap.ts + robots.ts + Open Graph
- 性能优化:Bundle Analyzer + 动态导入 + ISR 缓存 + unstable_cache