Docker 部署 React 与 Next.js:开发环境到生产上线的完整流程

详解如何用 Docker 容器化 React SPA 和 Next.js 应用:开发环境热重载配置、Nginx 静态文件托管、Next.js standalone 输出优化、多阶段构建减小镜像、docker-compose 编排全栈部署。

将前端应用 Docker 化已经是现代软件交付的事实标准。它抹平了开发机与服务器之间的环境差异,将构建产物、运行时依赖与操作系统一同打包成不可变镜像,真正实现了"一次构建、到处运行"。无论是基于 Vite 的轻量 React SPA,还是带有服务端渲染能力的 Next.js 全栈应用,合理的容器化策略决定了上线后的性能、稳定性与弹性伸缩能力。本文将从最基础的 Dockerfile 写起,逐步深入到环境变量运行时注入、Nginx 高级优化、全栈服务编排、ISR 持久化、CDN 边缘缓存以及健康监控等生产级细节,力求为所有正要将前端项目迁移到容器平台的团队提供一份可直接落地的手册。

一、React SPA(Vite / CRA)的 Docker 化

React 单页应用本质上是一组静态资源(HTML、JS、CSS 与各类媒体文件)。生产部署的核心思路是在容器内完成构建,再使用高性能 Web 服务器对外提供服务。多阶段构建(Multi-stage Build)可以把 Node.js 运行时与构建依赖隔离在构建层,最终运行层只保留 Nginx 及编译后的产物,从而把镜像体积压缩到数十 MB 级别。

1.1 Dockerfile

下面的 Dockerfile 分为 builder 与 runner 两个阶段。builder 使用 node:20-alpine 安装依赖并执行构建,runner 基于 nginx:alpine 提供对外的 HTTP 服务。由于 Nginx 只做静态文件读取,不需要 Node 运行时,因此最终镜像安全可控且极小。

# ========== 构建阶段 ==========
FROM node:20-alpine AS builder
WORKDIR /app

# 利用 Docker 缓存层:仅当 package 变化时才重新 npm ci
COPY package*.json ./
RUN npm ci --prefer-offline --no-audit --no-fund

COPY . .
RUN npm run build

# ========== 运行阶段 ==========
FROM nginx:alpine
LABEL maintainer="devops@example.com"

# 把构建产物复制到 Nginx 默认目录
COPY --from=builder /app/dist /usr/share/nginx/html
# 自定义 Nginx 配置:包含 SPA 回退、压缩与缓存头
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

构建与运行命令如下:

docker build -t spa-app:latest .
docker run -d -p 3000:80 --name spa spa-app:latest

1.2 Nginx 配置

对于 SPA,最关键的是 try_files 指令,它保证任何路由(如 /dashboard/profile)都会回退到 index.html,随后由前端路由库接管渲染。同时需要对带 Hash 的静态资源设置长期缓存,避免用户反复下载未变更的 JS 与 CSS。

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;
    charset utf-8;

    # Gzip 压缩传输
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_types text/plain text/css application/json application/javascript text/xml;

    # SPA 路由回退
    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }

    # 带 Hash 的静态资源(Vite 默认会在构建时为文件名追加 content-hash)
    location ~* \.(js|css|woff2?|ttf|eot|ico|png|jpg|jpeg|svg|gif|webp)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # 不带 Hash 的公共资源(如 favicon)
    location = /favicon.ico {
        expires 7d;
        add_header Cache-Control "public";
        access_log off;
    }
}

1.3 docker-compose.yml

在本地集成测试时,通过 Compose 统一管理服务端口、环境变量与卷映射,可以显著提高开发效率。

version: '3.8'
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:80"]
      interval: 30s
      timeout: 5s
      retries: 3

二、Next.js 的 Docker 化

Next.js 既有静态页面(SSG),也有服务端渲染(SSR)与接口路由(API Routes),因此不能像 React SPA 那样直接丢给 Nginx。官方推荐的 output: 'standalone' 会把 Node.js 服务端及其所需依赖打包到 .next/standalone 目录,实现零额外 node_modules 的运行。

2.1 next.config.js

开启 standalone 输出后,Next.js 会自动分析页面所需的运行时依赖并精简打包。

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  poweredByHeader: false,      // 隐藏 X-Powered-By 头
  compress: false,             // 交由 Nginx 或反向代理处理压缩
  images: {
    minimumCacheTTL: 60 * 60 * 24 * 7, // 图片优化缓存 7 天
  },
};

module.exports = nextConfig;

2.2 Dockerfile

本例使用三阶段构建:deps 阶段独立完成依赖安装,builder 阶段负责构建,runner 阶段仅保留运行产物。运行用户从 root 降级到 nextjs,遵循最小权限原则。libc6-compat 用于解决 Alpine Linux 下某些原生模块兼容性问题。

# ========== 依赖阶段 ==========
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci --prefer-offline --no-audit --no-fund

# ========== 构建阶段 ==========
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# ========== 运行阶段 ==========
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000

# 创建非 root 用户
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

# standalone 模式下只需复制 public、standalone 与 static
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# 自定义启动脚本(支持运行时注入环境变量)
COPY --chown=nextjs:nodejs entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh

USER nextjs
ENTRYPOINT ["./entrypoint.sh"]
CMD ["node", "server.js"]

2.3 基础 docker-compose.yml

在单应用部署中,至少需要暴露 Node.js 端口并挂载 .env。

version: '3.8'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
      interval: 20s
      timeout: 5s
      retries: 3

三、开发 vs 生产配置

开发环境追求更快的反馈与调试便利性,生产环境则关注体积、性能与安全。以下是各维度的详细对比,帮助团队在制定 CI/CD 流程时有据可依。

维度开发生产
构建命令next dev(基于 Webpack/Turbopack)next build(全量预编译)
热重载(HMR)开启,保存即时刷新关闭,减少服务负担
源码映射开启,浏览器可直接调试 TSX关闭或仅保留独立 SourceMap 文件
日志级别debug / verbosewarn / error
镜像体积大(含 devDependencies、源码、调试工具)小(standalone,无源码与构建依赖)
Node 进程数单进程即可借助 PM2 或 Cluster 模块多核利用
Nginx 反向代理不需要,直连 Next.js 端口强烈建议:提供压缩、缓存、HTTPS 终止
环境变量注入本地 .env 或 Compose 环境容器编排平台 Secret / ConfigMap
HTTPS不需要或使用 mkcert 自签名必须:Let’s Encrypt 或商业证书
监控控制台输出 + 本地 DevToolsAPM(如 Datadog / Sentry)+ 日志聚合

此外,开发阶段通常将源代码目录通过 volumes 映射到容器内,保证宿主机修改即时生效;生产阶段则完全使用镜像内构建产物,避免任何运行时文件漂移。

四、环境变量与运行时配置

Next.js 的环境变量分为两类:构建时变量与运行时变量。名称带 NEXT_PUBLIC_ 前缀的变量会在构建阶段被内联到客户端代码中,因此一旦镜像构建完成,这些值就不可更改。而在容器化交付场景下,同一份镜像需要在开发、测试、预发布与生产环境复用,硬编码显然不符合 twelve-factor app 规范。

4.1 NEXT_PUBLIC_ 前缀规则

Next.js 的编译器会在构建时扫描 process.env.NEXT_PUBLIC_* 并直接替换为字符串字面量。这意味着:

  • 它们必须存在于 docker build 阶段,否则会缺失或回退到默认值;
  • 无法在容器启动后通过 docker run -e 动态修改客户端行为;
  • 敏感信息(如 API Secret)不应使用此前缀。

服务端专用变量(如数据库连接串)不需要 NEXT_PUBLIC_ 前缀,可在运行容器时通过环境变量注入,Next.js 会在服务端渲染或 API Route 中实时读取。

4.2 运行时注入方案:env.sh + entrypoint

为了实现"一份镜像 + 多套配置",业界通行的做法是在容器启动时执行一段脚本:将外部传入的环境变量写入 window 对象,供前端 JavaScript 运行时读取。配合 Next.js standalone 运行时,可用 entrypoint 脚本在 node server.js 之前生成 env-config.js。

entrypoint.sh 示例:

#!/bin/sh
set -e

# 生成供客户端读取的运行时配置
PUBLIC_ENV_PREFIX="NEXT_PUBLIC_"
ENV_JS="/app/public/env-config.js"

echo "window.__ENV__ = {" > "$ENV_JS"
env | while IFS='=' read -r key value; do
  case "$key" in
    ${PUBLIC_ENV_PREFIX}*)
      printf '  "%s": "%s",\n' "$key" "$value" >> "$ENV_JS"
      ;;
  esac
done
echo "};" >> "$ENV_JS"

# 继续执行 Dockerfile 或 docker-compose 指定的默认命令
exec "$@"

前端在 pages/_app.tsx(或 app/layout.tsx)中引入该脚本:

import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <Script src="/env-config.js" strategy="beforeInteractive" />
      </head>
      <body>{children}</body>
    </html>
  );
}

业务代码中通过统一封装读取环境变量:

export const getEnv = (key: string, fallback = '') => {
  if (typeof window !== 'undefined' && (window as any).__ENV__) {
    return (window as any).__ENV__[key] || fallback;
  }
  return process.env[key] || fallback;
};

该方案既保留了 Next.js 的服务端直出能力,也让 NEXT_PUBLIC_* 真正变成启动时可调参数,适配 Kubernetes ConfigMap、Docker Compose .env 文件或 CI/CD 变量注入的任意场景。

五、Nginx 高级配置:Brotli 压缩、HTTP/2 Server Push、缓存策略

当 Next.js standalone 与 Nginx 共同部署时,Nginx 承担 TLS 终止、压缩、静态资源缓存以及负载均衡职责。以下配置面向生产级别,覆盖了现代 Web 性能优化所需的全部细节。

5.1 Brotli 压缩

Brotli 在同等质量下相比 Gzip 可减少约 20% 的文本传输体积。需要在 Nginx 编译或安装时启用 ngx_brotli 动态模块。基于官方 nginx:alpine 镜像的 Dockerfile:

FROM nginx:alpine AS nginx-builder
RUN apk add --no-cache git build-base pcre-dev zlib-dev openssl-dev cmake
WORKDIR /src
RUN git clone --depth=1 https://github.com/google/ngx_brotli.git && \
    cd ngx_brotli && git submodule update --init

FROM nginx:alpine
COPY --from=nginx-builder /src/ngx_brotli /usr/lib/nginx/modules/ngx_brotli
# 实际生产建议使用预编译的动态模块包或官方 nginx-mainline-alpine 自带 brotli 的镜像

在 nginx.conf 中启用:

http {
    # Brotli
    brotli on;
    brotli_comp_level 6;
    brotli_types text/plain text/css application/json application/javascript text/xml;

    # Gzip 降级兼容
    gzip on;
    gzip_vary on;
    gzip_comp_level 5;
    gzip_types text/plain text/css application/json application/javascript text/xml;
}

5.2 HTTP/2 Server Push

HTTP/2 Server Push 允许服务器在客户端请求 HTML 时主动推送关键 CSS 或 JS,减少往返延迟。Nginx 1.13.9 起支持 http2_push 指令。如果服务已升级到 HTTP/3(QUIC),可改用 Early Hints(103 Status)。

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

    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location / {
        proxy_pass http://nextjs_upstream;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 推送关键资源(需根据构建产物 Hash 动态调整文件名)
        http2_push /_next/static/css/critical.css;
        http2_push /_next/static/chunks/main.js;
    }
}

5.3 完整 Nginx 生产配置

以下是一份可直接挂载到容器 /etc/nginx/conf.d/default.conf 的配置文件,涵盖反向代理、缓存策略、安全响应头与静态文件托管。

upstream nextjs_upstream {
    server app:3000 max_fails=3 fail_timeout=10s;
}

server {
    listen 80;
    server_name _;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # 安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';" always;

    # 静态资源(Next.js standalone 内置 public 与 .next/static)
    location /_next/static/ {
        alias /app/.next/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    location /static/ {
        alias /app/public/static/;
        expires 7d;
        add_header Cache-Control "public";
        access_log off;
    }

    # Next.js 图片优化接口(不缓存,避免用户上传图片被误缓存)
    location /_next/image {
        proxy_pass http://nextjs_upstream;
        proxy_set_header Host $host;
        add_header Cache-Control "private, no-cache";
    }

    # 反向代理到 Next.js standalone
    location / {
        proxy_pass http://nextjs_upstream;
        proxy_http_version 1.1;
        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_buffering on;
        proxy_cache_valid 200 5m;
    }
}

六、Docker Compose 编排全栈(Frontend + API + Postgres + Redis + MinIO)

真实业务很少只有单独一个前端容器。下面提供一份全栈 docker-compose.yml,包含:前端服务、API 服务(可复用同一 Next.js standalone 镜像,也可独立 Node 服务)、PostgreSQL、Redis 与 MinIO(对象存储,替代 AWS S3 本地开发)。所有服务通过 app_network 桥接网络隔离,数据库与缓存不暴露到宿主机公网。

version: '3.8'

services:
  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:
      - frontend
      - api
    networks:
      - app_network
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/healthz"]
      interval: 30s
      timeout: 5s
      retries: 3

  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
      - NEXT_PUBLIC_API_URL=http://api:4000
      - NEXT_PUBLIC_S3_ENDPOINT=http://minio:9000
    networks:
      - app_network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
      interval: 20s
      timeout: 5s
      retries: 3

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://postgres:${DB_PASSWORD}@postgres:5432/myapp
      - REDIS_URL=redis://redis:6379
      - S3_ENDPOINT=http://minio:9000
      - S3_ACCESS_KEY=${MINIO_ROOT_USER}
      - S3_SECRET_KEY=${MINIO_ROOT_PASSWORD}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app_network
    restart: unless-stopped

  postgres:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: myapp
    networks:
      - app_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - app_network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    volumes:
      - minio_data:/data
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    ports:
      - "9000:9000"
      - "9001:9001"
    networks:
      - app_network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 5s
      retries: 3

volumes:
  postgres_data:
  redis_data:
  minio_data:

networks:
  app_network:
    driver: bridge

关键设计要点:

  • depends_on 的 condition: service_healthy 确保后端启动前数据库已就绪;
  • 所有密码通过 .env 注入,不硬编码在 YAML 中;
  • Nginx 作为唯一对外入口,提供日志统一收口与 TLS 终止;
  • 每个服务自带 healthcheck,Compose 可根据状态自动重启或告警。

七、Next.js ISR 与 Docker 持久化 Storage

增量静态再生(ISR)让 Next.js 在首次请求时后台重新生成页面,兼具 SSR 的实时性与 SSG 的高性能。但在容器环境中,ISR 生成的 HTML 与 JSON 默认写入本地文件系统。如果容器重启或横向扩展为多实例,每个 Pod 的文件系统相互独立,会导致缓存不一致。

7.1 ISR 原理简述

当用户访问一条带 ISR 的页面路由时:

  1. Next.js 先返回已缓存的静态 HTML(stale);
  2. 后台触发 getStaticProps 重新执行;
  3. 生成完成后,新 HTML 写入磁盘,后续请求直接命中最新版本。

这要求 .next/cache 与生成路径在实例间保持共享,否则用户会随机拿到新旧版本。

7.2 Docker 持久化方案

单节点 Compose 场景下,最直接的方法是将 .next/cache 挂载为命名卷:

services:
  frontend:
    build: ./frontend
    volumes:
      - nextjs_cache:/app/.next/cache
    environment:
      - NODE_ENV=production

多节点 Kubernetes 场景下,需要使用 ReadWriteMany(RWX)存储类,例如 NFS、AWS EFS 或 Azure Files:

# kubernetes-deployment.yaml(节选)
spec:
  template:
    spec:
      containers:
        - name: nextjs
          image: myapp/frontend:latest
          volumeMounts:
            - name: nextjs-cache
              mountPath: /app/.next/cache
      volumes:
        - name: nextjs-cache
          nfs:
            server: 10.0.0.5
            path: /exports/nextjs-cache

在 next.config.js 中可显式配置 ISR 再生并发数与超时:

const nextConfig = {
  output: 'standalone',
  experimental: {
    isrMemoryCacheSize: 50 * 1024 * 1024, // 50MB 内存缓存上限
  },
};
module.exports = nextConfig;

注意:ISR 不适合高频率更新(如秒级股票行情)。对于需要即时一致性的数据,应回退到 SSR 或客户端 SWR(stale-while-revalidate)轮询。

八、CDN 集成与边缘缓存(Vercel Edge / Cloudflare Workers / AWS CloudFront)

容器化并不等于自建所有基础设施。将 Next.js 部署在自有服务器或 Kubernetes 后,可通过 CDN 把静态资源、API 响应甚至 HTML 页面缓存到离用户最近的 PoP 节点,进一步降低延迟与回源带宽。

8.1 Cloudflare Workers 边缘改写

Cloudflare Workers 运行在 V8 isolate 中,可以在边缘节点对请求进行路由改写、A/B 测试或身份校验。以下脚本将 /api/* 以外的所有请求缓存到 Cloudflare 边缘,API 请求透传回源:

// cloudflare-worker.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // API 请求不走缓存
    if (url.pathname.startsWith('/api/')) {
      return fetch(request);
    }

    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;
    let response = await cache.match(cacheKey);

    if (!response) {
      response = await fetch(request, { cf: { cacheTtl: 300 } });
      response = new Response(response.body, response);
      response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=600');
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
    }

    return response;
  },
};

8.2 AWS CloudFront 缓存策略

使用 CloudFront 在 ECS 或 EC2 前方做缓存层时,建议为不同路径创建独立的缓存行为:

{
  "PathPattern": "/_next/static/*",
  "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
  "OriginRequestPolicyId": "88a5a4b4-7831-47bf-883e-871a5067b8b5"
}

其中默认的 Managed-CachingOptimized 策略会对静态资源启用长期缓存,同时对查询字符串与 Cookie 做最小化转发,提高缓存命中率。对需要 ISR 的页面,可在 getStaticProps 返回中设置 revalidate 与自定义 Cache-Control:

export async function getStaticProps() {
  const data = await fetchData();
  return {
    props: { data },
    revalidate: 60, // 每 60 秒后台再生一次
  };
}

九、健康检查、优雅关闭与 OOM 监控

生产系统必须具备快速发现故障与优雅处理终止信号的能力,否则 SIGKILL 会导致正在处理的请求直接断开,用户体验受损。

9.1 Dockerfile HEALTHCHECK

Docker 内置 HEALTHCHECK 指令可通过定期请求判断服务是否存活。对于 Next.js standalone,推荐暴露一个轻量 /api/health 接口:

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD wget --spider -q http://localhost:3000/api/health || exit 1

对应的简易健康接口(Next.js App Router):

// app/api/health/route.ts
export async function GET() {
  return Response.json({ status: 'ok', uptime: process.uptime() });
}

9.2 优雅关闭

Node.js 在接收到 SIGTERM 时有 10 秒(默认)的宽限期关闭连接。Next.js standalone 基于 Node HTTP Server,可通过自定义 server.js 或 next.config.js 中的 experimental 配置来拦截信号:

// server.ts(自定义启动入口,供 standalone 模式替换默认 server.js)
import { createServer } from 'http';
import next from 'next';

const app = next({ dev: false, dir: './' });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = createServer((req, res) => handle(req, res));

  const gracefulShutdown = (signal: string) => {
    console.log(`Received ${signal}, shutting down gracefully...`);
    server.close(() => {
      console.log('Server closed.');
      process.exit(0);
    });
    // 强制退出兜底
    setTimeout(() => process.exit(1), 15000);
  };

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

  server.listen(3000, () => console.log('Ready on http://localhost:3000'));
});

9.3 OOM 监控与资源限制

Node.js 运行在 V8 引擎中,其堆内存限制在 64 位系统上约为 1.4GB(旧版)或随系统内存动态调整。在容器环境中,必须为每个服务设置 memory limit,防止某个 Pod 耗尽宿主机资源:

services:
  frontend:
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M
    environment:
      - NODE_OPTIONS=--max-old-space-size=400

同时,建议在生产环境集成 node --prof 或 clinic.js 进行内存剖析,排查泄漏;并配合 Prometheus + Grafana 监控容器级别的 container_memory_usage_bytes 与 container_oom_events_total 指标。

十、React SPA 到 Next.js 迁移的 Docker 策略

已有项目从 React SPA 向 Next.js 迁移时,通常不可能一次性重写所有页面。合理的做法是在 Docker 层面实现增量灰度:将新页面部署到 Next.js 容器,老页面保留在原 SPA 容器,通过 Nginx 按路由路径分发流量。

10.1 双容器共存架构

假设 /dashboard 以后的页面已迁移到 Next.js,而 /legacy/* 仍需由旧 SPA 提供:

upstream spa_legacy {
    server spa:80;
}
upstream nextjs_new {
    server nextjs:3000;
}

server {
    listen 80;
    location /legacy/ {
        proxy_pass http://spa_legacy;
    }
    location / {
        proxy_pass http://nextjs_new;
    }
}

10.2 蓝绿部署

在 Compose 或 Kubernetes 中,通过同时存在 “Blue” 与 “Green” 两组 Deployment,实现零停机切换:

# docker-compose.blue-green.yml
services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - nextjs-blue

  nextjs-blue:
    image: myapp/frontend:v1.0.0
    profiles: ["blue"]

  nextjs-green:
    image: myapp/frontend:v1.1.0
    profiles: ["green"]

运维只需调整 Nginx upstream 指向 green,再逐步回收 blue,即可完成平滑升级。如果 green 出现问题,可在数秒内回切 upstream 到 blue,最大程度降低线上风险。


通过以上十个章节的逐层深入,从 React SPA 的轻量 Nginx 托管,到 Next.js standalone 的三阶段构建,再到环境变量运行时注入、Nginx 高级压缩与缓存、全栈 Docker Compose 编排、ISR 持久化共享、CDN 边缘加速、健康监控以及渐进式迁移策略,团队应当能够构建出一套具备高可用、高性能与可观测性的容器化前端交付体系。建议将本文的 Dockerfile、Compose 文件与 Nginx 配置纳入代码库版本管理,并在 CI/CD 流水线中以 docker buildx 进行多平台镜像构建,真正做到从提交到上线的全流程自动化。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章

  1. 短链接对 SEO 的影响与优化最佳实践
  2. UTM 参数 + 短链接:追踪每一条营销链路
  3. 私域流量运营中的短链接策略:从引流到转化