Skip to content

Docker 部署

Dockerfile

dockerfile
# 构建阶段
FROM node:20-alpine AS builder

WORKDIR /app

# 先复制依赖文件(利用缓存层)
COPY package.json package-lock.json ./

# 安装依赖
RUN npm ci

# 复制源码
COPY . .

# 构建
RUN npm run build

# 生产阶段
FROM node:20-alpine

WORKDIR /app

# 只复制构建产物
COPY --from=builder /app/.output .output
COPY --from=builder /app/package.json .

# 设置环境变量
ENV HOST=0.0.0.0
ENV PORT=3000
ENV NODE_ENV=production

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

多阶段构建要点

问题解决原因
镜像太大多阶段构建,生产阶段只复制 .output构建阶段有 node_modules(~500MB),生产阶段不需要
构建缓存COPY package.jsonCOPY .依赖不变时复用缓存层,只重新复制源码
为什么用 Alpinenode:20-alpinenode:20 小 ~900MBAlpine 基于 musl libc,不含 apt 等多余包

为什么复制 package.json

部分运行时可能需要读取 package.json 中的版本信息或脚本。如果你确定不需要,可以只复制 .output

Docker Compose

yaml
# docker-compose.yml
version: '3.8'

services:
  # Nuxt 应用
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NUXT_JWT_SECRET=${JWT_SECRET}
      - NUXT_DB_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/flutter_api
      - NUXT_REDIS_URL=redis://redis:6379
      - NUXT_SMS_SECRET_ID=${SMS_SECRET_ID}
      - NUXT_SMS_SECRET_KEY=${SMS_SECRET_KEY}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  # PostgreSQL
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=flutter_api
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Redis
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Nginx
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

端口暴露安全

生产环境安全警告

上面的配置将 PostgreSQL (5432) 和 Redis (6379) 端口映射到了宿主机,这在生产环境中是严重的安全隐患

yaml
# ❌ 生产环境不要这样写(数据库端口暴露到公网)
db:
  ports:
    - "5432:5432"    # 任何人都可以尝试连接你的数据库

# ✅ 生产环境只暴露 Nginx 端口
db:
  # 不设置 ports,只在 Docker 内部网络通信
  # app 通过 db:5432 访问(Docker 内部 DNS)

安全原则

服务端口映射原因
Nginx80:80443:443必须暴露,用户通过浏览器访问
App不需要暴露Nginx 反向代理到 app:3000,外部不直接访问
PostgreSQL不暴露只需 Docker 内部 app → db:5432
Redis不暴露只需 Docker 内部 app → redis:6379
yaml
# ✅ 生产环境 docker-compose.yml
services:
  app:
    build: .
    # 不暴露端口,只通过 Nginx 访问
    environment:
      - NUXT_DB_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/flutter_api
    ...

  db:
    image: postgres:16-alpine
    # 不设置 ports!只在 Docker 内部网络可访问
    ...

  redis:
    image: redis:7-alpine
    # 不设置 ports!
    # 如果需要命令行调试,用 docker compose exec redis redis-cli
    ...

  nginx:
    ports:
      - "80:80"
      - "443:443"
    ...

开发时如何连接数据库?

开发时可以用 docker compose exec db psql -U postgres 进入容器操作,或者在 docker-compose.override.yml 中临时暴露端口(该文件不提交 Git)。

depends_on 和 healthcheck

yaml
app:
  depends_on:
    db:
      condition: service_healthy  # 等待 DB 健康检查通过才启动
    redis:
      condition: service_healthy

db:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres"]
    interval: 5s   # 每 5 秒检查一次
    timeout: 5s    # 超时时间
    retries: 5     # 连续 5 次失败才标记为 unhealthy

为什么需要 healthcheck?

depends_on 默认只等容器启动,不等服务就绪。PostgreSQL 容器启动后需要几秒才能接受连接。没有 healthcheck 的话,app 可能先于 DB 启动,导致连接失败。

环境变量文件

env
# .env.docker
JWT_SECRET=your-jwt-secret
DB_PASSWORD=your-db-password
SMS_SECRET_ID=your-sms-id
SMS_SECRET_KEY=your-sms-key
WECHAT_PAY_MCH_ID=your-mch-id
WECHAT_PAY_API_KEY=your-api-key
ALIPAY_APP_ID=your-app-id
ALIPAY_PRIVATE_KEY=your-private-key

.env.docker vs .env 的区别

  • .env:Nuxt 开发模式使用,变量名以 NUXT_ 前缀
  • .env.docker:Docker Compose 使用,变量名无 NUXT_ 前缀

Docker Compose 的 ${JWT_SECRET} 读取的是 .env.docker 中的 JWT_SECRET,然后通过 environment 映射为 NUXT_JWT_SECRET

环境变量安全清单

  • [ ] .env.docker.gitignore
  • [ ] 密码使用强随机值(openssl rand -hex 32
  • [ ] 不使用默认密码(如 postgres123456
  • [ ] 提供 .env.docker.example 模板(提交 Git),不含真实值

Docker 命令

bash
# 构建并启动
docker compose up -d --build

# 查看日志
docker compose logs -f app

# 重启
docker compose restart app

# 停止
docker compose down

# 停止并删除数据(⚠️ 会删除数据库和 Redis 数据)
docker compose down -v

# 进入容器
docker compose exec app sh

# 数据库迁移
docker compose exec app npx drizzle-kit migrate

# 查看 PostgreSQL 数据
docker compose exec db psql -U postgres -d flutter_api

# 查看 Redis 数据
docker compose exec redis redis-cli

docker compose down vs docker compose down -v

  • down:停止并删除容器,但保留数据卷(数据库数据不丢失)
  • down -v:同时删除数据卷(数据库数据永久删除,不可恢复)

Nginx 配置

nginx
# nginx.conf
upstream nuxt_app {
    server app:3000;
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    client_max_body_size 10M;

    location / {
        proxy_pass http://nuxt_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;
    }

    location /_nuxt/ {
        proxy_pass http://nuxt_app;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Nginx 配置要点

配置作用原因
return 301 https://...HTTP 强制跳转 HTTPS所有请求必须走加密通道
client_max_body_size 10M限制请求体大小防止大文件上传攻击,与上传 API 的 5MB 限制配合
proxy_set_header X-Real-IP传递真实客户端 IPNuxt 的 getRequestHeader(event, 'x-forwarded-for') 获取真实 IP
X-Forwarded-Proto告知 Nuxt 原始协议Nuxt 需要知道客户端用的是 HTTPS,才能设置 secure Cookie
expires 1y/_nuxt/长缓存 Nuxt 静态资源/_nuxt/ 下的文件包含内容哈希,内容变化文件名也变,可安全长缓存

为什么 Nuxt 静态资源可以缓存 1 年?

Nuxt 构建的 JS/CSS 文件名包含内容哈希(如 entry.abc123.js),文件内容变化时哈希也变,URL 也变。旧缓存不会被用到,新文件会重新请求。

CI/CD 集成

GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /app/flutter-api-server
            git pull origin main
            docker compose up -d --build
            docker compose exec app npx drizzle-kit migrate

CI/CD 安全注意事项

安全项做法原因
SSH 密钥存在 GitHub Secrets 中不提交到代码仓库
服务器密码存在 GitHub Secrets 中CI 日志是公开的,不能有明文密码
.env.docker不在 Git 中,服务器手动维护密钥不应进入版本控制
部署后验证添加健康检查步骤确保部署成功,不会上线一个挂掉的服务

更安全的部署方式

生产环境建议使用蓝绿部署或滚动更新,避免停机。上面的 SSH 拉取方式在构建期间会有短暂不可用。

多阶段构建优化

dockerfile
# 更小的镜像
FROM node:20-alpine

# 只安装生产依赖
WORKDIR /app
COPY .output .output

EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

镜像大小从 ~1GB 减少到 ~150MB。

为什么可以这么小?

Nuxt 的 .output 目录只包含运行时需要的代码,不包含 node_modules、源码、开发依赖。Nitro 会将所有依赖打包到 .output/server/index.mjs 中。

生产环境部署检查清单

安全

  • [ ] 数据库和 Redis 端口未暴露到公网
  • [ ] .env.docker 使用强密码(openssl rand -hex 32
  • [ ] HTTPS 已启用(Let's Encrypt 或其他证书)
  • [ ] Nginx 安全头已配置
  • [ ] NODE_ENV=production 已设置
  • [ ] Redis 设置密码(requirepass

性能

  • [ ] Nuxt 静态资源缓存(Nginx expires 1y
  • [ ] Gzip/Brotli 压缩已开启(Nginx 配置)
  • [ ] 数据库连接池已配置(postgres.js max 参数)
  • [ ] 使用多实例 + Nginx 负载均衡(可选)

运维

  • [ ] 日志持久化(Docker logs 或挂载卷)
  • [ ] 数据库定期备份(pg_dump cron job)
  • [ ] 健康检查端点可用(GET /health
  • [ ] 监控告警已配置(磁盘/CPU/内存)
  • [ ] docker compose down -v 不会误操作(数据卷有备份)

常见生产环境问题

问题原因解决
Cookie 设置失败Nginx 代理后 Nuxt 不知道原始协议确保 Nginx 设置 X-Forwarded-Proto
SSR 页面空白数据库连接超时检查 db:5432 内部 DNS 解析、healthcheck
静态资源 404Nginx 未代理 /_nuxt/添加 location /_nuxt/ 代理规则
上传文件丢失容器重启后 uploads/ 数据丢失使用 Docker volume 挂载 uploads 目录
构建后 API 报错环境变量未传入容器检查 docker-compose.ymlenvironment 配置
Redis 限流失效内存版限流,多实例不共享切换到 Redis 版限流中间件

基于 Nuxt 4 官方文档整理编写