Dockerfile 采用了多阶段构建(multi-stage build)的方式,构建并部署一个基于 Node.js + pnpm 的 Web 应用(如 Next.js 项目),优化了镜像体积和构建速度,并特别适配国内的网络环境。
# ==========================================================================
# base image
# ==========================================================================
FROM node:20-alpine3.20 AS base
# if you located in China, you can use aliyun mirror to speed up
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata
RUN npm install -g pnpm@9.12.2
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# ==========================================================================
# install packages
# ==========================================================================
FROM base AS packages
WORKDIR /
COPY package.json .
COPY pnpm-lock.yaml .
# if you located in China, you can use taobao registry to speed up
# RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
RUN pnpm install --frozen-lockfile
# ==========================================================================
# build resources
# ==========================================================================
FROM base AS builder
WORKDIR /app/web
COPY --from=packages / .
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN pnpm build
# ==========================================================================
# production stage
# ==========================================================================
FROM base AS production
ENV NODE_ENV=production
ENV EDITION=SELF_HOSTED
ENV DEPLOY_ENV=PRODUCTION
ENV CONSOLE_API_URL=http://127.0.0.1:5001
ENV APP_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_URL=http://127.0.0.1:5001
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
ENV PM2_INSTANCES=2
# set timezone
ENV TZ=UTC
RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone
WORKDIR /app/web
COPY --from=builder /app/web/public ./public
COPY --from=builder /app/web/.next/standalone ./
COPY --from=builder /app/web/.next/static ./.next/static
COPY docker/entrypoint.sh ./entrypoint.sh
# ==========================================================================
# global runtime packages
# ==========================================================================
RUN pnpm add -g pm2 \
&& mkdir /.pm2 \
&& chown -R 1001:0 /.pm2 /app/web \
&& chmod -R g=u /.pm2 /app/web
ARG COMMIT_SHA
ENV COMMIT_SHA=${COMMIT_SHA}
USER 1001
EXPOSE 3000
ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]
总体结构(多阶段构建)
共分为 四个阶段:
阶段名 | 作用 |
---|---|
base |
定义基础环境:Node.js、Alpine、pnpm、镜像源等 |
packages |
安装依赖(package.json、pnpm-lock.yaml) |
builder |
拷贝源码、构建项目(如 Next.js) |
production |
最终部署环境,最小化体积,仅保留构建产物和运行依赖 |
各阶段详解
FROM node:20-alpine3.20 AS base
基础镜像使用 Node.js v20 + Alpine 3.20
,适用于构建轻量级镜像。
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
将 Alpine 的源切换到阿里云,加快国内构建速度。
RUN apk add --no-cache tzdata
安装时区数据包(tzdata),后续可以设置时区。
RUN npm install -g pnpm@9.12.2
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
全局安装 pnpm,设置环境变量,支持构建依赖。
FROM base AS packages
用于安装项目依赖,这个阶段只做依赖缓存,减少后续构建重复下载。
WORKDIR /
COPY package.json .
COPY pnpm-lock.yaml .
RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/
拷贝 lock 文件,锁定依赖版本,并使用淘宝镜像加速。此时不会拷贝源码,只处理依赖。
FROM base AS builder
用于正式构建项目
WORKDIR /app/web
COPY --from=packages / . # 拷贝 node_modules 等依赖
COPY . . # 拷贝项目源码
设置最大内存 4GB:
ENV NODE_OPTIONS="--max-old-space-size=4096"
执行构建:
RUN pnpm build
这一步将生成 .next
构建产物(静态文件、server 代码等)。
FROM base AS production
最终的部署镜像,构建产物被拷贝到此处。
设置环境变量:
ENV NODE_ENV=production
ENV EDITION=SELF_HOSTED
...
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1
ENV PM2_INSTANCES=2
时区设置为 UTC:
ENV TZ=UTC
RUN ln -s /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone
拷贝运行所需文件(非整个项目):
COPY --from=builder /app/web/public ./public
COPY --from=builder /app/web/.next/standalone ./
COPY --from=builder /app/web/.next/static ./.next/static
COPY docker/entrypoint.sh ./entrypoint.sh
添加 pm2,并设置目录权限:
RUN pnpm add -g pm2 \
&& mkdir /.pm2 \
&& chown -R 1001:0 /.pm2 /app/web \
&& chmod -R g=u /.pm2 /app/web
设置构建时参数 COMMIT_SHA
:
ARG COMMIT_SHA
ENV COMMIT_SHA=${COMMIT_SHA}
切换运行用户为非 root 用户(1001),暴露 3000 端口:
USER 1001
EXPOSE 3000
ENTRYPOINT ["/bin/sh", "./entrypoint.sh"]
最终效果:
这个 Dockerfile 会生成一个用于生产部署的最小 Node.js 镜像,特点是:
- 构建快(使用镜像加速与多阶段缓存)
- 运行轻(只包含
.next
产物和静态资源) - 易于部署(入口脚本控制进程启动,如用 pm2)
- 国内网络适配良好
entrypoint.sh
entrypoint.sh 脚本就是 Docker 镜像的启动脚本,它的作用是:
#!/bin/bash
# if you are using windows, you may need to convert the file to unix format
# you can use the Ubuntu terminal to convert this file to unix format
# otherwise, you may get the error after running the docker container
# sudo apt-get install dos2unix
# dos2unix entrypoint.sh
set -e
export NEXT_PUBLIC_DEPLOY_ENV=${DEPLOY_ENV}
export NEXT_PUBLIC_EDITION=${EDITION}
export NEXT_PUBLIC_API_PREFIX=${CONSOLE_API_URL}/console/api
export NEXT_PUBLIC_PUBLIC_API_PREFIX=${APP_API_URL}/api
export NEXT_PUBLIC_MARKETPLACE_API_PREFIX=${MARKETPLACE_API_URL}/api/v1
export NEXT_PUBLIC_MARKETPLACE_URL_PREFIX=${MARKETPLACE_URL}
export NEXT_PUBLIC_SENTRY_DSN=${SENTRY_DSN}
export NEXT_PUBLIC_SITE_ABOUT=${SITE_ABOUT}
export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
pm2 start /app/web/server.js --name dify-web --cwd /app/web -i ${PM2_INSTANCES} --no-daemon
- 设置必要的环境变量(
NEXT_PUBLIC_
这些是给前端用的) - 启动 PM2 来运行 Next.js 应用(通过
.next/standalone/server.js
)
下面我们来逐行讲解:
一、文件格式声明和兼容性说明
#!/bin/bash
声明该脚本使用 bash
执行。
# if you are using windows, you may need to convert the file to unix format
# you can use the Ubuntu terminal to convert this file to unix format
# otherwise, you may get the error after running the docker container
# sudo apt-get install dos2unix
# dos2unix entrypoint.sh
说明:如果你在 Windows 上创建该文件,会携带 CRLF
换行符,Linux 执行会报错(例如 bash^M: not found
)。所以推荐用 dos2unix
转换格式。
二、错误处理
set -e
含义:脚本执行中如果任一命令出错(返回非 0),立即退出,避免继续运行。
三、环境变量注入(给 Next.js 使用)
export NEXT_PUBLIC_DEPLOY_ENV=${DEPLOY_ENV}
export NEXT_PUBLIC_EDITION=${EDITION}
export NEXT_PUBLIC_API_PREFIX=${CONSOLE_API_URL}/console/api
export NEXT_PUBLIC_PUBLIC_API_PREFIX=${APP_API_URL}/api
export NEXT_PUBLIC_MARKETPLACE_API_PREFIX=${MARKETPLACE_API_URL}/api/v1
export NEXT_PUBLIC_MARKETPLACE_URL_PREFIX=${MARKETPLACE_URL}
这些变量会被暴露给 前端页面,通过 NEXT_PUBLIC_
这个前缀传递到浏览器中。比如:
NEXT_PUBLIC_API_PREFIX
会变成浏览器访问后端的接口前缀NEXT_PUBLIC_DEPLOY_ENV
表示部署环境(如 PRODUCTION)NEXT_PUBLIC_MARKETPLACE_API_PREFIX
是商城相关接口地址
接下来是监控和页面配置:
export NEXT_PUBLIC_SENTRY_DSN=${SENTRY_DSN}
export NEXT_PUBLIC_SITE_ABOUT=${SITE_ABOUT}
export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
其他配置项,都是用于前端参数控制:
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
四、启动 PM2 运行 .next/standalone/server.js
pm2 start /app/web/server.js \
--name dify-web \
--cwd /app/web \
-i ${PM2_INSTANCES} \
--no-daemon
解释:
参数 | 说明 |
---|---|
/app/web/server.js |
Next.js 编译后的服务端入口(由 output: standalone 自动生成) |
--name dify-web |
给 PM2 设置进程名 |
--cwd /app/web |
设置当前工作目录(更好地识别路径) |
-i ${PM2_INSTANCES} |
多实例模式,${PM2_INSTANCES} 变量在 Dockerfile 中默认是 2 |
--no-daemon |
前台运行(防止 PM2 自己变为后台进程,否则容器会退出) |
这样容器启动后,会:
- 用设置的环境变量启动一个 Next.js 应用
- 使用 PM2 多进程处理流量
- 绑定在
3000
端口上(由 Dockerfile 的EXPOSE 3000
指定)
总结:entrypoint.sh 的作用
功能 | 说明 |
---|---|
环境变量注入 | 把 .env 配置转换成 NEXT_PUBLIC_* 的形式供 Next.js 用 |
启动应用 | 用 PM2 启动构建产物 server.js ,并发运行,支持自动重启 |
兼容性说明 | 提醒 Windows 用户注意换行符问题 |
扩展建议(可选)
你可以在脚本里加入打印日志,便于调试:
echo "Starting Dify Web with environment: $DEPLOY_ENV"
echo "Console API: $CONSOLE_API_URL"
或者处理空变量问题,例如:
: "${PM2_INSTANCES:=2}" # 如果没有设置,则默认2