记录一下 blog 重新部署的过程,从 pm2 踩坑到改用 Docker 镜像,再接上 GitHub Actions 自动发布。

2026-06-06 00:30:26
之前那台服务器换了手机号,我也把续费这件事忘了,结果服务器直接到期,数据全丢了,只能重新部署一遍。我对不起各位 !!!

最开始我用的是 pm2,图的就是简单省事。本地 build 完之后,把 .outputscp 丢到服务器,再执行一次 pm2 restart

结果遇到了: nuxt 刷新直接 404

查了一圈之后,基本可以确定问题还是出在平台差异上。本地正常,服务器异常;再看输出目录里还带着 node_modules ,就更容易怀疑到环境差异了。毕竟本地和服务器环境不一样,依赖细节也会有差别。

到这里就很自然了,这不就是 docker 最擅长解决的事情吗。 所以直接把应用打成镜像,统一按镜像部署,避免环境差异。

不过想想每次都要:

  1. 本地构建镜像
  2. 手动推镜像
  3. 登录服务器重启服务

那其实还是重复劳动。

# 1.
docker build --platform linux/amd64 -t blog:test .
# 2.
docker tag blog:test ACR_REGISTRY/ACR_NAMESPACE/blog:latest 
# 3.
docker push ACR_REGISTRY/ACR_NAMESPACE/blog:latest

所以顺手把这套流程也一起处理吧,直接接 CI/CD

整个流程其实就三步:

  1. master 分支有更新时,自动构建镜像
  2. 把构建好的镜像推到阿里云 ACR
  3. 通过 SSH 登录服务器,在现有的 docker-compose 目录里重启 blog 服务

用 GitHub Actions 自动构建

分支一更新就触发

GitHub Actions 处理

on:
  # 推送到 对应分支 时自动部署。
  push:
    branches:
      - master
  # 允许在 GitHub Actions 页面手动触发
  workflow_dispatch:

镜像推到阿里云 ACR

镜像仓库我放在阿里云 ACR 私仓里,所以首先将仓库地址和镜像名配好。

env:
  # 私有镜像仓库地址
  REGISTRY: ${{ secrets.ACR_REGISTRY }}
  # 镜像名
  IMAGE_NAME: ${{ secrets.ACR_NAMESPACE }}/blog

Action 里具体做什么

触发条件和镜像地址准备好之后,就可以进行: 拉代码、登录仓库、构建镜像、推送镜像。

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      # 拉取仓库代码
      - name: Checkout
        uses: actions/checkout@v6

      #  Docker Buildx,构建多平台镜像。
      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 登录阿里云 ACR
      - name: Login To Aliyun ACR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      # # 生成镜像元数据和 tag。
      # - latest: 服务器部署时使用
      # - 提交的 SHA: 方便进行版本回溯
      - name: Extract Docker Metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=latest
            type=raw,value=${{ github.sha }}

      # 基于当前镜像,并推送到 ACR。
      - name: Build And Push Image
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          # 使用 GitHub Actions 缓存加速重复构建。
          cache-from: type=gha
          cache-to: type=gha,mode=max

服务器上更新服务

镜像推上去之后,最后一步就是连到服务器,把服务更新起来。

- name: Pull And Rebuild On Server
  uses: appleboy/ssh-action@v1.2.2
  env:
    ACR_REGISTRY: ${{ secrets.ACR_REGISTRY }}
    ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
    ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
    # 服务器上存在 docker-compose.yml 的目录
    DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
  with:
    host: ${{ secrets.SSH_HOST }}
    username: ${{ secrets.SSH_USER }}
    password: ${{ secrets.SSH_PWD }}
    envs: ACR_REGISTRY,ACR_USERNAME,ACR_PASSWORD,DEPLOY_PATH
    script: |
      # `-e` 遇错退出,`-u` 遇到未定义变量直接报错,避免静默失败。
      set -eu

      cd "$DEPLOY_PATH"
      # 拉新的镜像
      docker compose pull blog
      # 重启
      docker compose up -d --force-recreate blog
      # 删除空的镜像
      docker image prune -f

额外踩坑记录

有些镜像因为网络原因,服务器上没法直接从 Docker Hub 拉。

我选择的是: 服务器拉不下来,那我本地拉好再传上去。

  1. 拉取 amd64 镜像
      docker pull --platform linux/amd64 artalk/artalk-go:latest
    
  2. 导出镜像
    docker save -o artalk-go-amd64.tar artalk/artalk-go:latest
    
  3. 上传到服务器
    scp artalk-go-amd64.tar name@ip:path
    
  4. 服务器上导入
    docker load -i path/artalk-go-amd64.tar 
    docker manifest inspect artalk/artalk-go:latest # 查看当前架构支持哪些镜像
    docker inspect artalk/artalk-go:latest --format='{{.Architecture}}'  # 验证镜像架构
    docker pull artalk/artalk-go@sha256:34c9287650c6a662fb1bba7435b6a02f992f96aa0d2a7fa2bac4e5bfe4fa4f97 # 拉取对应的镜像
    
  5. 只要镜像已经落到服务器上,后面怎么配就都简单了