前言
入职两个月后,我开始了第一个完全由我自己开发的网站项目。本网站的目的是 以及 ,峰会期间还需要在网站上进行 。从网站开发到部署在线,可以说有无数的坑,最终实现的网站在这里 https://apisix-summit.org/
背景
由于时间有限,整个网站的架构是基于 vercel 网站脚手架 virtual-event-starter-kit 以上开发,不需要从头开始建设项目。 脚手架是用的 实现服务端渲染,预设了许多第三方平台接口,如 datoCMS 和 upstash。
当时我对 react 不太熟悉,更别说了 框架和一些 react 系的第三方库,所以这种发展不仅是一项任务,更是一个学习过程。
部署过程
构建服务器基本环境
最初的网站是通过的 vercel 部署,但后期需要转移 AWS 部署并配置三台裸金属服务器 AWS Elastic Load Balancing 负载平衡。因为网站使用服务端渲染,不能直接打成静态包,放在里面 CDN 在使用之前,必须运行服务。
为了防止服务挂断,我用了 pm2 守护服务流程,关于 node.js 和 pm2 我执行的安装操作如下:
sudo apt update sudo apt install nodejs npm -y sudo npm i n pm2 yarn -g n stable
安装在这里的原因 是因为 apt 安装的 node.js 太旧了,通过 来快速切换下 node.js 保证依赖性能够顺利安装的版本。
简化部署过程
因为每次都有三台服务器, github 合并新代码后,我不得不手动拉三台服务器,构建新的服务。更新一次需要十多分钟,所以我想写一个 CD 自动化部署,所以我努力学习 AWS pipeline ,但最终,公司老板说没有必要自动部署,以防代码中有 bug 三台服务器中的代码一旦自动部署,同时出现问题,人工操作更可靠。
所以我还是要老老实实去服务器操作,但是我写了一个小脚本,帮我减少了很多繁琐的操作。脚本内容如下:
# deploy.sh cd apache-apisix-summit-website git config --global --add safe.directory /home/ubuntu/apache-apisix-summit-website git pull origin main sudo yarn && sudo yarn build sudo pm2 restart 0
具体操作是去 github 上拉下代码, 然后重新打包,重启服务。这里需要注意的是,如果你想使用这种方法,从 github 需要先配置拉代码 ssh 密钥,然后将远程仓库设置为 github 里 ssh 协议地址。
这里其实也可以用 docker 将产品打包成镜像,然后在服务器上运行,但是
next.js 与负载均衡
部署完成后,我发现每个页面之间的切换速度非常慢。通过浏览器控制台,我发现每次切换页面都会重新加载所有资源,所以我推断这是一个与服务平衡相关的问题。经过不断的搜索,我终于看到了这篇文章 : How to Deploy Next.js on Multiple Servers
一般来说,每次执行都意味着每次执行 build 操作时,Next.js 都会产生新的建筑 ID,用于识别新生成的唯一例子。如果将构建后的包放在多个服务器上,则构建在每个服务器上 ID 都一样,Next.js 判断加载页面 ID 这将是一样的 ,也就是说,重用一些依赖。
但我们目前的操作是在三台服务器上建立自己的包,所以在三台服务器上 ID 不统一,当我们访问网站时,由于负载平衡,每次访问的服务器可能会有所不同 Next.js 判断到了 ID 会有变化 ,重新加载资源会导致新页面访问缓慢。
既然知道了原因,就可以开始解决问题了,Next.js 是 的构建 ID ,我们需要构建三台服务器 ID 统一,在 next.js 有这样一篇文档 Configuring the Build ID 告诉我们,我们可以在配置文件中以以下方式定制构建 ID :
module.exports = {
generateBuildId: async () => {
// You can, for example, get the latest git commit hash here return 'my-build-id' }, }
但是构建的 ID 如果使用静态,新版本的包将无法被使用 Next.js 识别并继续使用旧的依赖性,因此我们需要引入与版本相关的值。一旦网站版本更新,它将同步更新。本文推荐了一种使用它的方法 next-build-id
建造这个包 ID
const nextBuildId = require('next-build-id'); module.exports = {
// ... generateBuildId: () => nextBuildId({
dir: __dirname }) };
nextBuildId
会返回一个本地 git 存储库最新的 git commit 的哈希值,这样咱们更新代码后就会更新构建 ID ,并且保证在三台服务器上都可以同步了。
单人部署小妙招
如果说你只是个人开发者,并且没有使用 github 等工具进行代码托管,那么你也可以使用 scp 进行产物构建后的传输,下面是代码实现:
// 部署服务器
(async () => {
const client = require("scp2")
const ora = await import("ora")
const chalk = require("chalk")
const spinner = ora.default(chalk.green("正在发布。。。"))
/* host: 服务器ip port:scp上传的端口号 (默认:22) username:服务器账号 password:服务器密码 path:部署到服务器的路径 */
spinner.start()
client.scp(
"./dist/",
{
host: server.host,
port: server.port,
username: server.username,
password: server.password,
path: server.path,
},
(err) => {
spinner.stop()
if (!err) {
console.log(chalk.green("成功"))
} else {
console.log(err)
}
}
)
})()
只需要将 修改为你的服务器相关的信息,使用 node.js 执行一下这个脚本就可以将你的构建产物发送到服务器上指定的位置。
docker 部署
目前大部分的前端都会了解到 docker 容器技术,使用 docker 进行网站打包的话需要先写一个 dockerfile 将网站打包为镜像,对于网站而言 ockerfile
基本的思路就是如下几点:
- 将当前目录代码复制到容器中,并设置工作目录
- 安装依赖
- 打包构建
- 启动项目
像 react 和 vue 的项目,dockerfile 网上随便就能搜到,针对自己的项目进行一些修改即可,而 next.js 的项目官方也为我们提供了一个现成的 dockerfile :
# Install dependencies only when needed
FROM node:16-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 yarn.lock ./
RUN yarn install --frozen-lockfile
# If using npm with a `package-lock.json` comment out above and use below instead
# COPY package.json package-lock.json ./
# RUN npm ci
# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
# If using npm comment out above and use below instead
# RUN npm run build
# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
使用官方的 dockerfile 进行打包时,记得在 next.config.js 中将 outputStandalone
开启:
module.exports = {
experimental: {
outputStandalone: true,
},
}
这样打包出来的镜像会比用常规方式打包出来的小很多,常规方式打包大概 1-2Gb ,而用这种方式打包则只有 200-300 Mb ,具体的原理我没有深入了解,仅仅只是尝试了一下。
将站点打包成镜像后,我们还需要把镜像同步至服务器中,并且服务器还得主动去拉最新的镜像重新 run 一次,这一步实现的方式有很多,讲下思路:
- 在服务器上跑一个服务,当接受到指定的请求时就会执行
docker pull xxx
拉镜像和docker run xxx
运行容器 - 在 github action 中构建镜像完成后将镜像推到例如 dockerhub 等镜像托管平台。
- 推送后向服务器发送一个请求,告诉服务器该更新了,这一步最好传下环境变量让服务端判断一下,避免被其他人攻击。当然也可以手动执行请求的发送。
具体的实现方式就因项目而异啦。
总结
这篇文章主要记录一下 next.js 的部署方式和踩坑,当然如果你是传统网站或者后端服务也可以用作参考,如果对你有帮助可以点赞支持下~