Chapter 10

部署:Fly.io / Railway / Cloudflare Workers

Dockerfile 最佳实践、环境变量管理、边缘部署、健康检查与零停机滚动更新

部署方案选择

平台适合场景定价特点
Fly.io全功能后端服务按用量计费,有免费层全球边缘 VM、原生持久化、内置 PostgreSQL
Railway快速原型/全栈$5/月起一键部署、内置数据库、超简单
Render传统后端服务有免费层(会休眠)类似 Heroku,操作简单
Cloudflare Workers轻量 API/边缘逻辑免费 10万请求/天全球 300+ 节点,冷启动 <5ms
自托管 VPS成本敏感/特殊需求最低完全控制,运维复杂度高

Dockerfile 最佳实践

多阶段构建(Multi-stage Build)

多阶段构建将"构建环境"和"运行环境"分离,最终镜像不包含 node_modules 的开发依赖、源代码等,体积大幅减小:

# Dockerfile — Bun 项目(多阶段构建)

# ── 阶段1:安装依赖 ──
FROM oven/bun:1 AS installer
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

# ── 阶段2:构建 ──
FROM oven/bun:1 AS builder
WORKDIR /app
COPY --from=installer /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN bunx prisma generate
# 可选:编译 TypeScript
RUN bun build ./src/index.ts --outdir ./dist --target bun

# ── 阶段3:运行(最小镜像)──
FROM oven/bun:1-slim AS runner
WORKDIR /app

# 非 root 用户运行(安全最佳实践)
USER bun

# 只复制运行时需要的文件
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/prisma ./prisma
COPY package.json ./

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1

# 启动命令
CMD ["bun", "run", "./dist/index.js"]
# Dockerfile — Node.js 22 项目
FROM node:22-alpine AS installer
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build        # tsc 编译
RUN npx prisma generate

FROM node:22-alpine AS runner
WORKDIR /app
USER node
COPY --from=installer /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
COPY package.json ./

EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]

.dockerignore

# .dockerignore — 排除不需要的文件
node_modules
dist
.env
.env.local
.git
*.test.ts
*.spec.ts
README.md
.DS_Store

健康检查端点

// src/routes/health.ts
import { Hono } from 'hono';
import { prisma } from '../lib/prisma';

const health = new Hono();

// 简单健康检查(负载均衡器用)
health.get('/health', (c) => c.json({ status: 'ok' }));

// 深度健康检查(包含依赖项状态)
health.get('/health/ready', async (c) => {
  const checks = {
    database: false,
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    timestamp: new Date().toISOString(),
  };

  try {
    await prisma.$queryRaw`SELECT 1`;
    checks.database = true;
  } catch {}

  const allHealthy = checks.database;
  return c.json({ status: allHealthy ? 'ok' : 'degraded', ...checks },
    allHealthy ? 200 : 503);
});

export { health as healthRouter };

Fly.io 部署

# 安装 flyctl
curl -L https://fly.io/install.sh | sh

# 登录
fly auth login

# 初始化(在项目目录中)
fly launch
# 会自动检测到 Dockerfile,生成 fly.toml 配置文件

# 设置环境变量(Secrets)
fly secrets set DATABASE_URL="postgresql://..."
fly secrets set JWT_ACCESS_SECRET="your-super-secret-key"
fly secrets set JWT_REFRESH_SECRET="another-secret-key"

# 部署
fly deploy

# 查看日志
fly logs

# 扩容(2个实例)
fly scale count 2

# 查看状态
fly status
# fly.toml — Fly.io 配置文件
app = "my-backend"
primary_region = "nrt"  # 日本东京(亚洲用户选这里)

[build]

[env]
  PORT = "3000"
  NODE_ENV = "production"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true   # 无流量时自动停机(省钱)
  auto_start_machines = true
  min_machines_running = 0

  [http_service.concurrency]
    type = "connections"
    hard_limit = 1000
    soft_limit = 800

[[vm]]
  size = "shared-cpu-1x"     # 最小规格,按需升级
  memory = "256mb"

数据库迁移(部署时自动运行)

# fly.toml — 添加 release 命令(部署前自动运行)
[deploy]
  release_command = "bunx prisma migrate deploy"

Cloudflare Workers — 边缘部署

Cloudflare Workers 运行在 CF 的 300+ 个全球节点上,代码离用户最近的地方执行。限制:不能使用 Node.js 特定 API,不能访问文件系统。Hono 完美支持 Workers:

npm create hono@latest my-worker -- --template cloudflare-workers
// src/index.ts — Cloudflare Workers 版本
import { Hono } from 'hono';

// 定义环境变量类型(绑定 CF 的 KV、D1 等)
type Bindings = {
  DB: D1Database;        // Cloudflare D1(SQLite)
  KV: KVNamespace;       // Cloudflare KV 存储
  JWT_SECRET: string;    // Workers Secrets
};

const app = new Hono<{ Bindings: Bindings }>();

app.get('/api/users', async (c) => {
  // 查询 D1 数据库(全球复制的 SQLite)
  const { results } = await c.env.DB
    .prepare('SELECT id, name, email FROM users LIMIT 20')
    .all();
  return c.json(results);
});

app.get('/api/cache/:key', async (c) => {
  // 读取 KV 缓存
  const cached = await c.env.KV.get(c.req.param('key'), 'json');
  if (cached) return c.json({ source: 'cache', data: cached });
  return c.json({ source: 'miss' }, 404);
});

export default app; // Workers 使用 default export
# 部署到 Cloudflare Workers
npx wrangler deploy

# 本地开发(模拟 CF 环境)
npx wrangler dev

# 设置 Secret
npx wrangler secret put JWT_SECRET

环境变量管理

# 推荐目录结构
.env                  # 提交到 git:公开的默认值
.env.example          # 提交到 git:模板文档
.env.local            # 不提交:本地覆盖
.env.test             # 不提交:测试环境
.env.production       # 不提交:生产环境(或使用平台 Secrets)
// lib/env.ts — 使用 Zod 验证环境变量
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_ACCESS_SECRET: z.string().min(32),
  JWT_REFRESH_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
  GITHUB_CLIENT_ID: z.string().optional(),
  GITHUB_CLIENT_SECRET: z.string().optional(),
});

// 启动时验证所有环境变量,缺失则立即失败
const result = envSchema.safeParse(process.env);
if (!result.success) {
  console.error('❌ 环境变量配置错误:', result.error.flatten().fieldErrors);
  process.exit(1);
}

export const env = result.data;

GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Test and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        ports: ['5432:5432']

    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with: { bun-version: latest }

      - run: bun install --frozen-lockfile
      - run: bun run typecheck      # tsc --noEmit
      - run: bun test --coverage
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: fly deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

零停机滚动更新

# Fly.io 默认支持滚动更新
# 新版本实例启动并通过健康检查后,旧实例才会停止
fly deploy

# 手动回滚到上一个版本
fly releases list
fly deploy --image registry.fly.io/my-app:v123

# 蓝绿部署:保留 n 个旧实例直到新实例就绪
fly deploy --strategy bluegreen

生产清单:① 健康检查端点就绪;② 优雅关机(SIGTERM 处理);③ 日志结构化(JSON 格式);④ 错误监控(Sentry);⑤ 性能监控(OpenTelemetry);⑥ 数据库连接池配置合理;⑦ 速率限制防止滥用;⑧ HTTPS 强制;⑨ 安全头部(helmet)。

优雅关机(Graceful Shutdown)

// 处理 SIGTERM(容器停止信号)
const server = Bun.serve({ port: 3000, fetch: app.fetch });

const shutdown = async (signal: string) => {
  console.log(`收到 ${signal},开始优雅关机...`);

  // 停止接受新请求
  server.stop(true); // true = 等待现有请求完成

  // 等待队列任务完成(给30秒)
  await emailWorker.close();

  // 断开数据库连接
  await prisma.$disconnect();

  console.log('关机完成');
  process.exit(0);
};

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

恭喜完成本教程! 你已经掌握了从运行时选择到生产部署的完整后端开发知识链:Node.js 事件循环 → Bun 工具链 → Hono API → tRPC 类型安全 → Prisma 数据库 → 认证系统 → WebSocket → 任务队列 → 自动化测试 → 生产部署。现在去构建真实的项目吧!