服务器容器化部署 Next.js 应用的 Dockerfile 示例

status
Published
type
Post
slug
self-host-nextjs-app-in-docker-container
date
Oct 5, 2024
tags
Docker
Web
Note
summary
文章讨论了如何将 Next.js 应用容器化部署,文章说明了通过配置 next.config.js 中的 output: "standalone",可以在构建时生成一个仅包含生产所需文件的文件夹,从而简化 Docker 镜像的大小。提供了一个示例 Dockerfile,详细描述了如何构建和运行 Next.js 应用的 Docker 镜像,并介绍了使用 Docker Buildx 构建多平台镜像的步骤。最后,文章提到除了使用 Docker 进行容器化,还可以考虑开源替代方案,如 Coolify 和 Dokploy,以便更灵活地部署 Next.js 应用。

背景

Next.js
是一个用于构建全栈 Web 应用程序的 React 框架。你可以使用 React 组件来构建用户界面,而 Next.js 则提供额外的功能和优化。
在底层,Next.js 还抽象化并自动配置了 React 所需的工具,如打包、编译等。这使你能够专注于构建应用程序,而不必花时间在配置上。
无论你是个人开发者还是大型团队的一员,Next.js 都能帮助你构建交互式、动态且快速的 React 应用程序。
在一众 PaaS 平台中,目前 Next.js 项目部署的首选平台自然是其背后的公司—— Vercel 。对一般个人用户来说,Vercel 的免费额度已经足够使用,而且其整个操作管理体验,UI 交互都很不错。但在某些情况下我们可能还是会需要将 Next.js 项目部署到服务器(VPS)上,那就得考虑 Next.js 项目的容器化了。

Next.js 项目容器化

容器化(Docker)允许你在将你的应用在一致的环境中运行,并轻松地将其部署到任何服务器或云服务商。

Next.js output 可选配置

Next.js 可以在构建时自动创建一个 standalone 文件夹,只包含生产部署所需的文件,包括 node_modules 中的依赖文件。我们可以通过此特性使最终得到的 Docker 镜像尽可能小一些。
next.config.js 中启用它:
module.exports = { output: "standalone", // ... rest of your config };
构建时会创建 .next/standalone 文件夹,其可以用于部署而无需额外的 node_modules。此外,构建后会得到一个最小的 server.js 文件,可以用它代替 next start 命令。默认情况下,构建时不会自动复制 public 或 .next/static 静态文件文件夹,因为这些文件夹理论上应由 CDN 处理,也可以手动将这些文件夹复制到 standalone/public 和 standalone/.next/static 文件夹中,之后 server.js 文件将接管对这些文件的访问。

Dockerfile 编写

在项目根目录下创建一个 Dockerfile

展开查看 未配置 Next.js output 时的 Dockerfile
# 安装依赖 FROM node:22-alpine AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json ./ RUN npm install # 构建镜像 FROM node-22-alpine AS runner WORKDIR /app # 创建非 root 用户 (以用户名 app 为例) RUN addgroup -S app && adduser -S app -G app USER app # 从第一阶段复制依赖 node_modules COPY . . COPY --from=deps /app/node_modules ./node_modules RUN npm run build # 生产环境变量 # Next.js 会收集完全匿名的使用数据用于分析。 # 详情请访问:https://nextjs.org/telemetry # 若要禁用此功能,请取消注释以下行: # ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production ENV HOSTNAME="0.0.0.0" EXPOSE 3000 # 启动应用程序 CMD ["npm", "start"]
这个 Dockerfile 示例实现了通过多阶段构建机制来创建一个 Next.js 应用镜像。
第一阶段 (deps):
  • 基于 node:22-alpine 镜像,安装必要的依赖 libc6-compat
  • 设置工作目录为 /app
  • 复制 package.json 和 package-lock.json 文件。
  • 使用 npm install 安装项目依赖。
第二阶段 (runner):
  • 基于 node:22-alpine 镜像。
  • 设置工作目录为 /app
  • 创建非 root 用户 app 并切换到该用户,提高安全性。
  • 从第一阶段复制项目文件和 node_modules,避免重复安装依赖。
  • 执行 npm run build 构建 Next.js 应用。
  • 设置生产环境变量,包括 NODE_ENV 和 HOSTNAME
  • 暴露 3000 端口。
  • 使用 npm start 启动应用程序。
其中 多阶段构建将依赖安装和应用构建分离,减小最终镜像体积。使用 非 root 用户增强安全性。将Alpine Linux作为轻量级基础镜像,进一步减小镜像体积。
 

如下为配置了 output: "standalone" 后的 Dockerfile 示例:
# 基础镜像 FROM node:22-alpine AS base # 依赖安装 FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi # Next.js 构建打包 FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Next.js 会收集完全匿名的使用数据用于分析。 # 详情查看:https://nextjs.org/telemetry # 若要在构建时禁用数据收集,请取消注释以下行: # ENV NEXT_TELEMETRY_DISABLED=1 RUN \ if [ -f yarn.lock ]; then yarn run build; \ elif [ -f package-lock.json ]; then npm run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ else echo "Lockfile not found." && exit 1; \ fi # 构建最终运行镜像 FROM base AS runner WORKDIR /app ENV NODE_ENV=production # 若要在运行时禁用数据收集,请取消注释以下行: # ENV NEXT_TELEMETRY_DISABLED=1 # 创建非 root 用户 (以用户名 app 为例) RUN addgroup -S app && adduser -S app -G app USER app # 从构建阶段复制 output 及静态文件 # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=app:app /app/.next/standalone ./ COPY --from=builder --chown=app:app /app/public ./public COPY --from=builder --chown=app:app /app/.next/static ./.next/static EXPOSE 3000 ENV PORT=3000 # server.js 启动 # https://nextjs.org/docs/pages/api-reference/next-config-js/output ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"]
 
与此同时在 Dockerfile 同级目录创建一个 .dockerignore 文件,以从 Docker 构建上下文中排除不必要的文件:
node_modules Dockerfile README.md .dockerignore .git .next .env*
现在就能够使用 Docker 命令来构建和运行你的应用程序了:
docker build -t nextjs-app .
在本地试运行 Docker 容器:
docker run -p 3000:3000 nextjs-app
此时打开 http://localhost:3000,你应该会看到 Next.js 应用的主页。
 

构建多平台镜像

有时我们需要在本机构建其他平台架构的镜像,那就可以通过 使用 Docker Buildx 来构建多平台镜像。
以上述 Dockerfile 为例,我们需要调整其中的镜像指令,更改如下:
# 使用 Buildx 构建多平台镜像 FROM --platform=$BUILDPLATFORM node:22-alpine AS deps ... ... FROM --platform=$BUILDPLATFORM node:22-alpine AS runner
即在 FROM 指令中添加 --platform=$BUILDPLATFORM 参数,使 Docker Buildx 能识别目标平台并选择合适的镜像版本。
BUILDPLATFORM 是 Docker Buildx 提供的自动变量,用于表示当前构建的目标平台。
此时构建操作命令如下:
  • 初始化 Buildx:
    • docker buildx create --use
  • 构建多平台镜像:
    • docker buildx build -t nextjs-app --platform linux/amd64,linux/arm64 . --push
    • -push 参数会将构建好的镜像推送到镜像仓库。
执行完命令会创建 amd64arm64 两种架构的镜像,并将它们作为一个多平台镜像推送到镜像仓库。
在拉取镜像时,Docker 会根据运行平台自动选择合适的镜像版本。
 

其他

回到最开始,因为不部署在 Vercel 等 PaaS 上,那么除了利用 Docker 等容器化工具,还有没有其他方案呢。答案就是我们可以使用 Vercel 的开源替代品,比如下面这两个项目:
仅个人角度出发,dokploy 的体验比 coolify 更好一点,coolify 的功能很多,但是其层级组织嵌套显得有点庞杂,整个 UI 显得很 Professional。还是 Dokploy 的操作来得直接一点。
后续如果有空就把之前折腾的记录整理出来。(挖坑