各位前端、后端、全栈的大佬们,还有那些正在对着满屏红色的 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 中,利用 context 和 dockerfile 参数来指定路径。但在 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
这段代码的逻辑是:
- 先把
composer.json复制进去。 - 执行安装。这时候
composer.json变了,它会重新下载。 - 把你的代码(.php 文件)复制进去。如果代码改了,Docker 发现代码层变了,会重新执行
dump-autoload,但不会重新下载 Composer 包! --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: # 定义卷的名称
深度解析:
- Volumes (
db_data):这就是我们的保险箱。不管容器怎么重启、删除,db_data这个卷里的数据都在。这是生产环境必须的,也是开发环境必须的。 - Ports (
"80:80"):将宿主机的 80 端口映射到容器的 80 端口。这样你访问 localhost 就能看到你的 PHP 网站了。 - 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 吗?
绝对不行!不能直接用。
为什么?
- 安全性:你不想暴露数据库端口给全世界看吧?Nginx 应该是唯一的外部入口。
- 性能:本地开发用的 Alpine 镜像虽然小,但我们可以优化构建过程,使用多阶段构建。
- 资源限制:生产环境需要限制容器的内存和 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"
这段代码的意思是:
- 当你推送代码时,GitLab 会拉起一个临时的 Docker 机器。
- 读取你的 Dockerfile,构建镜像,打上当前代码的 Commit ID 标签,推送到你的私有镜像仓库(如阿里云镜像仓库、Harbor)。
- 自动 SSH 登录到生产服务器。
- 拉取最新的镜像。
- 重新部署。
这就是 DevOps 的灵魂。 代码一提交,环境就变了。
第八章:排错指南——容器黑话词典
最后,作为讲座的压轴,我们来聊聊容器里常见的报错。如果看到这些,别慌,这是它们的“黑话”。
-
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)。
- 症状:Nginx 日志里报错
-
Permission denied- 症状:文件上传失败,或者执行 PHP 脚本时报错。
- 原因:容器里的用户(通常是
www-data)没有权限读写宿主机挂载的目录。 - 解决:
chmod -R 775 .或者chown -R 1000:1000 .(把 1000 换成容器内的 UID)。或者,干脆用sudo(不推荐,违背 Docker 精神)。
-
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!