引言
最近需要更新一台老机器的 PHP 版本,目标是从 PHP 7.4 到 PHP 8.2。说是老机器,也确实年代久远,系统甚至还是 Ubuntu 20.04。
当时的 LNMP 一整套是使用 Oneinstack 安装的,彼时还是一个非常不错的开源产品…近年发生了什么事大家也都知道了,也就是Oneinstack 投毒事件。
于是更新的话…自然不能用 Oneinstack 的升级脚本了。但由于是老系统,apt 安装多少也有些困难,因此就干脆决定用 Docker 部署吧。
Oneinstack 一整套安装的东西是高度客制化的,路径也是非标准的。比如 php 和 php-fpm 包括其配置文件在/usr/local/php
下,采用 unix socket 而非 TCP 来通信(nginx 中也是这样配置的)。以及包括一些扩展,比如 gd、redis、memcached 等等。使用 Docker 进行 PHP 部署的时候,也要将这些纳入考虑。
最终的效果应该是,只有 PHP 相关的东西在容器内运行,MySQL、Nginx 都保持在容器外不变。
Docker 相关
Dockerfile
先在命令行输入php -i
查看当前 php 环境,输出的是一大堆和环境有关的描述,相当于控制台模式下的 phpinfo。将这些内容全部复制给 AI,让它构建一个 PHP 8.2 的 Dockerfile。
由于 Oneinstack 安装的 php-fpm 和 nginx 都以 www 用户运行,所以也要把这一点信息告诉 AI。
可以先cat /etc/passwd
看一下 www 用户的 ID 号。
我使用的是 Gemini 2.5 Pro,在撰写本文时,算是第一梯队的模型了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
# 使用一个具体的 PHP 8.2 FPM 版本
FROM php:8.2.29-fpm
# 设置环境变量,避免 apt-get 在构建过程中进行交互式提问
ENV DEBIAN_FRONTEND=noninteractive
# 定义构建参数,用于设置用户ID和组ID,默认值为 1001
ARG PUID=1001
ARG PGID=1001
# 1. 安装系统依赖
# 添加了 imagick 和 memcached 所需的开发库
RUN apt-get update && apt-get install -y \
build-essential \
pkg-config \
autoconf \
gettext \
libcurl4-openssl-dev \
libfreetype-dev \
libicu-dev \
libjpeg62-turbo-dev \
# 新增:imagick 依赖
libmagickwand-dev \
# 新增:memcached 依赖
libmemcached-dev \
libonig-dev \
libpng-dev \
libsodium-dev \
libssl-dev \
libxml2-dev \
libxslt-dev \
libzip-dev \
# 新增:unzip 工具,常用于处理压缩包
unzip \
--no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 2. 配置并安装 PHP 扩展
# -j$(nproc) 利用所有 CPU 核心并行编译,加快构建速度
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) \
bcmath \
exif \
ftp \
gd \
gettext \
intl \
mbstring \
mysqli \
pcntl \
pdo_mysql \
shmop \
sockets \
soap \
sysvsem \
xsl \
zip \
sodium \
curl \
# xmlrpc 在 PHP 8.0+ 中已从核心移除,需要通过 PECL 安装
&& pecl install xmlrpc-beta \
&& docker-php-ext-enable xmlrpc \
# 显式启用 opcache (在 fpm 镜像中通常默认启用)
&& docker-php-ext-enable opcache
# 3. 安装缺失的 PECL 扩展 (imagick, redis, memcached)
RUN pecl install imagick redis memcached \
&& docker-php-ext-enable imagick redis memcached
# 6. 配置 FPM 用户和组
# 创建 'www' 用户和组,并修改 FPM 配置文件以使用它们
RUN groupadd -g ${PGID} www \
&& useradd -u ${PUID} -g www -s /sbin/nologin www \
&& sed -i 's/user = www-data/user = www/' /usr/local/etc/php-fpm.d/www.conf \
&& sed -i 's/group = www-data/group = www/' /usr/local/etc/php-fpm.d/www.conf
|
这里 AI 用的是官方的docker-php-ext-configure
帮助脚本来安装扩展。其实也可以用这个项目,这样就不用手动写 apt 指令和使用 pecl 安装了。不过这样也就相当于引入另一个开源项目了…
我们希望在容器外部管理 php 相关的配置文件,于是打包镜像之后,然后先随便启动一个容器,把容器内部/usr/local/etc/
底下的文件复制出来到容器外部,假设为~/my-php/etc
。这些文件包括了 php-fpm 的基础配置,以及安装扩展后自动生成的配置文件。
可以使用docker cp
命令来复制。
然后我们需要把 Oneinstack 的php.ini
文件同样拷贝到~/my-php/etc/php
底下。Oneinstack 的 php.ini 包括了一些常用的配置,比如调整 POST 方法可上传的文件大小、禁用一些不安全的函数等等。
最终的目录应该是长这样子的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
root@witch:~/my-php/etc# tree
.
├── pear.conf
├── php
│ ├── conf.d
│ │ ├── docker-fpm.ini
│ │ ├── docker-php-ext-bcmath.ini
│ │ ├── docker-php-ext-exif.ini
│ │ ├── docker-php-ext-ftp.ini
│ │ ├── docker-php-ext-gd.ini
│ │ ├── docker-php-ext-gettext.ini
│ │ ├── docker-php-ext-imagick.ini
│ │ ├── docker-php-ext-intl.ini
│ │ ├── docker-php-ext-memcached.ini
│ │ ├── docker-php-ext-mysqli.ini
│ │ ├── docker-php-ext-opcache.ini
│ │ ├── docker-php-ext-pcntl.ini
│ │ ├── docker-php-ext-pdo_mysql.ini
│ │ ├── docker-php-ext-redis.ini
│ │ ├── docker-php-ext-shmop.ini
│ │ ├── docker-php-ext-soap.ini
│ │ ├── docker-php-ext-sockets.ini
│ │ ├── docker-php-ext-sodium.ini
│ │ ├── docker-php-ext-sysvsem.ini
│ │ ├── docker-php-ext-xmlrpc.ini
│ │ ├── docker-php-ext-xsl.ini
│ │ └── docker-php-ext-zip.ini
│ ├── php.ini
│ ├── php.ini-development
│ └── php.ini-production
├── php-fpm.conf
├── php-fpm.conf.default
└── php-fpm.d
├── docker.conf
├── www.conf
├── www.conf.default
└── zz-docker.conf
3 directories, 32 files
|
docker-compose
接下来是 docker-compose 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
services:
php-fpm:
image: my-php
container_name: my-php-fpm-container
restart: always
# 卷挂载
volumes:
# 1. 挂载网站根目录
# 将宿主机的 /data/wwwroot 目录挂载到容器内相同的路径
- /data/wwwroot:/data/wwwroot
# 2. 挂载配置文件
# :ro 表示在容器内为只读,这是一个好习惯,防止容器意外修改配置
- ./etc:/usr/local/etc:ro
# 3. 挂载 sock 文件目录
# 将宿主机的 /var/run/php 目录挂载到容器内,用于共享 sock 文件
# 如果您选择使用 sock 文件方式连接,请使用此项
- /tmp:/tmp
- /usr/local/php/var/run/:/usr/local/php/var/run/
# 设置工作目录,方便执行 docker exec 命令时直接进入网站根目录
working_dir: /data/wwwroot
command:
- /usr/local/sbin/php-fpm
- --nodaemonize
|
其中/data/wwwroot
是网站目录。当 nginx 进行 fastcgi 调用的时候,会把 php 脚本的路径发送给 php-fpm。
挂载/tmp
主要是为了让容器内能够访问到/tmp/mysql.sock
,以便通过 unix socket 的方式访问数据库。顺便一提,如果在 php 的代码中,数据库地址写127.0.0.1
的话,走的是 TCP,而写localhost
的话走的就是 unix socket 了。
/usr/local/php/var/run/
是我们等下创建 php-fpm 的 sock 文件的地址。
PHP 和 Nginx 的配置修改
PHP
编辑etc/php-fpm.d/zz-docker.conf
文件:
1
2
3
4
5
6
7
|
root@witch:~/my-php# cat etc/php-fpm.d/zz-docker.conf
[global]
daemonize = no
[www]
listen = /usr/local/php/var/run/sock
listen.mode = 0666
|
将 listen 字段改成/usr/local/php/var/run/sock
,意思是启动时就在/usr/local/php/var/run/
下创建一个sock
作为名称的 unix socket 文件来监听连接。这样容器外的 nginx 就可以通过这个文件请求容器内部的 php-fpm 服务了。
需要注意区分一下容器的启动用户和 php-fpm 的运行用户。后者我们刚才已经在 Dockerfile 中改成 www 了,但前者还是 root。由于套接字文件本身是以容器的启动用户身份创建的,而外部 nginx 的运行用户是 www,所以 nginx 可能会出现没有执行权限,无法联通的情况。因此listen.mode
这一行是必要的。
顺带一提,php-fpm 在加载 php-fpm.d 下的文件时,是按照字母顺序加载的,后加载的文件中如果有相同的配置项,会覆盖掉前面的配置。所以我们编辑的是zz-docker.conf
,也就是按照字母顺序最后加载的这个文件。
Nginx
接着修改 nginx 的配置,/usr/local/nginx/conf/vhost/www.xxx.com.conf
,定位到相应的行:
1
2
|
# 原来的:fastcgi_pass unix:/dev/shm/php-cgi.sock;
fastcgi_pass unix:/usr/local/php/var/run/sock;
|
然后service nginx reload
。
这样应该就大功告成了,可以使用docker compose up -d
启动试试。
其他
如果之后需要为容器增加新的 php 扩展的话,需要重新随便启动一个容器,把这个目录底下的东西复制出来,覆盖掉现有的配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
root@witch:~/my-php/etc/php/conf.d# tree
.
├── docker-fpm.ini
├── docker-php-ext-bcmath.ini
├── docker-php-ext-exif.ini
├── docker-php-ext-ftp.ini
├── docker-php-ext-gd.ini
├── docker-php-ext-gettext.ini
├── docker-php-ext-imagick.ini
├── docker-php-ext-intl.ini
├── docker-php-ext-memcached.ini
├── docker-php-ext-mysqli.ini
├── docker-php-ext-opcache.ini
├── docker-php-ext-pcntl.ini
├── docker-php-ext-pdo_mysql.ini
├── docker-php-ext-redis.ini
├── docker-php-ext-shmop.ini
├── docker-php-ext-soap.ini
├── docker-php-ext-sockets.ini
├── docker-php-ext-sodium.ini
├── docker-php-ext-sysvsem.ini
├── docker-php-ext-xmlrpc.ini
├── docker-php-ext-xsl.ini
└── docker-php-ext-zip.ini
0 directories, 22 files
|
因为安装扩展的时候,会创建新的 ini 配置项。如果不复制出来的话,新扩展就无法加载了。