前端自动部署模式对比

前端自动部署的几种方式

维护项目的一大痛点就是希望推送代码后自动触发更新部署,对此可以采用 GitHub Actions 设置自动部署的工作流(Workflow),对于前端来说有三种主流做法:

直传文件服务器构建推送仓库拉取
构建位置CI服务器CI
传输内容静态产物完整源码镜像
服务器需装Web 服务Docker + 构建工具仅 Docker
多服务器部署逐个上传逐个构建各自拉取同一镜像
回滚方式切备份目录切旧镜像拉指定 tag 镜像
主要局限只能静态文件服务器负担重、多台不一致需维护镜像仓库

三种模式遵循同一个核心逻辑:构建 → 替换线上 → 出问题可回退。区别只在于构建在哪发生、中间产物是什么。


模式一:CI 构建 + 直传文件

思路

这是最直接的方式。在 CI 里装依赖、跑构建,把产出的静态文件传到服务器站点目录,用新旧目录互换上线。但它的前提是:构建产物是静态文件,丢到 Web 服务器就能跑。如果项目需要以容器方式运行,光传文件没用,得走 Docker 构建。

流程

推送代码 → CI 构建 → SCP 传文件到服务器 → 备份旧目录 → 替换为新文件 → 刷新服务

示例

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          source: 'dist/*'
          target: '/www/site/dist_new/'
          strip_components: 1

      - uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          script: |
            cd /www/site
            BACKUP="backup_$(date +%Y%m%d_%H%M%S)"
            mkdir -p "$BACKUP"
            find . -maxdepth 1 -mindepth 1 ! -name 'dist_new' ! -name 'backup_*' \
              -exec mv {} "$BACKUP"/ \;
            cp -r dist_new/* . && rm -rf dist_new
            ls -dt backup_* | tail -n +6 | xargs -r rm -rf
            nginx -s reload

模式二:传源码 + 服务器构建

思路

模式一只能传最终的静态产物,碰上需要完整源码环境才能构建的项目(比如容器化部署)就行不通。模式二把整个仓库源码传到服务器,在服务器上现场构建、打包成镜像再启动。但这种方式的问题是:服务器必须装上全套构建工具,而且如果有好几台服务器,每台都得各自构建一遍,费时且结果可能不一致。

流程

推送代码 → SCP 传源码 → 服务器上构建镜像 → 停旧容器 → 启新容器

示例

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          source: '.'
          target: '/opt/project/build/'
          strip_components: 1

      - uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          script: |
            cd /opt/project/build
            docker build -t my-app:latest .
            docker stop my-app 2>/dev/null || true
            docker rm my-app 2>/dev/null || true
            docker run -d --name my-app -p 3000:80 --restart=always my-app:latest
            docker image prune -f

模式三:CI 构建镜像 + 推送仓库 + 服务器拉取

思路

模式二让服务器承担了构建任务,服务器多了就成问题。模式三把构建和部署彻底分开:CI 里构建好镜像,推到镜像仓库;服务器不再参与构建,只需要拉取镜像、替换容器。这样每台服务器拿到的都是同一个镜像,构建环境统一在 CI 里,服务器只管运行。

可以考虑 GitHub Container Registry 托管镜像,其与 Actions 无缝集成,登录直接用 GITHUB_TOKEN 即可。

流程

推送代码 → CI 构建镜像 → 推送到镜像仓库 → 服务器拉取镜像 → 停旧容器 → 启新容器

示例

name: Deploy

on:
  push:
    branches: [main]

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

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PRIVATE_KEY }}
          script: |
            docker pull ghcr.io/${{ github.repository }}:latest
            docker stop my-app 2>/dev/null || true
            docker rm my-app 2>/dev/null || true
            docker run -d \
              --name my-app \
              -p 3000:80 \
              --restart=always \
              ghcr.io/${{ github.repository }}:latest
            docker image prune -f