# 基于docker的phpfpm+nginx环境

## 简介

本文以php7.4为例，介绍在具有docker环境的系统上，不安装任何php相关服务，完全利用docker容器来实现运行环境的搭建，最后还会以ThinkPHP6.0为例演示真实项目的运行，本文中的环境可在生产环境使用。

## 准备工作

在本地创建一个工作目录，笔者的宿主机上它的位置就是`/app/dev/phpfpm-demo`。

```sh
mkdir -p /app/dev/phpfpm-demo
cd /app/dev/phpfpm-demo
```

## 制作过程

### 基础镜像

```sh
docker pull nginx:1.24
docker pull php:7.4.33-fpm-bullseye
```

其中，nginx镜像不需要做更改可以直接使用，而php镜像则要在基础镜像的基础上安装一些必要的依赖和扩展。

### 制作PHP镜像

#### 为PHP安装扩展

```sh
docker run --rm --name php-fpm -it -d php:7.4.33-fpm-bullseye
docker exec -it php-fpm
```

在容器内安装依赖前最好先更换一下软件源，否则可能会很慢。

```sh
cp /etc/apt/sources.list /etc/apt/sources.list.bak && echo "deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb http://mirrors.aliyun.com/debian-security/ bullseye-security main
deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main
deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib" | tee /etc/apt/sources.list >/dev/null
```

基础php镜像中没有`redis`、`mysql`等扩展，我们要自己安装一下。在容器内可以随时用`php -m`命令查看当前已经生效的扩展

```sh
apt update && apt install -y \
# gd扩展依赖
libzip-dev libpng-dev \
ibwebp-dev libjpeg-dev \
libfreetype6-dev \
# pgsql扩展依赖
libpq-dev \
# gmp扩展依赖
libgmp-dev \
# composer需要
git

# redis扩展
pecl install redis && docker-php-ext-enable redis
# mysql扩展
docker-php-ext-install pdo_mysql && docker-php-ext-enable pdo_mysql
# opcache扩展
docker-php-ext-install opcache && docker-php-ext-enable opcache
# zip扩展
docker-php-ext-install zip && docker-php-ext-enable zip
# xlswriter扩展（超高速读写excel文件）
pecl install xlswriter && docker-php-ext-enable xlswriter
# pgsql扩展（连接postgres数据库）
docker-php-ext-install pdo_pgsql && docker-php-ext-enable pdo_pgsql
# gmp扩展
docker-php-ext-install gmp && docker-php-ext-enable gmp
# gd扩展（直接使用docker-php-ext-install gd虽然也能安装成功，但是不支持freetype的话tp的验证码会出错）
docker-php-source extract
cd /usr/src/php/ext/gd
docker-php-ext-configure gd --with-webp=/usr/include/webp --with-jpeg=/usr/include --with-freetype=/usr/include/freetype2
make && make install
docker-php-ext-install gd && docker-php-ext-enable gd
# 安装composer
php -r "copy('https://install.phpcomposer.com/installer', 'composer-setup.php');" && php composer-setup.php && php -r "unlink('composer-setup.php');" && mv composer.phar /usr/local/bin/composer
```

以上是笔者经历过的项目中比较常用的扩展，如果还有其他需要，可以按照类似的方式安装。

最后一切完毕后就可以离开容器了：

```sh
exit
```

另外，我们需要知道几个关键路径：

- php.ini文件所有路径：`/usr/local/etc/php/php.ini`
- 应用路径：`/var/www/html`

这两个路径我们未来要采用映射的方式从项目目录映射到容器内，因此不需要在容器内做什么动作了。

如果在自己的宿主机中没有现成的`php.ini`文件，可以从容器内复制出来一份：

```sh
# 开发环境建议
docker cp php-fpm:/usr/local/etc/php/php.ini-development ./php.ini
# 生产环境建议
docker cp php-fpm:/usr/local/etc/php/php.ini-production ./php.ini
```

#### 保存PHP镜像

```sh
docker commit php-fpm php-fpm:7.4
```

以后我们要使用的就是这个新创建的镜像，如果有`hub.docker.com`网站的账号，可以使用`docker push`命令把这个镜像上传到自己的docker hub上，方便以后在其他地方使用。

现在我们刚才启动的镜像已经不再需要了，直接删除就行。

```sh
# 启动时已经使用了--rm参数，所以容器被关闭就会自动删除
docker stop php-fpm
```

### 准备nginx配置文件

我们直接启动nginx容器，然后从容器是把配置文件复制到宿主机上。

```sh
# 当前目录是宿主机上的工作目录
mkdir nginx
docker run --rm --name php-fpm-nginx -it -d nginx:1.24
docker cp php-fpm-nginx:/etc/nginx/nginx.conf ./nginx/
docker cp php-fpm-nginx:/etc/nginx/conf.d ./nginx/
dorker stop php-fpm-nginx
```

现在我们已经准备好了`nginx:1.24`镜像和`php-fpm:7.4`镜像，已经具备工作条件，下面就开始使用。

## 环境与配置

到目前为止，我们已经有两个镜像：`nginx:1.24`和`php-fpm:7.4`，同时工作目录`/app/dev/phpfpm-demo`中的目录结构如下：

|目录或文件|说明|
|---|---|
|php.ini|PHP环境的配置文件|
|nginx|Nginx的配置文件目录|
|nginx/nginx.conf|nginx的主配置文件|
|nginx/conf.d|nginx下的站点配置目录|

现在我们已经把环境准备好了，接下来准备一个可以一键启动的`docker-compose.yml`文件，再修改一些nginx的配置，就可以了。

`docker-compose.yml`文件：

```yml
version: "3.1"

services:
  php:
    image: php-fpm:7.4
    container_name: php74
    restart: always
    volumes:
      - ./php.ini:/usr/local/etc/php/php.ini
      - ./:/var/www/html
    stdin_open: true
  nginx:
    image: nginx:1.24
    container_name: phpnginx
    restart: always
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./:/usr/share/nginx/html
    stdin_open: true
    ports:
      - 8008:80
    depends_on:
      - php
```

简单解读一下：

- 把当前工作目录中的`php.ini`文件映射到php容器对应的路径上。
- 把当前上作目录映射到php容器中的默认站点根目录上。
- 把当前工作目录中的nginx配置文件映射到nginx容器对应的路径上。
- 把当前工作目录映射到nginx容器中的默认站点根目录上。
- 把nginx容器的80端口映射到宿主机上的8008端口上（可以自己改）。
- nginx容器和php容器都不使用host网络而是自己组成一个私有网络。

接下来我们去修改nginx的配置文件，只需要修改`nginx/conf.d/default.conf`这一个文件即可，在里面加入下面这一段：

在`location /`段中添加`index.php`：

```
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm index.php;
    }
```

然后再添加下面一段：

```
    location ~ \.php$ {
       fastcgi_pass   phpfpm74:9000;
       fastcgi_index  index.php;
       fastcgi_param  SCRIPT_FILENAME  /var/www/html/$fastcgi_script_name;
       include        fastcgi_params;
    }
```

接下来，我们在站点根目录上创建一个`index.html`文件：

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>欢迎</title>
</head>
<body>
    <h1>Hello, web</h1>
</body>
</html>
```

再创建一个`phpinfo.php`文件：

```php
<?php
phpinfo();
```

接下来，在工作目录上，直接运行下面的命令：

```sh
docker compose up -d
```

看到容器启动成功。

- 用浏览器访问`http://ip:8008`应该可以看到我们刚才创建的`index.html`中的内容。
- 用浏览器访问`http://ip:8008/phpinfo.php`应该可以看到我们刚才创建的`phpinfo.php`中的内容，也就是php服务的参数页。

现在一个基于docker的nginx+php-fpm环境已经制作好了，任何支持docker的环境都可以直接使用。

## 高级应用

### 在宿主机使用composer命令

目前假设我们的宿主机没有php环境，那么如果我们执行`composer create-project topthink/think:6.0.* tp`时，那自然会得到`composer: command not found`的结果，这不是我们想要的结果，解决办法也很简单——做个直接进入容器执行composer命令的脚本就行了。

> 下面的操作只适用于linux系统，如果是windows系统的话可以按照这个思路做`composer.bat`批处理文件。

```sh
touch composer
```

编辑`composer`脚本内容如下：

```shell
#!/bin/bash

docker compose run --rm webman-jsonrpc composer $@
```

给文件添加执行权限：

```sh
chmod +x composer
```

现在就可以使用这个`composer`脚本了：

```sh
./composer create-project topthink/think:6.0.* think
```

网络正常的情况下，我们应该会看到工作目录下已经出现的`tp`文件夹，打开就会看到完整的thinkphp项目目录。

### 开发ThinkPHP项目

下面我们整理一下工作目录结构：

```sh
mv tp/* .
mv tp/.* .
rm -rf tp
```

接下来，我们再修改一下nginx配置文件来支持thinkphp的伪静态，再把nginx站点目录和php站点目录指向到工作路径的pulic目录下。

编辑`nginx/conf.d/default.conf`文件如下：

```
    location / {
        root   /usr/share/nginx/html/public/;
        index  index.html index.htm index.php;
        try_files   $uri    $uri/   /index.html last;
        if (!-e $request_filename){
            rewrite  ^(.*)$  /index.php?s=$1  last;   break;
        }
    }

    location ~* (runtime|app)/{
        return 403;
    }

    location ~ \.php$ {
       fastcgi_pass   phpfpm74:9000;
       fastcgi_index  index.php;
       fastcgi_param  SCRIPT_FILENAME  /var/www/html/public/$fastcgi_script_name;
       include        fastcgi_params;
    }
```

现在，再次在工作目录中用docker compose启动容器就可以启动这个tp项目了：

```sh
docker compose up -d
```

再次在浏览器中打开`http://ip:8008`，可以看到tp的欢迎页面了。

最后别忘了给`runtime`目录修改一个所有者，否则项目运行还是会出问题：

```sh
chown www-data:www-data -R runtime
```

### 在宿主机使用php命令

和制作`composer`脚本一样，我们同样可以通过制作名为`php`的脚本来实现在工作目录中使用`./php`命令来代替`php`命令：

```sh
touch php
```

编辑`php`脚本内容：

```shell
#!/bin/bash

docker compose run --rm php php "$@"
```

给脚本执行权限：

```sh
chmod +x php
```

使用脚本：

```sh
./php think make:controller Test
```

看到`Controller:app\controller\Test created successfully.`结果后，就可以看到工作目录下的`app\controller`目录中已经出现`Test.php`文件了。

## 一键完成

以上是搭建这样一套环境的整个过程，按照以上过程，已经做好了现成的一套代码库，可以直接使用：

```sh
git clone https://git.wandoubaba.com/wandoubaba/docker-tp6-php74.git tp-project
cd tp-project
git remote remove origin
./composer install
chown www-data:www-data -R runtime
docker compose up -d
```
