(逃离Vercel部署)Next JS + Docker + Github Action + 阿里云自动化部署

700 阅读8分钟

1. 编写Dockerfile文件,将nextjs项目打包成镜像

  • 首先找到nextjs的官方文档,其中有一个将nextjs项目打包成镜像的示例代码

Next JS官方文档——部署

docker_build_01.png

  • 找到示例代码中的Dockerfile文件,直接copy(不需要任何修改,先打包后再根据需要进行修改)

Docker示例代码

docker_build_02.png

FROM node:18-alpine AS base

# Install dependencies only when needed
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* ./
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

# Rebuild the source code only when needed
FROM base 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 \
  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

# Production image, copy all the files and run next
FROM base 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

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# 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

# server.js is created by next build from the standalone output
# <https://nextjs.org/docs/pages/api-reference/next-config-js/output>
CMD HOSTNAME="0.0.0.0" node server.js
  • 执行 docker build 命令,将项目打包成 docker 镜像,在本地进行测试
docker image build -t my-first-portfolio:latest

docker镜像运行截图: docker_build_03.png

注意: 上面截图中的docker build 命令是带有环境变量参数的,最开始的时候可以不用传入

本地docker仓库截图: docker_build_04.png

  • 使用docker run 命令,启动打包好的镜像,对项目进行测试

docker_build_05.png

  • 测试没问题,就可以进行项目自动化部署的过程了

2.阿里云docker环境安装

阿里云docker安装文档

  • 按照官方安装docker的步骤一步一步进行,没啥大问题

aliyun_docker_init_01.png

根据自己的服务器系统,选择对应的安装步骤

aliyun_docker_init_02.png

  • docker安装后,只允许root用户执行docker相关命令记得添加其他用户的操作权限

aliyun_docker_init_03.png

# 非root用户 执行docker命令报错
# 添加用户到docker权限
sudo gpasswd -a YourUserName docker

newgrp docker

# 再次执行命令
docker ps
  • 至此,阿里云服务器的Docker环境就安装成功啦

3. 初始化设置阿里云镜像仓库(这里镜像仓库可以根据自身需要来创建,可以直接使用dockerhub)

这个镜像仓库的作用就是我们用docker build构建生成的镜像推送到这个仓库,后面远程服务器的docker来拉取这个镜像,实现自动化部署

  • 访问阿里云镜像服务,找到示例列表(一般购买了服务器后,实例列表里面自动会有一个个人实例),点击进入这个实例

阿里云镜像服务

aliyun_images_01.png

aliyun_images_02.png

  • 在创建镜像仓库之前,需要创建一个命名空间

aliyun_images_03.png

aliyun_images_04.png

  • 创建成功后,再在镜像仓库页面中,创建自己的镜像库,填写好相关信息后,绑定代码源

aliyun_images_05.png

aliyun_images_06.png

aliyun_images_07.png

  • 这样阿里云镜像仓库就创建完毕了,点击对应的仓库名称,跳转到仓库基本信息页面,这个页面有相关的docker操作指南

aliyun_images_08.png

aliyun_images_09.png

  • 在本地测试docker login 到阿里云镜像仓库 aliyun_images_10.png

第一次登录会失败,因为阿里云镜像仓库的密码和阿里云账号的密码是不一致的,需要在阿里云镜像仓库这边设置初始密码

aliyun_images_11.png

  • 测试登录成功后,就可以进行本地镜像推送了,我这里没有在本地测试推送,直接使用github action自动化创建镜像 并推送到阿里云镜像仓库

4. Github Action自动化部署配置

参考文档:

juejin.cn/post/705081…

blog.bot-flow.com/nextjs-dock…

  • 第一步就是使用github action自动创建docker镜像,并将镜像推送到阿里云镜像仓库(这里参照阿里云镜像仓库的操作文档来实现)
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # 拉取main分支代码
      - name: Checkout
        uses: actions/checkout@v4.1.7

      # 制作docker镜像并推送到阿里云容器镜像服务
      - name: build and push docker image
        run: |
          echo ${{ secrets.ALIYUN_DOCKER_PASSWORD }} | docker login ${{ vars.REGISTRY_MIRROR }} --username ${{ secrets.ALIYUN_DOCKER_USERNAME }} --password-stdin

          docker image build -t ${{ vars.APP_NAME }}:latest .
          docker tag ${{ vars.APP_NAME }} ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest
          docker push ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest
          docker logout

          echo "app name ${{ vars.REGISTRY_MIRROR }}"

其中的secrets和vars变量是在github setting中配置的, secrets是加密的,vars是明文,看个人需要添加对应的字段 github_action_build_00.png

  • 第二步,由于执行了第一步,这个项目的docker镜像已经打包并上传到阿里云镜像仓库,可以在阿里云镜像仓库查看是否存在镜像版本

github_action_build_01.png

执行结果:

github_action_build_02.png github_action_build_03.png

  • 第三步,通过ssh连接到阿里云服务器,执行docker pull和docker run,实现服务的docker镜像部署启动

注意: 下面这段代码和第一步中要合并起来,注意yml文件的空格并进行格式化对齐

   		# 登录远程服务器,拉取镜像,制作并重启容器
      # <https://github.com/marketplace/actions/remote-ssh-commands>
      - name: ssh remote deploy
        uses: fifsky/ssh-action@master
        with:
          command: |
            cd /
            echo -e "1.docker login start==>"
            echo ${{ secrets.ALIYUN_DOCKER_PASSWORD }} | docker login ${{ vars.REGISTRY_MIRROR }} --username ${{ secrets.ALIYUN_DOCKER_USERNAME }} --password-stdin

            echo -e "2.docker stop myblog container==>"
            docker container stop ${{ vars.APP_NAME }}

            echo -e "3.docker conatainer rm==>"
            docker container rm ${{ vars.APP_NAME }}

            echo -e "4.docker image rm==>"
            docker image rm ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest

            echo -e "5.docker pull==>"
            docker pull ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest

            echo -e "6.docker container create and start==>"
            docker container run -d -p 3000:3000 --name ${{ vars.APP_NAME }} ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest

            echo -e "7.docker logout==>"
            docker logout
          host: ${{ secrets.HOST }}
          user: ${{ secrets.USER }}
          pass: ${{ secrets.PASSWORD }}

执行结果: github_action_build_04.png github_action_build_05.png

  • 至此,Next JS项目使用docker完成镜像打包和自动化部署就大功告成了,我们把代码合进main分支就会触发github action构建,完成自动化部署

验证部署是否成功:

github_action_build_06.png

5.NextJS Production环境变量注入

如果创建一个.env.production 文件,将需要的环境变量进行写入并保存,在本地进行docker build和docker run命令,我们可以发现环境变量能够正常注入到打包的镜像中,Next JS应用的一些请求能够正常发送

但是由于.env.production 文件中保存有一些敏感信息, 如果直接将.env.production 文件上传到github代码仓库中,就会导致信息泄露,所以我们这边需要采取将环境变量手动注入的方式来实现

  • 第一步,修改Dockerfile文件,我们可以通过docker --build-arg来传入外部参数,在 Dockerfile 通过ARG变量来接收,然后将这些变量写入到.env.production中

完整配置如下:

FROM node:20-alpine AS base

# Install dependencies only when needed
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* ./
RUN \
  if [ -f yarn.lock ]; then yarn config set registry <https://registry.npmmirror.com/>; \
  elif [ -f package-lock.json ]; then npm config set registry <https://registry.npmmirror.com/>; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm config set registry <https://registry.npmmirror.com/>; \
  else echo "Lockfile not found." && exit 1; \
  fi

RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 设置构建参数
ARG NOTION_TOKEN
ARG NOTION_ABOUT_DATABASE_ID
ARG NOTION_PROJECT_DATABASE_ID
ARG NOTION_EXPERIENCE_DATABASE_ID
# 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 echo "NOTION ---> "
RUN echo "NOTION_TOKEN=${NOTION_TOKEN}" >> .env.production
RUN echo "NOTION_ABOUT_DATABASE_ID=${NOTION_ABOUT_DATABASE_ID}" >> .env.production
RUN echo "NOTION_PROJECT_DATABASE_ID=${NOTION_PROJECT_DATABASE_ID}" >> .env.production
RUN echo "NOTION_EXPERIENCE_DATABASE_ID=${NOTION_EXPERIENCE_DATABASE_ID}" >> .env.production

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

# Production image, copy all the files and run next
FROM base 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

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# 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

# server.js is created by next build from the standalone output
# <https://nextjs.org/docs/pages/api-reference/next-config-js/output>
CMD HOSTNAME="0.0.0.0" node server.js
  • 本地执行docker build和docker run命令,看打包后的镜像环境变量是否正常注入(看接口请求是否成功)

next_production_dev_01.png

docker image build -t my-first-portfolio:latest \                       54m 39s   20:37:23
            --build-arg NOTION_TOKEN=YOUR ARG \
            --build-arg NOTION_ABOUT_DATABASE_ID=YOUR ARG \
            --build-arg NOTION_PROJECT_DATABASE_ID=YOUR ARG \
            --build-arg NOTION_EXPERIENCE_DATABASE_ID=YOUR ARG \
            .
docker run -p 8081:3000 my-first-portfolio
  • 查看本地项目运行结果,如果没问题,修改Github Action中的yml文件,并将相关的环境变量注入

next_production_dev_02.png

完整github action配置代码:

name: Docker Image CI

on:
  push: # push 时触发ci
    branches: [main] # 作用于main分支
  # pull_request:
  #   branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # 拉取main分支代码
      - name: Checkout
        uses: actions/checkout@v4.1.7

      # 制作docker镜像并推送到阿里云容器镜像服务
      - name: build and push docker image
        run: |
          echo ${{ secrets.ALIYUN_DOCKER_PASSWORD }} | docker login ${{ vars.REGISTRY_MIRROR }} --username ${{ secrets.ALIYUN_DOCKER_USERNAME }} --password-stdin

          docker image build -t ${{ vars.APP_NAME }}:latest \
            --build-arg NOTION_TOKEN=${{ secrets.NOTION_TOKEN }} \
            --build-arg NOTION_ABOUT_DATABASE_ID=${{ secrets.NOTION_ABOUT_DATABASE_ID }} \
            --build-arg NOTION_PROJECT_DATABASE_ID=${{ secrets.NOTION_PROJECT_DATABASE_ID }} \
            --build-arg NOTION_EXPERIENCE_DATABASE_ID=${{ secrets.NOTION_EXPERIENCE_DATABASE_ID }} \
            .
          docker tag ${{ vars.APP_NAME }} ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest
          docker push ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest
          docker logout

          echo "app name ${{ vars.REGISTRY_MIRROR }}"
      # 登录远程服务器,拉取镜像,制作并重启容器
      # <https://github.com/marketplace/actions/remote-ssh-commands>
      - name: ssh remote deploy
        uses: fifsky/ssh-action@master
        with:
          command: |
            cd /
            echo -e "1.docker login start==>"
            echo ${{ secrets.ALIYUN_DOCKER_PASSWORD }} | docker login ${{ vars.REGISTRY_MIRROR }} --username ${{ secrets.ALIYUN_DOCKER_USERNAME }} --password-stdin

            echo -e "2.docker stop myblog container==>"
            docker container stop ${{ vars.APP_NAME }}

            echo -e "3.docker conatainer rm==>"
            docker container rm ${{ vars.APP_NAME }}

            echo -e "4.docker image rm==>"
            docker image rm ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest

            echo -e "5.docker pull==>"
            docker pull ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest

            echo -e "6.docker container create and start==>"
            docker container run -d -p 3000:3000 --name ${{ vars.APP_NAME }} ${{ vars.REGISTRY_MIRROR }}/${{ vars.REGISTRY_NAMESPACE }}/${{ vars.APP_NAME }}:latest

            echo -e "7.docker logout==>"
            docker logout
          host: ${{ secrets.HOST }}
          user: ${{ secrets.USER }}
          pass: ${{ secrets.PASSWORD }}
  • 至此,Next JS项目的生产环境的环境变量算是真正注入成功,只需要等自动化部署成功验证接口请求是否正常

当前页面会通过接口获取Notion文档中的数据:

next_production_dev_03.png