Next.js: Docker 自托管与部署

最后更新:2026-08-26

自托管部署让你完全掌控应用的运行环境——当合规、成本或网络要求让你无法使用云平台时,Docker 是你最可靠的伙伴。

1. 你将学到


2. 一个 DevOps 工程师的真实故事

(1) 痛点:客户要求数据不能出境

Charlie 在一家服务于中东金融机构的 SaaS 公司工作。他们的 TaskFlow 产品需要部署在沙特阿拉伯的本地数据中心——客户要求所有用户数据必须物理存储在沙特境内。

但 Vercel 没有沙特区域的数据中心。Charlie 面临的问题:

问题 影响
数据主权合规 沙特金融监管要求数据不出境
网络延迟 从欧洲服务器访问延迟 > 200ms
供应商锁定 Vercel 每月账单 $2,000+
内网要求 客户希望部署在企业内网

(2) Docker 自托管的解法

Charlie 用 Docker 构建了可移植的部署包:

BASH
# 一次构建,到处运行
docker build -t taskflow:latest .
docker run -p 3000:3000 \
  -e DATABASE_URL="postgresql://..." \
  -e AUTH_SECRET="..." \
  taskflow:latest

(3) 收益

维度 Vercel Docker 自托管
数据位置 仅限 Vercel 区域 任意数据中心
月度成本 $2,000+ $300(服务器)
部署延迟 全球 ~100ms 本地 < 20ms
供应商锁定 低(可迁移)

3. output: 'standalone' 配置

Next.js 16 的 output: 'standalone' 模式会创建一个独立的 Node.js 服务器,包含所有运行所需的文件。

100%
graph TB
    A[next.config.js] --> B[output: 'standalone']
    B --> C[构建过程]
    C --> D[.next/standalone/ 目录]
    D --> E[server.js — 独立 HTTP 服务器]
    D --> F[.next/static — 静态资源]
    D --> G[node_modules — 最小依赖]
    D --> H[package.json — 入口配置]
    
    style A fill:#cce5ff
    style D fill:#d4edda

(1) 配置 next.config.js

JS
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  
  // 需要外部化处理的依赖
  serverExternalPackages: ['@prisma/client'],
  
  // 生产环境优化
  productionBrowserSourceMaps: false,
  swcMinify: true,
  
  // 图片优化保留
  images: {
    unoptimized: false
  }
}

module.exports = nextConfig

(2) standalone 输出目录结构

TEXT 📖 仅展示
.next/standalone/
├── server.js              # 独立 HTTP 服务器(入口)
├── package.json           # 运行时依赖声明
├── node_modules/          # 仅生产依赖
├── .next/
│   ├── server/            # 服务端代码
│   ├── static/            # 静态资源
│   ├── build-manifest.json
│   └── ...
├── public/                # 公共静态资源
└── trace                  # 构建追踪

▶ 示例:验证 standalone 构建

BASH
# 构建项目
npm run build

# 查看 standalone 目录大小
du -sh .next/standalone/

# 启动独立服务器
node .next/standalone/server.js

# 在另一个终端验证
curl http://localhost:3000
💻 输出:

TEXT 📖 仅展示
.next/standalone/    358M    # 总大小
.next/standalone/server.js   # 入口文件(自动生成)

4. 多阶段 Docker 构建

多阶段构建将镜像分为三个阶段:依赖安装 → 应用构建 → 最小化运行环境。

DOCKERFILE
# ============================================
# Dockerfile — Next.js 16 多阶段构建
# ============================================

# --- 阶段 1: 依赖安装 ---
FROM node:20-alpine AS deps
LABEL stage=deps

RUN apk add --no-cache libc6-compat

WORKDIR /app

COPY package.json package-lock.json pnpm-lock.yaml ./

RUN npm ci --only=production && \
    npm cache clean --force

# --- 阶段 2: 构建 ---
FROM node:20-alpine AS build
LABEL stage=build

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production

RUN npm run build

# --- 阶段 3: 运行 ---
FROM node:20-alpine AS runner
LABEL stage=runner

RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

WORKDIR /app

# 从构建阶段复制产物
COPY --from=build --chown=nextjs:nodejs \
    /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs \
    /app/.next/static ./.next/static
COPY --from=build --chown=nextjs:nodejs \
    /app/public ./public

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

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
ENV NODE_ENV=production

CMD ["node", "server.js"]

(1) 构建与运行

BASH
# 构建镜像
docker build -t taskflow:latest .

# 查看镜像大小
docker images taskflow:latest

# 运行容器
docker run -d \
  --name taskflow-app \
  -p 3000:3000 \
  -e DATABASE_URL="postgresql://user:pass@host:5432/taskflow" \
  -e AUTH_SECRET="your-secret-key" \
  -e NEXT_PUBLIC_API_URL="https://api.taskflow.local" \
  --restart unless-stopped \
  taskflow:latest

(2) 各阶段镜像大小对比

阶段 基础镜像 大小 包含内容
deps node:20-alpine ~150 MB node_modules + 系统依赖
build node:20-alpine ~450 MB 源码 + node_modules + 构建产物
runner node:20-alpine ~358 MB standalone + 生产依赖
裸 node:20-alpine ~126 MB 基础系统

▶ 示例:Docker 运行环境变量注入

BASH
# 使用 .env 文件注入环境变量
cat > .env.production << EOF
DATABASE_URL=postgresql://user:pass@db:5432/taskflow
AUTH_SECRET=super-secret-key
NEXT_PUBLIC_API_URL=https://api.taskflow.local
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxx
REDIS_URL=redis://redis:6379
EOF

docker run -d \
  --name taskflow-app \
  --env-file .env.production \
  -p 3000:3000 \
  --network taskflow-net \
  taskflow:latest

5. Nginx 反向代理

Nginx 处理 SSL 终止、静态资源缓存和负载均衡,是生产环境的必备组件。

100%
graph LR
    A[用户浏览器] --> B[Nginx :443]
    B --> C{路径匹配}
    C -->|/_next/static/*| D[Nginx 直接服务<br/>缓存 1 年]
    C -->|/api/health| E[Next.js :3000]
    C -->|/*| E
    B --> F[SSL 终止<br/>Let's Encrypt]
    
    style B fill:#cce5ff
    style D fill:#d4edda

(1) Nginx 配置

NGINX
# nginx/nginx.conf
upstream nextjs_upstream {
    server app:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name taskflow.local;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name taskflow.local;

    # SSL 证书
    ssl_certificate /etc/nginx/ssl/taskflow.crt;
    ssl_certificate_key /etc/nginx/ssl/taskflow.key;
    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 X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # 日志
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # 静态资源缓存(由 Next.js 处理)
    location /_next/static/ {
        proxy_pass http://nextjs_upstream;
        expires 365d;
        add_header Cache-Control "public, immutable";
    }

    location /static/ {
        proxy_pass http://nextjs_upstream;
        expires 30d;
        add_header Cache-Control "public";
    }

    # 健康检查端点
    location /api/health {
        proxy_pass http://nextjs_upstream;
        access_log off;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    # 所有其他请求转发到 Next.js
    location / {
        proxy_pass http://nextjs_upstream;
        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_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}

6. PM2 进程守护

PM2 确保 Node.js 进程在崩溃后自动重启,并提供日志管理和集群模式。

(1) PM2 配置

JS
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'taskflow',
    script: 'server.js',
    cwd: '/app',
    
    // 集群模式(使用所有 CPU 核心)
    exec_mode: 'cluster',
    instances: 'max',
    
    // 环境变量
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      HOSTNAME: '0.0.0.0'
    },
    
    // 日志配置
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    error_file: '/var/log/pm2/taskflow-error.log',
    out_file: '/var/log/pm2/taskflow-out.log',
    merge_logs: true,
    
    // 自动重启
    max_restarts: 10,
    restart_delay: 1000,
    min_uptime: 5000,
    
    // 内存监控
    max_memory_restart: '500M',
    
    // 健康检查
    listen_timeout: 3000,
    kill_timeout: 5000
  }]
}

(2) PM2 Docker 集成

DOCKERFILE
# 在 runner 阶段安装 PM2
FROM node:20-alpine AS runner

RUN npm install -g pm2 && \
    addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs

WORKDIR /app

COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=build --chown=nextjs:nodejs /app/public ./public
COPY --chown=nextjs:nodejs ecosystem.config.js ./

USER nextjs

EXPOSE 3000

# 使用 PM2 启动集群模式
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

▶ 示例:PM2 常用命令

BASH
# 查看所有进程
pm2 list

# 查看日志
pm2 logs taskflow
pm2 logs taskflow --lines 100

# 监控资源
pm2 monit

# 重新加载(零停机)
pm2 reload taskflow

# 停止/重启
pm2 stop taskflow
pm2 restart taskflow

# 保存当前进程列表
pm2 save
pm2 startup

7. Docker Compose 三容器编排

Docker Compose 编排 App + Nginx + PostgreSQL 三个容器,一键启动完整环境。

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

networks:
  taskflow-net:
    driver: bridge

volumes:
  postgres-data:
    driver: local
  nginx-logs:
    driver: local

services:
  # === 1. PostgreSQL 数据库 ===
  db:
    image: postgres:16-alpine
    container_name: taskflow-db
    restart: unless-stopped
    networks:
      - taskflow-net
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d
    environment:
      POSTGRES_USER: taskflow
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: taskflow
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U taskflow"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  # === 2. Next.js 应用 ===
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: runner
    image: taskflow:latest
    container_name: taskflow-app
    restart: unless-stopped
    networks:
      - taskflow-net
    depends_on:
      db:
        condition: service_healthy
    environment:
      NODE_ENV: production
      PORT: 3000
      HOSTNAME: "0.0.0.0"
      DATABASE_URL: postgresql://taskflow:${DB_PASSWORD}@db:5432/taskflow
      AUTH_SECRET: ${AUTH_SECRET}
      AUTH_URL: ${AUTH_URL}
      NEXT_PUBLIC_API_URL: ${PUBLIC_API_URL}
      NEXT_PUBLIC_POSTHOG_KEY: ${POSTHOG_KEY:-}
    env_file:
      - .env.production
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 3s
      retries: 3

  # === 3. Nginx 反向代理 ===
  nginx:
    image: nginx:alpine
    container_name: taskflow-nginx
    restart: unless-stopped
    networks:
      - taskflow-net
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - nginx-logs:/var/log/nginx
    depends_on:
      app:
        condition: service_healthy

(1) 环境变量文件

BASH
# .env.production(不要提交到 Git)
DB_PASSWORD=StrongPassword123!
AUTH_SECRET=your-auth-secret-key-min-32-chars
AUTH_URL=https://auth.taskflow.local
PUBLIC_API_URL=https://api.taskflow.local
POSTHOG_KEY=phc_exampleKey123

(2) 启动与管理

BASH
# 首次启动
docker compose up -d

# 查看日志
docker compose logs -f app
docker compose logs -f nginx

# 重新构建应用
docker compose build app
docker compose up -d app

# 更新数据库迁移
docker compose exec app npx prisma migrate deploy

# 查看运行状态
docker compose ps

# 停止所有服务
docker compose down

# 完全清理(含卷)
docker compose down -v

▶ 示例:docker-compose.override.yml(开发环境)

YAML
# docker-compose.override.yml
version: '3.8'

services:
  app:
    build:
      target: build  # 开发阶段用 build 而非 runner
    environment:
      NODE_ENV: development
    volumes:
      - ./src:/app/src:ro
      - ./public:/app/public:ro
    command: npm run dev  # 使用开发服务器

  db:
    ports:
      - "5432:5432"  # 开发时暴露数据库端口

  nginx:
    ports:
      - "3000:80"  # 开发时简化端口映射

8. 运行时环境变量注入

Docker 容器的环境变量在运行时注入,而非构建时——这样可以一份镜像部署到多个环境。

100%
graph LR
    A[Docker 构建] --> B[镜像<br/>(无环境变量)]
    B --> C[运行时注入]
    C --> D[开发环境 .env.dev]
    C --> E[测试环境 .env.test]
    C --> F[生产环境 .env.prod]
    D --> G[容器启动]
    E --> G
    F --> G
    G --> H[server.js 读取 process.env]
    
    style B fill:#cce5ff
    style G fill:#d4edda

(1) 构建时 vs 运行时变量

变量类型 注入时机 示例 存储位置
构建时 docker build NEXT_PUBLIC_*、版本号 Dockerfile ARG
运行时 docker run DATABASE_URLAUTH_SECRET docker compose env_file
混合 两者都需要 NEXT_PUBLIC_API_URL 构建时注入给前端,运行时给后端

(2) 构建时变量注入

DOCKERFILE
# Dockerfile 使用 ARG 传递构建时变量
FROM node:20-alpine AS build

ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

ARG SENTRY_DSN
ENV SENTRY_DSN=$SENTRY_DSN

RUN npm run build
BASH
# 构建时传入变量
docker build \
  --build-arg NEXT_PUBLIC_API_URL=https://api.taskflow.com \
  --build-arg SENTRY_DSN=https://xxx@sentry.io/123 \
  -t taskflow:latest .

▶ 示例:运行时环境验证脚本

TS
// src/lib/env.ts
// 运行时环境变量验证
function getRequiredEnvVar(name: string): string {
  const value = process.env[name]
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`)
  }
  return value
}

export const env = {
  databaseUrl: getRequiredEnvVar('DATABASE_URL'),
  authSecret: getRequiredEnvVar('AUTH_SECRET'),
  authUrl: process.env.AUTH_URL || 'http://localhost:3000',
  publicApiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000',
  posthogKey: process.env.NEXT_PUBLIC_POSTHOG_KEY,
  nodeEnv: process.env.NODE_ENV || 'development',
  isProduction: process.env.NODE_ENV === 'production',
  port: parseInt(process.env.PORT || '3000', 10)
}

9. 完整示例:TaskFlow Docker 部署

BASH
# ============================================
# 生产部署脚本:deploy.sh
# 功能:构建 → 迁移 → 启动 → 健康检查
# ============================================

#!/bin/bash
set -euo pipefail

echo "=== TaskFlow Production Deployment ==="

# 1. 加载环境变量
if [ ! -f .env.production ]; then
    echo "ERROR: .env.production not found"
    exit 1
fi
source .env.production

# 2. 构建 Docker 镜像
echo "Building Docker image..."
docker compose build app

# 3. 启动数据库(如果未运行)
echo "Starting database..."
docker compose up -d db
echo "Waiting for database to be ready..."
sleep 5

# 4. 运行数据库迁移
echo "Running database migrations..."
docker compose run --rm app npx prisma migrate deploy

# 5. 启动应用和 Nginx
echo "Starting application and Nginx..."
docker compose up -d app nginx

# 6. 健康检查
echo "Running health check..."
for i in {1..10}; do
    if curl -s -o /dev/null -w "%{http_code}" http://localhost:80/api/health | grep -q 200; then
        echo "Health check passed!"
        break
    fi
    echo "Waiting... ($i/10)"
    sleep 3
done

# 7. 清理旧镜像
echo "Cleaning up old images..."
docker image prune -f

# 8. 部署信息
echo ""
echo "=== Deployment Complete ==="
echo "App:      http://localhost:80"
echo "API:      http://localhost:80/api/health"
echo "DB:       postgresql://taskflow@localhost:5432/taskflow"
echo "Logs:     docker compose logs -f app"
echo "Restart:  docker compose restart app"
TS
// src/app/api/health/route.ts
// ============================================
// 健康检查 API:被 Docker HEALTHCHECK 调用
// ============================================
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function GET() {
  const checks = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    checks: {} as Record<string, boolean>
  }

  try {
    // 检查数据库连接
    await prisma.$queryRaw`SELECT 1`
    checks.checks.database = true
  } catch {
    checks.checks.database = false
    checks.status = 'degraded'
  }

  try {
    // 检查 Redis(如果配置了)
    // await redis.ping()
    checks.checks.redis = true
  } catch {
    checks.checks.redis = false
    if (!checks.checks.database) {
      checks.status = 'unhealthy'
    }
  }

  const statusCode = checks.status === 'healthy' ? 200 : 503

  return NextResponse.json(checks, { status: statusCode })
}

❓ 常见问题

Q output: 'standalone'output: 'export' 有什么区别?
A standalone 生成一个包含 Node.js 服务器的独立包,支持 SSR/ISR/API Routes 等所有 Next.js 特性。export 生成纯静态 HTML(禁用 SSR),适用于 CDN 托管。Docker 自托管必须使用 standalone 模式。
Q 多阶段构建为什么比单阶段好?
A (1) 镜像更小——运行阶段只包含运行所需的最小文件(358MB vs 1.2GB);(2) 更安全——构建工具和源码不在最终镜像中;(3) 构建缓存更高效——依赖层不常变,可重复利用 Docker 缓存层。
Q Nginx 是必需的还是可有可无?
A 生产环境强烈推荐 Nginx:(1) SSL 终止——处理 HTTPS 证书;(2) 静态资源缓存——降低 Node.js 负载;(3) 安全头注入——XSS/CSP/ HSTS 等;(4) 负载均衡——多实例时分发请求。简单内网环境可以跳过,直接暴露 Next.js 端口。
Q PM2 和 Docker restart policy 需要同时用吗?
A 推荐同时使用。Docker 的 restart: unless-stopped 处理容器级别的崩溃(如 OOM),PM2 处理 Node.js 进程级别的崩溃(如未捕获异常)。PM2 还提供日志轮转、集群模式、零停机重启等 Docker 自身不具备的功能。
Q Docker Compose 和 Kubernetes 应该选哪个?
A 单机部署选 Docker Compose(配置简单、学习成本低);多机集群、自动扩缩、服务发现需求选 Kubernetes。对于中小团队(1-5 台服务器),Docker Compose + Swarm 模式足够应对大多数场景。

📖 小节


📝 作业

  1. 基础题(⭐):创建一个包含 output: 'standalone'next.config.js,编写多阶段 Dockerfile,成功构建并运行 docker run 后通过 curl localhost:3000 验证。

  2. 进阶题(⭐⭐):在 Docker Compose 中添加 Nginx 反向代理容器:(1) 配置 SSL 自签名证书;(2) 添加静态资源缓存规则;(3) 配置 /_next/static 缓存 365 天;(4) 验证 HTTPS 访问正常。

  3. 挑战题(⭐⭐⭐):构建完整的 CI/CD + Docker 自托管流水线:(1) GitHub Actions 自动构建 Docker 镜像并推送到 GHCR;(2) 在目标服务器上通过 SSH 拉取新镜像;(3) 使用 Docker Compose 零停机更新(docker compose up -d --no-deps --build app);(4) 配置 PM2 集群模式和日志轮转。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏