服务器容器化部署 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
参数会将构建好的镜像推送到镜像仓库。执行完命令会创建
amd64
和 arm64
两种架构的镜像,并将它们作为一个多平台镜像推送到镜像仓库。在拉取镜像时,Docker 会根据运行平台自动选择合适的镜像版本。
其他
回到最开始,因为不部署在 Vercel 等 PaaS 上,那么除了利用 Docker 等容器化工具,还有没有其他方案呢。答案就是我们可以使用 Vercel 的开源替代品,比如下面这两个项目:
仅个人角度出发,dokploy 的体验比 coolify 更好一点,coolify 的功能很多,但是其层级组织嵌套显得有点庞杂,整个 UI 显得很 Professional。还是 Dokploy 的操作来得直接一点。
后续如果有空就把之前折腾的记录整理出来。(挖坑)