Docker 部署
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.json 再 COPY . | 依赖不变时复用缓存层,只重新复制源码 |
| 为什么用 Alpine | node:20-alpine 比 node:20 小 ~900MB | Alpine 基于 musl libc,不含 apt 等多余包 |
为什么复制 package.json?
部分运行时可能需要读取 package.json 中的版本信息或脚本。如果你确定不需要,可以只复制 .output。
Docker Compose
# 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) 端口映射到了宿主机,这在生产环境中是严重的安全隐患。
# ❌ 生产环境不要这样写(数据库端口暴露到公网)
db:
ports:
- "5432:5432" # 任何人都可以尝试连接你的数据库
# ✅ 生产环境只暴露 Nginx 端口
db:
# 不设置 ports,只在 Docker 内部网络通信
# app 通过 db:5432 访问(Docker 内部 DNS)安全原则:
| 服务 | 端口映射 | 原因 |
|---|---|---|
| Nginx | 80:80、443:443 | 必须暴露,用户通过浏览器访问 |
| App | 不需要暴露 | Nginx 反向代理到 app:3000,外部不直接访问 |
| PostgreSQL | 不暴露 | 只需 Docker 内部 app → db:5432 |
| Redis | 不暴露 | 只需 Docker 内部 app → redis:6379 |
# ✅ 生产环境 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
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.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) - [ ] 不使用默认密码(如
postgres、123456) - [ ] 提供
.env.docker.example模板(提交 Git),不含真实值
Docker 命令
# 构建并启动
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-clidocker compose down vs docker compose down -v
down:停止并删除容器,但保留数据卷(数据库数据不丢失)down -v:同时删除数据卷(数据库数据永久删除,不可恢复)
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 | 传递真实客户端 IP | Nuxt 的 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
# .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 migrateCI/CD 安全注意事项
| 安全项 | 做法 | 原因 |
|---|---|---|
| SSH 密钥 | 存在 GitHub Secrets 中 | 不提交到代码仓库 |
| 服务器密码 | 存在 GitHub Secrets 中 | CI 日志是公开的,不能有明文密码 |
.env.docker | 不在 Git 中,服务器手动维护 | 密钥不应进入版本控制 |
| 部署后验证 | 添加健康检查步骤 | 确保部署成功,不会上线一个挂掉的服务 |
更安全的部署方式
生产环境建议使用蓝绿部署或滚动更新,避免停机。上面的 SSH 拉取方式在构建期间会有短暂不可用。
多阶段构建优化
# 更小的镜像
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.jsmax参数) - [ ] 使用多实例 + Nginx 负载均衡(可选)
运维
- [ ] 日志持久化(Docker logs 或挂载卷)
- [ ] 数据库定期备份(
pg_dumpcron job) - [ ] 健康检查端点可用(
GET /health) - [ ] 监控告警已配置(磁盘/CPU/内存)
- [ ]
docker compose down -v不会误操作(数据卷有备份)
常见生产环境问题
| 问题 | 原因 | 解决 |
|---|---|---|
| Cookie 设置失败 | Nginx 代理后 Nuxt 不知道原始协议 | 确保 Nginx 设置 X-Forwarded-Proto |
| SSR 页面空白 | 数据库连接超时 | 检查 db:5432 内部 DNS 解析、healthcheck |
| 静态资源 404 | Nginx 未代理 /_nuxt/ | 添加 location /_nuxt/ 代理规则 |
| 上传文件丢失 | 容器重启后 uploads/ 数据丢失 | 使用 Docker volume 挂载 uploads 目录 |
| 构建后 API 报错 | 环境变量未传入容器 | 检查 docker-compose.yml 的 environment 配置 |
| Redis 限流失效 | 内存版限流,多实例不共享 | 切换到 Redis 版限流中间件 |