欢迎来到“PHP 构建流”特训营:从源码到 Docker 镜像的奇幻漂流
各位未来的 Docker 架构师、PHP 大师、以及那些还没学会 <?php echo "Hello World"; ?> 的初学者们,大家晚上好。
我是你们今天的讲师。今晚,我们不聊什么“怎么用 Composer 安装 Laravel”,也不聊“为什么那个 array_push 比直接 [] = 慢了 0.0001 毫秒”。今晚,我们要玩点大的。我们要像造火箭一样造 PHP。
想象一下,你手头有一个 PHP 的 GitHub 仓库,里面全是源码。你不想等官方给你编译好那个傻乎乎的官方镜像,你想要:
- 定制化:我要带这个功能,不要那个功能。
- 极简:我要一个只有 20MB 的 PHP,里面只有
curl和openssl。 - 速度:我要 CI/CD 自动化,只要我
git push,Docker 镜像就自动推送到 Docker Hub。
这听起来很酷,对吧?就像科幻电影里的传送门。但实际上,这就是把 PHP 的编译器和 Docker 这两个大块头喂到一起,然后看着它们打架、产生火花,最后吐出一个完美的镜像。
别紧张,跟着我的节奏,今晚我们要完成从“源码下载”到“Docker Hub 上线”的整个闭环。
第一讲:背景故事——为什么要从源码构建?
在开始写 YAML 文件之前,我们先聊聊哲学。
我们为什么要折腾源码?因为默认的官方 PHP 镜像太“臃肿”了。它像个穿了十层棉袄的胖子,里面塞满了你根本用不到的扩展。而且,默认的配置往往不是你喜欢的。你想用 ZTS(线程安全版)来跑高并发,或者你想把 OPcache 的参数调到极致。
从源码构建,就像是去餐厅点菜。你告诉厨师:“我要全麦面包,不要香菜,酱汁多点芝士!”而官方镜像呢?就像是一个预制菜,虽然热乎,但你不知道放了什么料。
在这个讲座里,我们要搭建的流水线,就是一个全自动的米其林大厨。它每天 24 小时盯着你的代码仓库,一旦你提交了修改,它就立刻去后厨干活。
第二讲:GitHub Actions 的蓝图——流水线的骨架
首先,我们要跟 GitHub Actions 打个招呼。GitHub Actions 是 CI/CD 界的“超市收银员”,你扔给它什么(代码),它就吐出什么(构建结果)。
我们的 YAML 文件就是它的操作手册。假设你已经有一个 PHP 源码仓库了,我们要写一个 .github/workflows/build-php.yml。
让我们先搭建一个最基础的骨架,看看它的长相:
name: Build and Push Custom PHP Docker Image
# 触发器:当有人推送到 main 分支,或者创建 Pull Request 时,触发工作流
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# 工作流环境
jobs:
build-php-image:
runs-on: ubuntu-latest
steps:
# 第一步:克隆代码
- name: Checkout code
uses: actions/checkout@v4
# 第二步:安装构建依赖
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y build-essential re2c libxml2-dev libsqlite3-dev
# ... 后面才是重头戏 ...
看,这就是骨架。runs-on: ubuntu-latest 告诉 GitHub,我们想在一台最新的 Ubuntu 机器上干活。steps 就像是一串指令,执行完一个就执行下一个。
第三讲:繁重的编译——让 PHP 活过来
这是最痛苦也最有趣的一步。下载源码只是第一步,真正的魔法发生在 ./configure 和 make 之间。
我们需要一个步骤来下载 PHP 源码。为了简单起见,我们直接从官网下载最新的 8.2 版本(或者你喜欢的版本)。
- name: Download PHP Source
run: |
# 获取最新的 PHP 版本号(这里为了演示,我们硬编码一个版本,实际可以用脚本获取)
VERSION=8.2.12
wget https://www.php.net/distributions/php-$VERSION.tar.gz
tar -xzf php-$VERSION.tar.gz
cd php-$VERSION
好,现在我们在 /home/runner/work/your-repo/php-8.2.12 目录下。接下来是重头戏:配置。
./configure 是一个神奇的脚本,它会检查你的系统有没有 gcc,有没有 libxml2。如果缺胳膊少腿,它就会报错。如果你一切俱全,它就会生成一个 Makefile。这就好比装修队进场前,必须先规划好水电线路。
- name: Configure PHP
run: |
cd php-$VERSION
# 这是一个复杂的命令,我们拆开看:
# --prefix=/usr/local/php : 安装路径,我们想把 PHP 安装到一个自定义的目录
# --disable-all : 默认禁用所有扩展,保持极简
# --enable-fpm : 开启 PHP-FPM,这是 PHP 运行的核心组件
# --with-curl : 加上 cURL 支持,不然连个网页都爬不了
# --with-openssl : 加上 SSL 支持,HTTPS 必须的
# --with-zlib : 加上压缩支持
# --enable-mbstring : 多字节字符串支持,处理中文必备
./configure
--prefix=/usr/local/php
--disable-all
--enable-fpm
--with-curl
--with-openssl
--with-zlib
--enable-mbstring
执行完这一步,屏幕上可能会打印出成千上万行“Checking for…”和“checking … yes”的消息。别怕,那是它在挑老婆/老公呢,仔细挑一挑没坏处。如果看到 Congratulations!,那就说明配好了。
接下来,就是漫长的等待。make 命令会把源码编译成二进制文件。
- name: Compile PHP
run: |
cd php-$VERSION
# 编译,-j4 表示并行编译,利用多核 CPU 加速
make -j$(nproc)
想象一下,你的 CPU 风扇开始狂转,像飞机起飞一样。几分钟后,编译完成。此时,你的 PHP 二进制文件(sapi/fpm/php-fpm)就诞生了。
第四讲:Docker 化——打包你的杰作
编译出二进制文件只是第一步,现在我们要把它塞进 Docker 镜像里。这时候,我们需要一个 Dockerfile。
但是,我们有一个问题:我们在 GitHub Actions 的容器里编译 PHP,我们需要一个干净的“地基”。我们可以选择在一个 Ubuntu 基础镜像里复制我们刚才编译好的 PHP,但更好的办法是使用多阶段构建。
让我们来写这个 Dockerfile(注意:这只是一个简化版,专注于演示流程):
# 第一阶段:构建阶段
FROM ubuntu:22.04 AS builder
# 设置工作目录
WORKDIR /usr/src/php
# 安装编译依赖
RUN apt-get update && apt-get install -y
build-essential
re2c
libxml2-dev
libsqlite3-dev
&& rm -rf /var/lib/apt/lists/*
# 这里我们需要把 GitHub Actions 里下载的 php-8.2.12.tar.gz 复制过来
# 在实际 CI 中,我们会把 tar.gz 复制到容器里并解压
COPY php-8.2.12.tar.gz /usr/src/php/
# 同样的配置和编译步骤
RUN tar xzf php-8.2.12.tar.gz && cd php-8.2.12 &&
./configure --prefix=/usr/local/php --disable-all --enable-fpm --with-curl --with-openssl --with-zlib --enable-mbstring &&
make -j$(nproc) &&
make install
# 第二阶段:运行阶段
FROM ubuntu:22.04
# 安装运行时依赖(不需要编译工具了,只需要运行时库)
RUN apt-get update && apt-get install -y
libsqlite3-0
&& rm -rf /var/lib/apt/lists/*
# 从构建阶段复制编译好的 PHP 到运行阶段
COPY --from=builder /usr/local/php /usr/local/php
# 设置环境变量,让 PHP 能找到自己
ENV PATH="/usr/local/php/bin:${PATH}"
ENV LD_LIBRARY_PATH="/usr/local/php/lib"
# 复制 php-fpm 的配置文件(简单起见,我们使用默认配置,或者你可以复制你的自定义配置)
COPY php-$VERSION/sapi/fpm/php-fpm.conf /usr/local/php/etc/php-fpm.conf
COPY php-$VERSION/sapi/fpm/www.conf /usr/local/php/etc/php-fpm.d/www.conf
# 启动命令
CMD ["/usr/local/php/sbin/php-fpm", "-F", "-R"]
这段代码的意思是:
- 先建一个 Ubuntu 蓝图。
- 在蓝图里大干一场(编译 PHP)。
- 把编译好的成品切下来,放进一个干净的新蓝图里。
- 启动成品。
这就是 Docker 的精髓:关注点分离。编译环境脏乱差,运行环境干干净净。
第五讲:分发的艺术——推送到云端
现在,你的 Docker 镜像已经准备好了,但它还躺在你的 GitHub Actions 的虚拟硬盘里。怎么让它变成全球开发者都能用的 yourname/php-custom 呢?
我们需要告诉 Docker,我们要把它推送到 Docker Hub。
首先,我们需要在 GitHub 仓库里设置 Secrets。进入 Settings -> Secrets and variables -> Actions。添加一个名为 DOCKER_USERNAME,和一个名为 DOCKER_PASSWORD 的密钥。你的 Docker Hub 用户名和密码(或者 Access Token)就在这里。
现在,回到我们的 YAML 文件,增加最后一步:
- name: Build and push Docker image
# 使用 Docker 官方镜像构建和推送操作
uses: docker/build-push-action@v5
with:
# 目标镜像名称
context: .
# 标签:latest 表示最新版本,8.2 表示版本号
tags: your-dockerhub-username/php-custom:latest, your-dockerhub-username/php-custom:8.2
# 触发推送的条件(build 之后就 push)
push: true
等等! 还有个问题。我们怎么登录 Docker Hub?通常我们需要先 docker login。
所以,我们要在 Build and push Docker image 这个步骤之前,加一个登录步骤:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
第六讲:性能与优化——别让用户等死
现在的流程已经能跑通了。但是,作为一个“资深编程专家”,我不能只给你一个能跑的,我要给你一个快的。
如果每次构建都要重新下载 PHP 源码,重新 apt-get,那时间会慢到让你想摔键盘。我们需要缓存。
1. 缓存依赖
在安装 build-essential 之前,我们通常先缓存 apt 的缓存目录。这能省去大量下载时间。
- name: Cache apt packages
uses: actions/cache@v3
with:
path: /var/lib/apt/lists
key: ${{ runner.os }}-apt-${{ hashFiles('**/apt-cache.txt') }}
restore-keys: |
${{ runner.os }}-apt-
2. 缓存 Composer 依赖(如果有的话)
如果你的 PHP 仓库里有 composer.json,你需要安装 Composer 并缓存 vendor 目录。
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
3. 缓存 PHP 源码
这是重头戏。如果我们下载了 php-8.2.12.tar.gz,下次构建时,直接用压缩包解压肯定比重新去官网下要快得多。
- name: Cache PHP source
uses: actions/cache@v3
with:
path: php-8.2.12.tar.gz
key: ${{ runner.os }}-php-source-8.2.12
restore-keys: |
${{ runner.os }}-php-source-
第七讲:实战演练——故障排查指南
理论讲完了,现实总是残酷的。你的流水线可能会挂掉,就像你早起出门忘记关窗户一样常见。我们来聊聊几种最常见的“丧事”。
情景一:configure: error: xml2-config not found
症状:编译时报错,找不到 XML 库。
原因:你可能漏装了 libxml2-dev。
解法:在 apt-get install 里加上 libxml2-dev。别忘了,有时候 re2c 也很调皮,有时候编译器版本太新也会有兼容性问题。
情景二:php-fpm 启动失败
症状:镜像构建成功,但运行时容器直接退出了。
原因:PHP-FPM 配置文件里的 user 或 group 不存在,或者端口被占用。
解法:
- 在 Dockerfile 里,确保创建了一个非 root 的用户(安全最佳实践)。
- 检查
www.conf里的listen指令,确保端口是 9000 或者你在 Docker 中正确映射了端口。
情景三:权限问题
症状:文件没有写入权限。
原因:在 GitHub Actions 中,make install 通常以 root 权限运行,这没问题,但在生产环境中,我们不希望容器以 root 运行。
解法:
- 安装后,运行
chown -R www-data:www-data /usr/local/php。 - 或者修改 Dockerfile,在运行阶段使用
USER www-data。
第八讲:进阶玩法——自动化版本管理
每次手动在 YAML 里写版本号很累,而且容易写错。我们可以写个脚本自动抓取最新的 PHP 版本号。
写个脚本 get-latest-php-version.sh:
#!/bin/bash
# 从 php.net 的 XML 镜像中抓取最新版本号
curl -s https://www.php.net/releases/index.php?json | jq -r '.8.2.version'
然后在 GitHub Actions 里运行它:
- name: Get latest PHP version
id: version
run: echo "version=$(bash get-latest-php-version.sh)" >> $GITHUB_OUTPUT
- name: Download PHP Source
run: |
VERSION=${{ steps.version.outputs.version }}
wget https://www.php.net/distributions/php-$VERSION.tar.gz
这样一来,你只需要维护 8.2,如果 8.3 发布了,你只需要把脚本里的 8.2 改成 8.3,剩下的工作流水线会帮你自动搞定。
结束语:自动化之美
好了,伙计们。今天我们并没有做什么惊天动地的大事,我们只是教会了 GitHub Actions 如何把一堆文本文件(源码)变成一个可执行程序(二进制),再把它塞进一个便携箱(Docker 镜像)里,最后送到世界的各个角落。
这就是自动化构建流的魅力。它消除了“我改了代码,能不能跑?”的焦虑。
当你看到 GitHub Actions 的绿色对勾亮起,看到 Docker 镜像出现在你的仓库页面上,那种感觉,就像是你亲手种了一棵树,然后看着它结出了果实。
现在,去试试吧。克隆你的仓库,写个 git commit,写个 git push。然后,去喝杯咖啡,看着你的自动化工厂开工。这大概就是程序员的快乐源泉。
如果有任何问题——别问我,去查文档,或者问我,我会告诉你去查文档。再见!