Garlic Garlic

个人网站的部署变迁史

发表于 阅读时长12分钟

首先是分享自己网站的整体变迁流程

第一阶段

最开始是使用hexo + github pages

第二阶段

nextjs blog template vercel 好处是方便,坏处是要翻墙。

第三阶段

隔了几年,使用cra来搭建react项目,然后改成使用vite,打包后手动上传,纯前端单页应用,再用nginx来起服务。

第四阶段

宝塔

第五阶段

docker容器化

Docker + Nginx

单个nextjs镜像在本地运行起来后,使用nginx代理到3000端口

Docker-Compose + Github actions

这应该是我个人网站的终极答案了,后续也不想继续折腾部署这块了。

推荐一个xshell工具,termius。

ACR_NAMESPACE是阿里云的容器镜像仓库的命名空间 ARC_USERNAME是阿里云的容器镜像仓库的用户名 ARC_PASSWORD是阿里云的容器镜像仓库的密码, ARC_REGISTRY是阿里云的容器镜像仓库的仓库地址,这个在网页上有展示

SERVER_IP是服务器的公网ip地址 SERVER_SSH_KEY是服务器的ssh密钥,这里需要注意,读取的是哪种格式的内容,之前我因为一直用rsa格式的,结果authorize_keys里的不是这种格式。

SSH_PORT ssh端口,默认22 SSH_USERNAME 默认root

后来尝试了一下宝塔

域名的申请、https、阿里云服务上申请免费的证书、nginx配置https证书 备案

之前我使用了一个github上的nextjs博客模板,搭配vercel来跑我的个人博客。

这次我想除了博客外,还应该包括一些而外的东西,比如自己灵光乍现的项目,生活等等。

还是沿用了next,同时我希望在国内的网络环境下也能正常访问,所以我打算使用国内的服务器备案后,自己跑nextjs项目

一开始没做好了解,以为nextjs项目和普通的react项目没有区别,打包后,拖个dist目录到服务器上就完事了。

然而发现不是。

这种服务端渲染的项目和之前的静态页面展示有很大的不同。

我需要在我的服务器上直接把整个项目文件上传上去,然后运行这个项目。相当于在服务器上不打包,直接跑项目。

如果配置了安全组,我能通过ip加服务的端口号3000来直接访问这个项目。

同时为了这个项目能够持续运行,还需要使用pm2来守护这个运行的进程。

之后是nginx的配置,需要修改成代理到localhost:3000里去

在域名备案还未成功时,遇到一些https直接访问无法访问的问题,当时还联系了阿里云的客服,让他们帮我解答疑惑。

之后能顺利访问了。

接下来便是解决最大的问题,每次更新代码后,需要在服务器上git pull最新代码,然后把服务停了重新跑这一系列人工操作问题。

我自然而然想到的是github actions,不过之前我可能会想到jenkins来部署,但是jenkins我个人感觉还是有点繁琐。

所以我选用了github actions + docker来部署我的nextjs项目。

修改next.config.js文件,我的项目里是next.config.mjs,把output设置为standalone,然后打包。

import createMDX from "@next/mdx";

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
  output: "standalone",
};

const withMDX = createMDX({
  extension: /\.mdx?$/,
});

export default withMDX(nextConfig);

在服务器上安装docker。 docker的一些命令。 比如build打包镜像,run运行容器等。

dockerfile内容,这里是直接参照官网给的示例,唯一的改动是把node的版本从18改成了我服务器上当前的node版本20

# syntax=docker.io/docker/dockerfile:1

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* .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


# 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

# 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/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

这里遇到一个问题是,当我使用docker build运行时,直接报错

=> ERROR [internal] load metadata for docker.io/library/node:20-alpin  30.2s
------
 > [internal] load metadata for docker.io/library/node:20-alpine:
------
Dockerfile:3
--------------------
   1 |     # syntax=docker.io/docker/dockerfile:1
   2 |     
   3 | >>> FROM node:20-alpine AS base
   4 |     
   5 |     # Install dependencies only when needed
--------------------
ERROR: failed to solve: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: failed to resolve source metadata for docker.io/library/node:20-alpine: failed to do request: Head "https://registry-1.docker.io/v2/library/node/manifests/20-alpine": dial tcp 108.160.166.148:443: i/o timeout

因为一开始我配置的docker镜像地址是阿里云提供的,发现根本用不了,只能改daemon.json里的registry-mirrors地址。

docker compose 管理nextjs容器和nginx容器。

services:
  nextjs:
    # build:
    #   context: .  # 使用当前目录作为构建上下文
    #   dockerfile: Dockerfile  # 指定 Dockerfile 路径
    image: ${ACR_FULL_ADDRESS}/website:${IMAGE_TAG}
    container_name: nextjs
    ports:
      - "3000:3000"
    restart: always

  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"  # 将宿主机的 80 端口映射到 Nginx 容器的 80 端口
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf  # 挂载 Nginx 配置文件
      - /usr/local/nginx/conf/cert:/etc/nginx/cert
    depends_on:
      - nextjs
    restart: always

首先需要在项目下建dockerfile和nginx.conf。 dockerfile的作用是用于打包一个nextjs项目镜像,然后把nextjs 在npm run build之后的一些文件映射到容器里去。

docker-compose里则是把我们nextjs项目里配置的nginx.conf映射到nginx容器的配置里去,同时把服务器上的https证书也映射进去。

github actions的流程

name: Build, Push, and Deploy

on:
  push:
    branches: [ main ]

jobs:
  build-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Login to Aliyun ACR
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.ACR_REGISTRY }}
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      - name: Build and Push Docker Images
        run: |
          export IMAGE_TAG=${{ github.sha }}
          export ACR_FULL_ADDRESS=${{ secrets.ACR_REGISTRY }}/${{ secrets.ACR_NAMESPACE }}

          # 只构建一次镜像
          docker build -t $ACR_FULL_ADDRESS/website:$IMAGE_TAG .
          docker push $ACR_FULL_ADDRESS/website:$IMAGE_TAG

  deploy:
    runs-on: ubuntu-latest
    needs: build-push
    steps:
      - name: SSH to ECS and Deploy
        uses: appleboy/ssh-action@v1.2.1
        with:
          host: ${{ secrets.SERVER_IP }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          debug: true
          script: |
            # 登录阿里云镜像仓库
            echo ${{ secrets.ACR_PASSWORD }} | docker login ${{ secrets.ACR_REGISTRY }} \
              -u ${{ secrets.ACR_USERNAME }} \
              --password-stdin

            # 拉取最新镜像并启动容器
            export IMAGE_TAG=${{ github.sha }}
            export ACR_FULL_ADDRESS=${{ secrets.ACR_REGISTRY }}/${{ secrets.ACR_NAMESPACE }}

            # 拉取新镜像
            docker pull $ACR_FULL_ADDRESS/website:$IMAGE_TAG

            # 同步仓库信代码
            cd /www/wwwroot/zzhwhd.com/zzhwhd
            git reset --hard origin/main
            git pull origin main
            git checkout main
            
            # 停止并删除旧容器
            docker compose -f /www/wwwroot/zzhwhd.com/zzhwhd/docker-compose.yml down

            # 清理旧镜像,只保留最新的一个
            docker images "$ACR_FULL_ADDRESS/website" -q | tail -n +2 | xargs -r docker rmi -f
            
            # 清理悬空镜像
            docker image prune -f

            # 启动新容器
            docker compose -f /www/wwwroot/zzhwhd.com/zzhwhd/docker-compose.yml up -d

阿里云docker镜像仓库 命名空间、项目名称

在代码提交到main分支时触发action

在github上配置secrets

首先是登录我的阿里云docker镜像仓库,把我github上最新代码docker build后推送到这个镜像仓库上。

然后我登录阿里云ecs服务器,拉取最新的镜像,同时删除旧的容器,准备跑我新的容器。

这里我遇到一个问题,就是明明我已经构建了最新的镜像,我看镜像上的tag也是我最新的提交。但是我访问页面时,看到的还是旧的内容。

起初我以为是nginx缓存的问题,给nginx.conf的访问上全都设置了不缓存,结果发现一点用的没有。

知道我在宝塔上,访问文件路径,点开我更改的文件内容时,发现仍旧是旧内容。此时我才发现端倪。

原来之前我的dockerfile一直配置有问题,每次打包都会基于dockerfile所在目录的文件进行重新构建。

即使我拉取了镜像仓库的最新镜像,然而我使用的nextjs镜像还是基于服务器上的旧代码重新构建的镜像。

所以我调整了dockerfile的配置以及deploy.yml的配置,同时把原来nginx的无缓存配置给还原了。

现在在登录到阿里云服务器是,我的deploy流程还有一步,是会去git pull拉取一遍代码,确保是最新的,因为比如我的docker compose运行是基于这个远程目录下的docker-compose.yml,所以我要确保它也是最新的, 免得我有时更新了这些配置,在运行的时候却还是用老配置。

目前我的网站已经完全自主可控啦,撒花!