PHP如何利用Docker快速部署完整开发与生产运行环境

各位前端、后端、全栈的大佬们,还有那些正在对着满屏红色的 Error 报错怀疑人生的实习生朋友们,大家好!

欢迎来到今天的讲座现场。我是你们的老朋友,一个头发越来越少但经验越来越丰富的资深工程师。今天,我们不讲那些虚头巴脑的架构设计,不讲那些让你听了就想睡觉的设计模式。今天,我们来聊聊一个让无数PHP开发者痛彻心扉,却又爱恨交织的话题——环境配置

大家有没有过这种经历?
你用 Mac 写代码,顺手 composer install,丝滑流畅,数据库连上就能跑。
你同事用 Windows,回来复刻你的代码,结果 php version mismatch,或者 extension pdo_mysql not found
你把代码扔到 Linux 服务器上,以为这就稳了?结果 vendor 文件夹不对,PHP-FPM 配置不对,权限不对,甚至是那一行 sudo chown 都能把你折腾得怀疑人生。

这时候,你可能会听到身边的高手抛出一句话:“在我电脑上能跑,你的电脑上肯定也能跑。

听听,多么霸道的逻辑! 这简直是把“赌博”当成了科学。

但今天,我们要把这种赌博变成科学。我们要用 Docker,这个容器化技术的“瑞士军刀”,来彻底终结 PHP 开发中的环境地狱。我们将从开发环境的“舒适区”一路狂奔到生产环境的“安全区”,中间不需要换鞋,不需要重新安装系统。

准备好了吗?Let’s dive into the code.


第一章:环境配置的“地中海”式悲剧

首先,我们要承认一个事实:PHP 的生态系统有时候就像个乱糟糟的客厅。你需要 PHP 7.4,但他妈的你的服务器默认安装了 PHP 8.0。你需要 xdebug 进行调试,你需要 gd 库来处理图片,你需要 redis 扩展来缓存。

传统的方式是什么?是在宿主机上手动安装 PHP,然后配置 php.ini,然后 apt-get install mysql-server,然后在 /etc/nginx/sites-available/ 配置虚拟主机,再在 mysql 里建库建表。

这一套流程下来,如果你能一次成功,那你不仅是个优秀的程序员,你绝对是天选之子。

为什么我们讨厌这个?因为环境具有传染性。你在开发环境把代码改坏了,可能永远不知道为什么,因为你的本地环境可能恰好“修补”了代码中的 Bug。这就是“在我的机器上能跑”的假象。

Docker 是什么?

简单来说,Docker 就是一个“便携式服务器”。它把你的代码、PHP 运行时、数据库、Web 服务器全部打包进一个隔离的容器里。就像你买的一箱苹果,不管你在哪个国家打开,打开的一瞬间,它就是苹果,不是香蕉。它不会因为换了个容器就变质。


第二章:构建你的“PHP 终极战舰”

要利用 Docker,第一步不是去配置什么复杂的架构,而是学会如何“写菜谱”。在 Docker 里,这个菜谱叫 Dockerfile

想象一下,你要做一道 PHP 的“宫保鸡丁”。传统的做法是去买现成的半成品(安装系统包),但 Docker 的做法是:从零开始,严格按照你的口味,一点点炒出来

让我们从一个最基础的 PHP-FPM 镜像开始。

1. 选择合适的“食材”(基础镜像)

我们不自己编译 PHP,那太费时间了。我们用官方的镜像。

# 第一行:声明基础镜像。这是你的厨房地基
FROM php:7.4-fpm-alpine

# 下面这一行是绝对经典的“咒语”,用来增加扩展
RUN docker-php-ext-install pdo pdo_mysql mbstring

为什么要用 Alpine?
这就像是选用了速冻蔬菜而不是新鲜蔬菜。alpine 版本的镜像非常小,只有几 MB。这能极大提升构建速度,而且更安全(漏洞少)。

深度解析:
docker-php-ext-install 是 PHP 官方提供的一个脚本,它会自动下载源码、编译、安装扩展。你只需要输入你想用的扩展名字(比如 redis, gd, soap),剩下的交给 Docker。

2. 安装 Composer(你的调料)

没有 Composer,PHP 就像没有盐的菜,没味道。我们在镜像里安装 Composer。

# 下载 Composer 安装器
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

这里用了一个高级技巧:COPY --from=composer:latest。我们直接把别人已经打包好的 Composer 镜像里的二进制文件复制过来。这是 Docker 的精髓:复用

3. 设置工作目录(把厨房搬到这儿来)

WORKDIR /var/www/html

所有的操作都在这个目录进行。容器启动后,这个就是你的根目录。

4. 复制代码(把原材料搬进厨房)

COPY . .

注意,这里通常是在 docker-compose.yml 中,利用 contextdockerfile 参数来指定路径。但在 Dockerfile 本身,我们假设我们在项目根目录。


第三章:Composer 的“依赖地狱”与缓存

写完了 Dockerfile,你可能觉得这就行了。别急,还没完。如果你每次 docker build 都去网络下载所有的 Composer 包,那你的构建速度会慢得让你想砸键盘。Composer 包那么多,下载一个 monolog/monolog 都要好几秒。

解决方案:分层缓存。

Docker 的构建是分层的,如果上面的层没变,它不会重新执行下面的层。

# 第一层:依赖
COPY composer.json composer.lock ./
RUN composer install --no-scripts --no-autoloader --prefer-dist

# 第二层:代码(代码经常变,但依赖很少变)
COPY . .

# 第三层:生成 Autoloader
RUN composer dump-autoload --optimize

这段代码的逻辑是:

  1. 先把 composer.json 复制进去。
  2. 执行安装。这时候 composer.json 变了,它会重新下载。
  3. 把你的代码(.php 文件)复制进去。如果代码改了,Docker 发现代码层变了,会重新执行 dump-autoload,但不会重新下载 Composer 包!
  4. --optimize 参数是生产环境的标配,能把 PSR-4 的映射编译成 map 文件,让 require 速度提升几倍。

幽默时刻:
这就像你做菜,昨天切了葱姜蒜,今天只换了肉。你肯定不会把葱姜蒜扔了重新切一遍吧?Docker 也很聪明。


第四章:数据库与卷——别让你的老婆睡大街

现在我们有了一个能跑 PHP 的容器,但这容器里是空的,没有数据库。而且,如果你重启这个容器,数据会全部丢失。就像你每天晚上回家,第二天早上发现老婆不见了,那是恐怖片,不是开发环境。

我们需要持久化存储。

在 Docker 里,这叫 Volumes

假设我们要接上 MySQL。

1. 编写 Dockerfile(再次强化)

为了让我们的 PHP 容器能连上 MySQL,我们需要安装 MySQL 的扩展。

FROM php:7.4-fpm

# 安装必要的扩展和工具
RUN apt-get update && apt-get install -y 
    libpng-dev 
    libonig-dev 
    libxml2-dev 
    && rm -rf /var/lib/apt/lists/* 
    && docker-php-ext-install pdo pdo_mysql mbstring

# 安装 Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

2. 编写 docker-compose.yml(真正的交响乐)

现在,我们需要把这些组件组合起来。Docker Compose 是指挥家,它告诉 Docker:“嘿,你要同时启动 Web 服务器、PHP 处理器、数据库和 Redis 缓存。”

version: '3.8'

services:
  # Web 服务器(这里用 Nginx,它是高性能的守门员)
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./public:/var/www/html/public # 确保访问的是 public 目录
      - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf # 挂载配置文件
    depends_on:
      - app

  # PHP 应用(我们刚才写的那个)
  app:
    build: .
    volumes:
      - ./:/var/www/html
      - ./docker/php/conf.d/custom.ini:/usr/local/etc/php/conf.d/custom.ini # 自定义配置
    depends_on:
      - db

  # 数据库(MySQL)
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: my_app
      MYSQL_USER: dev_user
      MYSQL_PASSWORD: dev_pass
    volumes:
      - db_data:/var/lib/mysql # 关键!持久化数据
    ports:
      - "3306:3306" # 允许外部连接(开发时方便用 Navicat)

  # Redis(缓存之王)
  redis:
    image: redis:alpine

volumes:
  db_data: # 定义卷的名称

深度解析:

  1. Volumes (db_data):这就是我们的保险箱。不管容器怎么重启、删除,db_data 这个卷里的数据都在。这是生产环境必须的,也是开发环境必须的。
  2. Ports ("80:80"):将宿主机的 80 端口映射到容器的 80 端口。这样你访问 localhost 就能看到你的 PHP 网站了。
  3. Volumes (挂载代码)- ./:/var/www/html。这一行把你的宿主机当前目录(代码)挂载到了容器里。这意味着,你本机改一行代码,容器里的 PHP 立刻就能看到。这是开发体验的巅峰!

第五章:Nginx 配置——如何正确地“喂饭”

光有 PHP 还不够,容器之间是隔离的。Nginx 是 Web 服务器,它负责接收用户的请求,然后把请求“转发”给 PHP-FPM 处理。

我们需要一个 Nginx 配置文件。

创建 docker/nginx.conf

server {
    listen 80;
    server_name localhost;
    root /var/www/html/public; # 指向 Laravel 或 Symfony 的 public 目录
    index index.php index.html;

    # 处理 .php 文件
    location ~ .php$ {
        try_files $uri =404; # 如果文件不存在,报错
        fastcgi_split_path_info ^(.+.php)(/.+)$;
        fastcgi_pass app:9000; # 关键!app 是我们在 docker-compose.yml 里定义的服务名
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    # 访问图片、CSS、JS 时不需要经过 PHP
    location ~* .(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 30d;
    }
}

注意那个 fastcgi_pass app:9000
这是 Docker 的魔法。容器之间的通信不需要 IP 地址,而是通过 Service Name(服务名)。app 是我们在 Compose 文件里定义的服务名。Nginx 会自动把这个名字解析成 PHP 容器的 IP。

这就像是说:“喂,PHP 容器,过来一下,有个活儿给你干。”


第六章:生产环境——从“游乐场”到“战场”

好,现在你在本地用 Docker Compose 跑得很爽了。但如果这时候产品经理说:“把代码部署到阿里云吧”,你还能用这个 docker-compose.yml 吗?

绝对不行!不能直接用。

为什么?

  1. 安全性:你不想暴露数据库端口给全世界看吧?Nginx 应该是唯一的外部入口。
  2. 性能:本地开发用的 Alpine 镜像虽然小,但我们可以优化构建过程,使用多阶段构建。
  3. 资源限制:生产环境需要限制容器的内存和 CPU,防止某个微服务把服务器内存吃光。

生产级 Dockerfile 优化

# 第一阶段:构建阶段(不需要 PHP 和 Composer)
FROM php:7.4-fpm AS build

WORKDIR /app

COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts

COPY . .

# 第二阶段:运行阶段(只包含 PHP 和扩展,非常小)
FROM php:7.4-fpm-alpine

WORKDIR /var/www/html

# 复制构建阶段生成的 vendor 目录,而不是重新安装
COPY --from=build /app/vendor ./vendor
COPY --from=build /app ./ .

# 安装 Nginx 和 Supervisor(用于进程管理)
RUN apk add --no-cache nginx supervisor

# 配置 Supervisor 来管理 PHP-FPM
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

EXPOSE 80

CMD ["/usr/bin/supervisord"]

这个 supervisord.conf 会同时启动 Nginx 和 PHP-FPM。这样你只需要一个容器就能承载整个 Web 应用。

幽默时刻:
在开发环境,你可能有 10 个终端窗口开着,分别跑着 MySQL、Redis、Postgres、Mongo、Selenium… 在生产环境,你只有 1 个容器,里面把所有活儿都干了。这就是优雅。


第七章:CI/CD 自动化部署——让部署像呼吸一样简单

作为资深专家,我不能只教你手动敲命令。我们要自动化。

假设你把代码提交到了 GitHub/GitLab。我们可以写一个 .gitlab-ci.yml(或者 GitHub Actions 的配置)。

image: docker:latest

services:
  - docker:dind

stages:
  - build
  - deploy

build_job:
  stage: build
  script:
    - echo "Building the application..."
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL
    - docker build -t $REGISTRY_URL/my-php-app:$CI_COMMIT_SHA .
    - docker push $REGISTRY_URL/my-php-app:$CI_COMMIT_SHA

deploy_job:
  stage: deploy
  script:
    - echo "Deploying to server..."
    - ssh user@server "cd /var/www/my-app && docker-compose pull && docker-compose up -d"

这段代码的意思是:

  1. 当你推送代码时,GitLab 会拉起一个临时的 Docker 机器。
  2. 读取你的 Dockerfile,构建镜像,打上当前代码的 Commit ID 标签,推送到你的私有镜像仓库(如阿里云镜像仓库、Harbor)。
  3. 自动 SSH 登录到生产服务器。
  4. 拉取最新的镜像。
  5. 重新部署。

这就是 DevOps 的灵魂。 代码一提交,环境就变了。


第八章:排错指南——容器黑话词典

最后,作为讲座的压轴,我们来聊聊容器里常见的报错。如果看到这些,别慌,这是它们的“黑话”。

  1. Connection refused

    • 症状:Nginx 日志里报错 connect() failed (111: Connection refused) while connecting to upstream
    • 原因:PHP 容器没起来,或者 PHP-FPM 没监听 9000 端口。
    • 解决:检查 docker-compose up -d,看看 PHP 容器状态是不是 Exited。或者检查 docker-compose.yml 里的服务名是不是写对了(比如是不是把 web 写成了 www)。
  2. Permission denied

    • 症状:文件上传失败,或者执行 PHP 脚本时报错。
    • 原因:容器里的用户(通常是 www-data)没有权限读写宿主机挂载的目录。
    • 解决chmod -R 775 . 或者 chown -R 1000:1000 .(把 1000 换成容器内的 UID)。或者,干脆用 sudo(不推荐,违背 Docker 精神)。
  3. Too many open files

    • 症状:偶尔 502 错误。
    • 原因:你的 PHP 脚本打开的文件句柄太多,超过了 Linux 的限制。
    • 解决:修改 docker-compose.yml 里的 ulimits 配置。
      ulimits:
        nproc: 65535
        nofile:
          soft: 20000
          hard: 40000

总结与展望

好了,今天的讲座就要结束了。我们讲了什么?
我们讲了如何逃离“在我的电脑上能跑”的陷阱,如何用 Dockerfile 构建一个纯净的 PHP 环境,如何用 docker-compose 将 PHP、Nginx、MySQL、Redis 组成一个家庭联欢会,以及如何将这些技能应用到 CI/CD 流程中。

Docker 不仅仅是工具,它是一种思维方式的转变。

以前,我们是“配置管理者”,每天盯着 /etc/hosts.ini 文件发愁。
现在,我们是“环境创造者”,通过代码定义世界。

当你下次再听到有人抱怨:“这代码在我这跑得好好的,怎么在你这就挂了?”的时候,你可以淡定地回他一句:
“别扯淡了,把你的 Dockerfile 发我看看。”

如果他没有 Dockerfile,那就告诉他:“小伙子,赶紧去学学 Docker 吧,那是通往技术自由的道路。”

记住,容器化不是目的,环境一致性才是王道。有了 Docker,你的代码就是一台随时待命的战车。

谢谢大家!如果有问题,欢迎在休息时间来我的工位(虚拟的)聊聊。Keep Coding, Keep Containering!

发表回复

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