Featured image of post 迁移早先用 Oneinstack 安装的 PHP 到 Docker

迁移早先用 Oneinstack 安装的 PHP 到 Docker

记录一次将老旧服务器上由 OneinStack 安装的 PHP 7.4 环境,平滑迁移至 Docker 部署的 PHP 8.2 的完整过程。包括如何根据现有环境编写 Dockerfile、配置 docker-compose 文件,以及如何设置 Nginx 与容器内的 PHP-FPM。

引言

最近需要更新一台老机器的 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 配置项。如果不复制出来的话,新扩展就无法加载了。

Licensed under CC BY-NC-SA 4.0