本章目标:掌握 Next.js 生产部署的完整技术栈——理解 Standalone Output 模式、构建优化后的 Docker 镜像、配置 GitHub Actions 自动化流水线,并了解 Vercel / Docker / Kubernetes 多种部署方案的取舍。
21.1 部署方案对比
主流部署平台
| 平台 | 类型 | 适用场景 | 复杂度 | 成本 |
|---|---|---|---|---|
| Vercel | Serverless | 快速上线、Next.js 官方推荐 | 低 | 按用量付费 |
| Netlify | Serverless | 静态站点为主 | 低 | 免费层 + 付费 |
| Docker 自托管 | 容器 | 私有部署、合规要求 | 中 | 服务器成本 |
| Kubernetes | 容器编排 | 大规模、高可用 | 高 | 集群成本 |
| Cloudflare Pages | Edge | 全球加速、低延迟 | 低 | 免费层优厚 |
| Render | PaaS | 全栈应用 | 低 | 月付固定价 |
| Railway | PaaS | 快速部署 + 数据库 | 低 | 按用量 |
| AWS ECS | 容器服务 | AWS 生态 | 中 | 按用量 |
选型决策树
你的需求...
├── 快速上线 + Next.js 最佳体验 → ✅ Vercel
├── 数据合规 / 私有部署 → ✅ Docker 自托管
├── 全球边缘加速 → ✅ Cloudflare Pages / Vercel Edge
├── 已有 K8s 集群 → ✅ Kubernetes + Helm
├── 预算有限 + 全栈应用 → ✅ Render / Railway
└── 已有 AWS / GCP / Azure → 对应云服务的容器方案
21.2 Next.js Standalone Output
启用 Standalone 模式
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone', // 启用独立部署包
// 可选:自定义 server 配置
// serverExternalPackages: ['sharp', 'bcrypt'],
};
module.exports = nextConfig;
Standalone 输出结构
npm run build
.next/
├── standalone/ # 独立部署包(仅包含必要依赖)
│ ├── node_modules/ # 精简的 node_modules
│ ├── public/ # 静态资源
│ ├── .next/ # 构建产物
│ └── server.js # 启动脚本
├── static/ # 静态文件(需要复制到 standalone)
└── cache/ # 构建缓存
为什么使用 Standalone?
传统部署:
- 需要完整 node_modules(通常 200-500MB)
- 启动慢、镜像大
- 依赖 npm install
Standalone 部署:
- 仅包含运行必需的依赖(通常 30-80MB)
- 启动快、镜像小
- 不需要 npm install
- 适合 Docker、Serverless
21.3 Docker 多阶段构建
完整 Dockerfile
# Dockerfile
# ============ 阶段 1:依赖安装 ============
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app
# ============ 阶段 2:安装依赖 ============
FROM base AS deps
COPY package.json package-lock.json* ./
COPY prisma ./prisma/
# 安装生产依赖(用于生成 Prisma Client)
RUN npm ci --omit=dev
# 生成 Prisma Client
RUN npx prisma generate
# 重新安装所有依赖(包括 dev,用于构建)
RUN npm ci
# ============ 阶段 3:构建 ============
FROM base AS builder
# 复制依赖和 Prisma Client
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/.prisma ./.prisma
COPY . .
# 设置构建时环境变量
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# 构建 Next.js 应用
RUN npm run build
# ============ 阶段 4:生产运行 ============
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制静态资源
COPY --from=builder /app/public ./public
# 复制 standalone 输出
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# 复制 Prisma 相关文件(用于运行时迁移)
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma
# 切换到非 root 用户
USER nextjs
# 暴露端口
EXPOSE 3000
# 配置环境变量
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# 启动命令(包含数据库迁移)
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]
.dockerignore
# .dockerignore
Dockerfile
.dockerignore
node_modules
npm-debug.log
.next
.git
.gitignore
README.md
.env
.env.local
.env.*.local
.vscode
.idea
coverage
playwright-report
test-results
*.md
!README.md
构建与运行
# 构建镜像
docker build -t my-blog:latest .
# 查看镜像大小
docker images my-blog
# REPOSITORY TAG IMAGE ID CREATED SIZE
# my-blog latest abc123 2 minutes ago 180MB
# 运行容器
docker run -d \
-p 3000:3000 \
--env-file .env.production \
--name my-blog \
my-blog:latest
# 查看日志
docker logs -f my-blog
# 停止容器
docker stop my-blog
docker rm my-blog
多阶段构建优化
优化技巧:
1. 使用 Alpine 基础镜像(5MB vs 900MB)
2. 多阶段构建(构建阶段 + 运行阶段分离)
3. 复制 standalone 而非完整 node_modules
4. 使用 npm ci 而非 npm install
5. 缓存 Prisma Client 生成
6. 非 root 用户运行(安全)
7. 健康检查端点(监控)
镜像大小对比:
- 传统方式:~800MB
- Standalone + Alpine:~150-200MB
- 进一步压缩(distroless):~120MB
21.4 Docker Compose 本地开发
完整开发环境
# docker-compose.yml
version: '3.9'
services:
# Next.js 应用
app:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myblog
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
networks:
- app-network
# PostgreSQL 数据库
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myblog
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# Redis 缓存
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
- redis_data:/data
networks:
- app-network
# Nginx 反向代理(可选)
nginx:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- app-network
volumes:
postgres_data:
redis_data:
networks:
app-network:
driver: bridge
开发环境 Docker Compose
# docker-compose.dev.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- '3000:3000'
environment:
- NODE_ENV=development
volumes:
- .:/app
- /app/node_modules
- /app/.next
command: npm run dev
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myblog_dev
ports:
- '5432:5432'
volumes:
- postgres_dev_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
postgres_dev_data:
使用方式
# 生产环境
docker-compose up -d
docker-compose down
docker-compose logs -f app
# 开发环境
docker-compose -f docker-compose.dev.yml up
docker-compose -f docker-compose.dev.yml down
21.5 GitHub Actions CI/CD
完整 CI/CD 流水线
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# ============ 代码质量检查 ============
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: ESLint
run: npm run lint
- name: TypeScript
run: npm run type-check
- name: Prettier
run: npm run format:check
# ============ 单元测试 ============
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:run
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
# ============ E2E 测试 ============
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Start PostgreSQL
run: |
docker run -d \
--name postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=test_db \
-p 5432:5432 \
postgres:16-alpine
- name: Wait for PostgreSQL
run: |
until pg_isready -h localhost -p 5432 -U postgres; do
sleep 2
done
- name: Run migrations
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: npx prisma migrate deploy
- name: Seed test data
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
run: npx prisma db seed
- name: Run E2E tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
NEXTAUTH_SECRET: test-secret
NEXTAUTH_URL: http://localhost:3000
run: npm run test:e2e
- name: Upload test artifacts
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
# ============ 构建检查 ============
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Generate Prisma Client
run: npx prisma generate
- name: Build Next.js
env:
NEXT_TELEMETRY_DISABLED: 1
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: next-build
path: |
.next/standalone
.next/static
public
retention-days: 1
CD 流水线(部署到 Docker Registry)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
tags: ['v*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ============ 构建并推送 Docker 镜像 ============
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# ============ 部署到生产环境 ============
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build-and-push
if: startsWith(github.ref, 'refs/tags/v')
environment:
name: production
url: https://example.com
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /opt/my-blog
docker-compose pull
docker-compose up -d --remove-orphans
docker image prune -f
- name: Verify deployment
run: |
sleep 10
curl -f https://example.com/api/health || exit 1
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Production deployment ${{ job.status }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
部署到 Vercel
# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Vercel CLI
run: npm install -g vercel
- name: Pull Vercel Environment
run: vercel pull --yes --environment=${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build ${{ github.ref == 'refs/heads/main' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy
id: deploy
run: |
url=$(vercel deploy --prebuilt ${{ github.ref == 'refs/heads/main' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }})
echo "url=$url" >> $GITHUB_OUTPUT
- name: Comment PR
if: github.event_name == 'pull_request'
uses: thollander/actions-comment-pull-request@v2
with:
message: |
🚀 Preview deployed to ${{ steps.deploy.outputs.url }}
21.6 Nginx 反向代理配置
基础配置
# nginx.conf
upstream nextjs_app {
server app:3000;
keepalive 64;
}
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# HTTPS 主配置
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL 证书
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# SSL 会话缓存
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/rss+xml
image/svg+xml;
# 静态资源(Next.js 构建产物,永久缓存)
location /_next/static/ {
proxy_pass http://nextjs_app;
proxy_cache_valid 200 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 公共资源(手动管理的静态文件)
location /public/ {
proxy_pass http://nextjs_app;
proxy_cache_valid 200 1d;
add_header Cache-Control "public, max-age=86400";
}
# API 路由(不缓存)
location /api/ {
proxy_pass http://nextjs_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
add_header Cache-Control "no-store";
}
# 默认路由(Next.js 处理)
location / {
proxy_pass http://nextjs_app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
}
# 健康检查
location /api/health {
proxy_pass http://nextjs_app;
access_log off;
}
# 阻止访问敏感文件
location ~ /\. {
deny all;
}
location ~* \.(env|git|gitignore|dockerignore)$ {
deny all;
}
}
Let’s Encrypt SSL
# 安装 Certbot
apt-get install certbot python3-certbot-nginx
# 申请证书
certbot --nginx -d example.com -d www.example.com
# 自动续期
certbot renew --dry-run
21.7 Kubernetes 部署
Deployment 配置
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-blog
labels:
app: my-blog
spec:
replicas: 3
selector:
matchLabels:
app: my-blog
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: my-blog
spec:
containers:
- name: my-blog
image: ghcr.io/username/my-blog:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: my-blog-secrets
key: database-url
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: my-blog-secrets
key: nextauth-secret
resources:
requests:
memory: '256Mi'
cpu: '250m'
limits:
memory: '512Mi'
cpu: '500m'
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
imagePullSecrets:
- name: ghcr-secret
Service 配置
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-blog
labels:
app: my-blog
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 3000
protocol: TCP
name: http
selector:
app: my-blog
Ingress 配置
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-blog
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: '50m'
nginx.ingress.kubernetes.io/ssl-redirect: 'true'
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
- www.example.com
secretName: my-blog-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-blog
port:
number: 80
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-blog
port:
number: 80
Helm Chart(可选)
# helm/my-blog/values.yaml
replicaCount: 3
image:
repository: ghcr.io/username/my-blog
tag: latest
pullPolicy: Always
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
hosts:
- host: example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: my-blog-tls
hosts:
- example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
env:
NODE_ENV: production
secrets:
database-url: ""
nextauth-secret: ""
部署命令
# 创建命名空间
kubectl create namespace my-blog
# 创建 Secret
kubectl create secret generic my-blog-secrets \
--from-literal=database-url="postgresql://..." \
--from-literal=nextauth-secret="your-secret" \
-n my-blog
# 应用配置
kubectl apply -f k8s/ -n my-blog
# 或使用 Helm
helm install my-blog ./helm/my-blog -n my-blog
# 查看状态
kubectl get pods -n my-blog
kubectl get svc -n my-blog
kubectl get ingress -n my-blog
# 查看日志
kubectl logs -f deployment/my-blog -n my-blog
# 滚动更新
kubectl set image deployment/my-blog my-blog=ghcr.io/username/my-blog:v2 -n my-blog
21.8 蓝绿部署与金丝雀发布
蓝绿部署脚本
#!/bin/bash
# scripts/blue-green-deploy.sh
set -e
BLUE_DEPLOYMENT="my-blog-blue"
GREEN_DEPLOYMENT="my-blog-green"
SERVICE="my-blog"
NEW_IMAGE=$1
# 检查当前活跃的颜色
ACTIVE_COLOR=$(kubectl get svc $SERVICE -o jsonpath='{.spec.selector.color}')
if [ "$ACTIVE_COLOR" == "blue" ]; then
NEW_COLOR="green"
NEW_DEPLOYMENT=$GREEN_DEPLOYMENT
else
NEW_COLOR="blue"
NEW_DEPLOYMENT=$BLUE_DEPLOYMENT
fi
echo "Active color: $ACTIVE_COLOR"
echo "Deploying to: $NEW_COLOR"
# 更新非活跃 deployment 的镜像
kubectl set image deployment/$NEW_DEPLOYMENT my-blog=$NEW_IMAGE
# 等待新 deployment 就绪
kubectl rollout status deployment/$NEW_DEPLOYMENT
# 健康检查
echo "Running health check on new deployment..."
kubectl exec deployment/$NEW_DEPLOYMENT -- curl -f http://localhost:3000/api/health
# 切换流量
echo "Switching traffic to $NEW_COLOR..."
kubectl patch svc $SERVICE -p "{\"spec\":{\"selector\":{\"color\":\"$NEW_COLOR\"}}}"
echo "Deployment complete! $NEW_COLOR is now active."
echo "To rollback, run: kubectl patch svc $SERVICE -p '{\"spec\":{\"selector\":{\"color\":\"$ACTIVE_COLOR\"}}}'"
金丝雀发布(Argo Rollouts)
# k8s/rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-blog
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 10 # 10% 流量到新镜像
- pause: { duration: 5m } # 观察 5 分钟
- setWeight: 30 # 30% 流量
- pause: { duration: 5m }
- setWeight: 60 # 60% 流量
- pause: { duration: 5m }
- setWeight: 100 # 100% 流量
analysis:
templates:
- templateName: success-rate
startingStep: 1
selector:
matchLabels:
app: my-blog
template:
metadata:
labels:
app: my-blog
spec:
containers:
- name: my-blog
image: ghcr.io/username/my-blog:latest
ports:
- containerPort: 3000
21.9 环境变量管理
环境分类
# .env.example(示例,提交到 git)
DATABASE_URL="postgresql://user:pass@localhost:5432/myblog"
NEXTAUTH_SECRET="your-secret-here"
NEXTAUTH_URL="http://localhost:3000"
# .env.local(本地开发,不提交)
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/myblog_dev"
NEXTAUTH_SECRET="dev-secret"
# .env.production(生产环境,不提交)
DATABASE_URL="postgresql://prod_user:prod_pass@db.example.com:5432/myblog"
NEXTAUTH_SECRET="prod-secret-very-long-and-random"
NEXTAUTH_URL="https://example.com"
使用 GitHub Secrets
# .github/workflows/deploy.yml
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
使用 Vault(生产环境)
# 安装 Vault CLI
brew install vault
# 登录
vault login
# 读取 secrets
export DATABASE_URL=$(vault kv get -field=url secret/my-blog/database)
export NEXTAUTH_SECRET=$(vault kv get -field=secret secret/my-blog/auth)
# Kubernetes 集成
kubectl create secret generic my-blog-secrets \
--from-literal=database-url="$DATABASE_URL" \
--from-literal=nextauth-secret="$NEXTAUTH_SECRET"
21.10 部署检查清单
## 生产部署检查清单
### Docker 镜像
- [ ] 使用 multi-stage build 优化镜像大小
- [ ] 镜像大小 < 300MB
- [ ] 使用 Alpine 或 Distroless 基础镜像
- [ ] 非 root 用户运行
- [ ] 配置了 HEALTHCHECK
### 安全
- [ ] 敏感信息使用 Secrets(不写入代码)
- [ ] 环境变量未泄露到 git
- [ ] HTTPS 已启用
- [ ] Nginx 隐藏了 server 头
- [ ] 依赖已扫描(npm audit、Trivy)
### 性能
- [ ] Standalone output 已启用
- [ ] Gzip / Brotli 压缩已开启
- [ ] 静态资源配置了长期缓存
- [ ] 数据库连接池已配置
- [ ] CDN 已配置(Cloudflare / CloudFront)
### 监控
- [ ] /api/health 端点可访问
- [ ] 日志已收集(Loki / CloudWatch)
- [ ] 错误监控已集成(Sentry)
- [ ] Uptime 监控已配置
- [ ] 告警已设置
### 备份
- [ ] 数据库自动备份(每日)
- [ ] 备份已测试恢复
- [ ] 上传文件有备份(S3 / R2)
### 扩展性
- [ ] 支持水平扩展(无状态)
- [ ] Session 存储在 Redis(而非内存)
- [ ] 文件上传使用外部存储
- [ ] 配置了 auto-scaling(如适用)
### 文档
- [ ] README 包含部署说明
- [ ] 环境变量已文档化
- [ ] Runbook(故障处理手册)已准备
- [ ] 团队成员知晓部署流程
本章小结
Key Takeaways
- Standalone Output 是 Docker 部署的前提:镜像更小、启动更快
- 多阶段构建是 Docker 优化的核心:构建阶段 + 运行阶段分离
- GitHub Actions 是 CI/CD 的标准方案:自动化测试、构建、部署
- 部署方案选择取决于业务需求:Vercel 适合快速、Docker 适合合规、K8s 适合大规模
- 蓝绿部署和金丝雀发布降低发布风险:渐进式发布、快速回滚
- 环境变量管理是安全的底线:使用 Secrets、Vault,永不写入代码
下一步
下一章(也是本系列最后一章)我们将深入 多租户 SaaS 架构实战——构建一个完整的多租户博客系统,涵盖租户隔离、计费、权限管理与自定义域名。
参考资料
- Next.js Docker 部署官方文档
- Docker 多阶段构建
- GitHub Actions 文档
- Kubernetes 官方文档
- Helm 包管理器
- Argo Rollouts 金丝雀发布
- Vercel 部署指南
- Nginx 配置最佳实践
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。