PHP 驱动的自动化构建流:利用 GitHub Actions 与 Docker 镜像缓存实现秒级全栈代码发布

嘿,大家好!欢迎来到今天的“深夜加班拯救计划”研讨会。我是你们的特邀讲师,一个头发虽然还在但正在为了你的代码发际线而焦虑的资深全栈工程师。

今天咱们不聊虚的,咱们来聊一个让无数后端工程师在下班前痛哭流涕,让产品经理在产品评审会上暴跳如雷的话题:部署

想象一下这个场景:你刚刚把那个改了三个变量、删除了一个废弃函数的 main 分支推送到 GitHub。产品经理在群里大喊:“今晚就要上!那个按钮的颜色不对,用户看不到!”你淡定地点击了“Create Release”,然后看着进度条开始转动。

一秒……五秒……三十秒……一分钟……你的咖啡凉了,你的灵魂在飘忽,你的服务器还在加载那该死的初始内存。

在 2024 年,如果你的代码发布还要花五分钟,那你不是在写代码,你是在玩复古游戏。我们要追求的是秒级发布

要实现这个“魔幻现实主义”般的速度,我们需要引入两个重量级选手:GitHub Actions(你的自动打工仔)和 Docker 镜像缓存(你的冰箱)。

来,让我们把键盘敲响,开始这场关于速度的圣战。


第一部分:为什么你的构建慢得像只蜗牛?

在解决这个问题之前,我们得先搞清楚,为什么我们以前要把部署搞得像拆弹一样。

全栈应用通常是这样的:

  1. 后端:PHP 代码,依赖 composer.json
  2. 前端:Vue/React/Nuxt,依赖 package.json
  3. 数据库:MySQL,Postgres,Redis。

如果每次都重新下载依赖、重新编译前端、重新安装系统包,那简直是资源浪费的典范。Docker 的核心哲学就是“构建一次,到处运行”,但如果你每次构建都像是在白手起家,那 Docker 也就白学了。

我们需要做的,是让“构建”变得像从冰箱拿牛奶一样快。


第二部分:Dockerfile —— 别再瞎折腾了

首先,我们的 Dockerfile 得写得体面点。别再用那些几万行历史的 FROM ubuntu:16.04 加上 apt-get update && apt-get install 的笨办法了。那是 2015 年的遗物。

我们要用 Alpine 镜像,或者直接用带工具链的官方镜像。这能省下 80% 的构建时间。

错误的写法(狗屎代码):

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y php8.2 php8.2-fpm php8.2-mysql git
RUN apt-get install -y nodejs npm
# 然后是各种奇怪的路径设置...

正确的写法(优雅代码):
我们直接用 FROM composer:2FROM node:20-alpine。这是官方提供的预制蛋糕,自带奶油和装饰。你只需要在上面加你自己的糖霜(代码)。

# 后端构建
FROM composer:2 AS backend-builder
WORKDIR /app
# 把 composer.lock 和 composer.json 复制进来(为了利用缓存)
COPY composer.json composer.lock ./
# 安装依赖(这一步慢,所以必须缓存)
RUN composer install --no-scripts --no-autoloader --no-dev --optimize-autoloader

# 这里你可以做一些自定义的 PHP 扩展安装,或者直接用官方镜像
FROM php:8.3-fpm-alpine
WORKDIR /var/www/html
COPY --from=backend-builder /app/vendor ./vendor
COPY . .
# 复制 php.ini 配置
COPY php.ini /usr/local/etc/php/conf.d/custom.ini

看懂了吗?我们将构建过程分成了阶段。backend-builder 只是干活,干完活就扔掉,不用带着那些没用的工具留在最终镜像里。最终镜像只有 PHP 和你的代码,轻得像只鸟。


第三部分:GitHub Actions —— 自动化的外骨骼

好了,镜像准备好了。现在我们需要一个大脑来指挥它。

GitHub Actions 的 Workflow 文件(.github/workflows/deploy.yml)就是我们的指挥棒。我们要把它写得像一首诗,而不是一堆呻吟的 SQL 语句。

1. 基础架构:矩阵策略

你的项目可能有三个环境:Staging(测试)、Production(线上)、Beta。难道你要写三份一模一样的 YAML 吗?别傻了,用矩阵策略

name: CI/CD Pipeline

on:
  push:
    branches: [ main, release/* ]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [staging, production]
        php-version: ['8.3'] # 锁定版本,避免版本漂移导致的意外

    steps:
      # 1. 检出代码
      - name: Checkout Code
        uses: actions/checkout@v4

      # 2. 设置 Docker Buildx(这是多平台和缓存的神器)
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 3. 登录镜像仓库(Docker Hub 或 GitHub Container Registry)
      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

看到没?一条 matrix 就搞定了所有环境。这就是效率,这就是自动化。


第四部分:Docker 镜像缓存 —— 秒级发布的核心奥义

这是今天的重头戏。为什么说秒级?因为缓存。

当你修改了一个 CSS 文件,你真的需要重新编译整个 PHP 应用吗?不需要。当你修改了一个 PHP 函数,你需要重新下载 Node 依赖吗?不需要。

Docker 的缓存机制是这样的:它检查文件是否变了。如果没变,它就假装那行命令从来没执行过。

拒绝“全量复制”

很多新手会在 Dockerfile 里写 COPY . .。这很方便,但这也是性能杀手。一旦你改了哪怕一个字符,整层缓存就失效了。所有基于这层的后续命令(比如 composer install)都要重新跑一遍。

正确的策略:分层复制。

# 阶段 1:依赖(变动频率最低)
COPY composer.json composer.lock ./
RUN composer install --optimize-autoloader --no-dev

# 阶段 2:前端资源(变动频率中等)
COPY package*.json ./
RUN npm ci

# 阶段 3:代码(变动频率最高)
COPY . .

这保证了:只要你没动 composer.json,Composer 就不会重新下载 50MB 的依赖包。这能省下十几秒甚至几十秒。

GitHub Actions 里的本地缓存

现在,让我们把 Docker 层缓存和 GitHub Actions 的缓存结合起来。

GitHub Actions 提供了一个叫做 actions/cache 的工具。我们可以用它来缓存 node_modulesvendor 目录。这比 Docker 自身的缓存更智能,因为它不受 Dockerfile 文件结构的影响。

      # 4. 缓存 Composer 依赖
      - name: Get Composer Cache Directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer dependencies
        uses: actions/cache@v3
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      # 5. 缓存 NPM 依赖
      - name: Cache NPM dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

这套组合拳下去,基本上,如果你只是修改了 PHP 代码里的逻辑,你的构建时间会从 3 分钟直接杀到 10 秒以内。因为 composer install 直接从缓存里把以前下载好的文件塞回去,而 npm ci 也是同理。


第五部分:Docker Buildx 与远程缓存(进阶玩法)

如果你觉得本地缓存还不够快,或者你在跨机器运行 Actions(比如你在日本的服务器上构建,推送到美国的镜像仓库),你需要使用远程缓存

这就像是把冰箱里的牛奶放在云端。你可以在 Actions 里配置 Docker 上下文来使用 GitHub Actions 的缓存卷。

      # 6. 配置 Docker 构建上下文以使用远程缓存
      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ github.sha }}
          restore-keys: |
            ${{ github.ref }}-buildx-

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: my-org/my-app:latest
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

      # 这是一个骚操作:交换缓存,防止缓存一直存在导致磁盘溢出
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

这利用了 Docker 的层 ID 去重机制。如果缓存里已经有了这个层,它直接复用,连层 ID 都是复制的,快得惊人。


第六部分:PHP 8.3 的魔法与 OpCache

既然是 PHP 驱动,咱们得提一下 PHP 8.3。这个版本是 PHP 历史上最快的版本之一。

而且,别忘了 OpCache。在 Docker 镜像里,默认可能没有开启,或者配置得很保守。

在我们的 php.ini 里,加上这几行,让你的 PHP 代码在运行时比 Java 还快:

opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=10000
opcache.revalidate_freq=0  # 关键!设置为 0 意味着不校验文件修改时间,全在内存里跑
opcache.fast_shutdown=1

注意 opcache.revalidate_freq=0。这有一个风险:如果你更新了代码但没重启 PHP-FPM,用户可能还在跑旧代码。但在构建流中,我们是自动重启服务的,所以这是安全的,也是性能最优解。


第七部分:实战演练 —— 一个完整的 Workflow

好了,理论讲累了,咱们把所有的零件拼起来。这是一个可以直接扔进 GitHub 仓库的 .yml 文件。

这个 Workflow 会做以下事情:

  1. 下载代码。
  2. 缓存 Node 和 Composer。
  3. 构建前端(如果需要)。
  4. 构建后端 Docker 镜像。
  5. 推送到镜像仓库。
  6. 触发服务器拉取新镜像。
name: Super Fast Deployment Pipeline

on:
  push:
    branches:
      - main
      - staging

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # --- 缓存策略开始 ---
      - name: Get Composer Cache Directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache Composer
        uses: actions/cache@v3
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: Get NPM Cache
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      # --- 缓存策略结束 ---

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}

      # --- 构建镜像 ---
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
          # 构建参数,根据分支决定是否开启调试模式
          build-args: |
            ENVIRONMENT=${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to Server
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io
            docker pull ghcr.io/${{ github.repository }}:latest
            docker stop my-php-app
            docker rm my-php-app
            docker run -d -p 80:80 --name my-php-app ghcr.io/${{ github.repository }}:latest

看,这代码是不是很干净?没有冗余的 apt-get,没有漫长的等待。只要 git push,流水线就跑,跑完,停。


第八部分:常见坑与“玄学”优化

虽然我们有了 Docker 和缓存,但偶尔还是会遇到慢的时候。这里有几个“祖传秘籍”。

1. 忘记 .dockerignore
如果你在 .dockerignore 里忘了写 node_modules,Docker 会把本地所有的 node_modules 都复制进镜像。这会让镜像体积变大,构建时间变长。而且,如果本地文件没变,Docker 层缓存可能会失效。

2. Composer 版本漂移
确保 composer.json 里锁定了具体的版本,而不是 ^8.0。如果依赖版本在 CI 环境和开发环境不一致,缓存就失效了。

3. Node 版本问题
有时候 GitHub Actions 下载 Node 后,npm install 会报错。这是网络问题。我们前面加的 NPM 缓存就是为了解决这个问题。如果还报错,检查 package-lock.json 是否提交了。

4. 多平台构建的代价
如果你在 Dockerfile 里写 platforms: linux/amd64,linux/arm64,构建时间会翻倍。对于个人项目,只构建你服务器架构对应的平台(通常是 amd64 或 arm64)就够了。


结语:速度的艺术

各位听众,我们今天从痛苦的构建流程,一路杀到了秒级发布的巅峰。

我们要记住几个核心原则:

  1. 分层构建:不要把所有东西混在一起。
  2. 缓存为王:不要重复造轮子,也不要重复下载轮子。
  3. 轻量化:Alpine 镜像,官方工具链。

当你把部署时间从 3 分钟降到 15 秒时,你会发现一种奇妙的快感。当你改了一个 Bug,不用等咖啡,不用等进度条,直接看结果。这种实时反馈,是现代软件开发的快乐源泉。

这就是 PHP 驱动的自动化构建流。希望你们回去以后,把那些还在用 deploy.sh 手动上传的服务器都替换掉。拥抱 Docker,拥抱 Actions,拥抱速度。

我是你们的讲师,祝大家的代码都能跑得比光还快,祝大家的头发……嗯,那个保重吧。下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注