首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Docker Compose 搭建可复现 PHP 开发环境与排错指南
📅 2026/10/23 1:08:05
✍️ 爱科研究院
👁 阅读 3,247
“Docker 搭建 PHP 开发环境”这个题目我前后折腾过至少五六轮从最早用一堆脚本装 Nginx、PHP、MySQL到后来写一个 compose 文件就把整套服务拉起来中间踩的坑基本都能写成本小册子。先说结论用 Docker 搭 PHP 开发环境核心价值不在于“先进”而在于把你脑子里那套环境变成一份能提交到代码仓库、能给别人复现、能随时删掉重建的配置。这件事对小团队尤其明显——一个人环境能跑十个人环境都能跑这才是它真正解决的问题。下面这些内容适合两类人看一类是刚接触容器、想把本地那套 PHP、Nginx、MySQL 搬进去的开发者另一类是环境已经搭起来了但被 502、权限报错、文件挂载慢折磨过的老手。我会把选型理由、完整配置、参数计算、排查路径都摊开讲你照着抄基本能跑通也能明白每一步为什么要这么写。1. 先想清楚为什么要把 PHP 环境装进容器1.1 本地直装环境最常见的四种崩法在宿主机上装 PHP 这套东西前半年通常没问题半年之后开始出各种毛病。第一种是版本冲突手上同时维护三个项目一个锁在 PHP 7.4一个要 8.1还有一个用了 8.2 的新特性。系统里只能有一个默认的 php 可执行文件切换靠改 PATH 或者软链切来切去总会忘。第二种是扩展污染某个项目要装 imagick 或者 sphinx 扩展编译完之后系统里的 PHP 多了一堆依赖过阵子想卸载发现连带着把别的扩展也搞坏了。第三种最要命是“隐式依赖”代码里用了一个只有特定版本才有的函数行为本机跑得好好的同事拉下来就报错查半天发现是他那边的 PHP 补丁版本不同。第四种是服务端组件难装。项目里用了 Redis、Elasticsearch 或者消息队列在宿主机上装这些要么用包管理器装个老版本要么手动下载解压配置开机自启卸载的时候残留一堆目录。这类组件本身和 PHP 没什么关系却污染了开发机的环境时间一长机器上堆了几十个不知道干什么用的服务。容器化不是唯一解法但它把“环境”这件事从“机器状态”变成了“文件描述”这是本质区别。1.2 容器化真正解决的是“可复现”很多人以为 Docker 的好处是“轻量”“快”其实对开发环境来说最值钱的是可复现。你的 Dockerfile 和 compose 文件就是一份环境说明书里面写清楚了 PHP 用哪个版本、装了哪些扩展、Nginx 怎么配置、数据库用什么字符集。这份说明书放进代码仓库新人拉下来执行一条命令环境就和他同事那边完全一致连 PHP 的编译参数都一样。我见过太多团队把环境搭建文档写在内部 wiki 上写得很详细但没有用因为文档会过期而配置文件不会——配置文件过期了环境就跑不起来问题会立刻暴露。另一个隐性收益是“可弃性”。你需要试一个新扩展、新版本直接改 Dockerfile 重建镜像就行搞坏了删掉重来宿主机一点痕迹都不留。这种“随便试错”的自由度在裸机环境上是要付出代价的。还有一层容器内的进程隔离让你能放心地把一些“看起来不太安全”的操作放进去做比如临时开个调试端口、跑个一次性脚本不会影响宿主机其他项目。1.3 代价与适用边界也得说实话容器化不是没有成本。第一是磁盘占用一个带扩展的 PHP 镜像轻松到六七百兆加上 MySQL、Redis 的镜像一台机器上跑几个项目几十个 G 就没了。第二是多了一层网络和文件系统的抽象出问题时排查路径变长你得先判断是容器内的问题还是容器间通信的问题。第三是文件挂载的性能损耗在 macOS 和 Windows 上尤其明显这点后面单独讲。我的判断标准是这样单人小项目、只用一个 PHP 版本、不依赖复杂中间件裸机装也完全没问题没必要为了容器而容器。但只要有第二个人参与或者项目数量超过两个或者需要 MySQL、Redis、消息队列这些组件就值得上容器。因为这时候的一致性收益会迅速超过学习成本。反过来说如果你连 Docker 的基本概念都不熟先从跑一个官方 PHP 镜像开始别一上来就写四五个服务的编排文件那样出问题你根本不知道该查哪里。2. 定方案镜像、组件和目录怎么选2.1 PHP 镜像的三个关键决策官方 PHP 镜像的选择看起来简单其实有三个决策点会直接影响后面踩坑的数量。第一个是版本标签php:8.2-fpm这种写法锁的是大版本会跟着小版本更新想要绝对可控就写php:8.2.15-fpm。我一般建议开发环境锁到小版本避免某天拉镜像后行为突变排查半天。第二个是基础系统Debian 版还是 Alpine 版。Alpine 体积小一百多兆但用的是 musl libc某些扩展编译时需要额外装依赖个别 Composer 包在 Alpine 上也遇到过兼容问题。Debian 版镜像大一些五六百兆但apt装依赖顺手docker-php-ext-install几乎不会出岔子。我的建议很直接新手、或者项目依赖比较杂用 Debian 版对镜像体积有强需求、扩展列表固定再考虑 Alpine。第三个是fpm还是apache。用php:8.2-apache确实省事一个容器里 PHP 和 Web 服务器都有了改个.htaccess就能跑。但它和大多数生产环境不一致——线上一般是 Nginx 加 php-fpm。开发环境的价值之一是“尽量贴近生产”所以我更推荐php:8.2-fpm加独立的 Nginx 容器多写一个配置文件换来的是配置语义和线上一致。决策点选项 A选项 B我的倾向版本标签大版本浮动小版本锁定锁小版本稳定性优先基础镜像DebianAlpine依赖复杂选 Debian追求体积选 Alpine运行模式apache 内置fpm 独立fpm 独立 Nginx贴近生产2.2 组件清单与选型对照一套典型的 PHP 开发环境服务清单大概是这样PHP-FPM 跑应用代码Nginx 做反向代理和静态资源MySQL 或 MariaDB 存数据Redis 做缓存和会话再加一个可选的 Composer 容器或者把 Composer 塞进 PHP 镜像。每一个都有取舍。数据库这块MySQL 8 和 5.7 的差异主要在默认认证插件8.0 默认用caching_sha2_password老版本的 PHP 客户端驱动连不上会报认证错误。如果你用比较新的pdo_mysql一般没问题如果项目里有老代码或者老驱动就在启动参数里加--default-authentication-pluginmysql_native_password能省掉一堆排查时间。MariaDB 兼容性更好配置项也少但要注意它和 MySQL 在某些 SQL 语法和函数上有细微差别团队里如果线上是 MySQL开发环境就别用 MariaDB避免“本地能跑线上报错”。Redis 基本没什么争议用官方redis:7-alpine就行体积小启动快。要开持久化就加--appendonly yes开发环境其实不太需要。Nginx 用nginx:1.25-alpine配置文件挂载进去改动后docker compose restart nginx生效不用重建镜像。这里有个原则凡是“配置”类的东西都挂载凡是“安装”类的东西都写进 Dockerfile。配置文件挂载可以热改装扩展必须重建分清楚这个界限日常开发效率会高很多。2.3 目录结构与挂载约定目录结构看起来是小事但它决定了后面配置好不好写。我习惯在项目根目录下建一个docker目录里面按服务分子目录docker/php/Dockerfile、docker/php/php.ini、docker/php/www.conf、docker/nginx/default.conf、docker/mysql/init/。项目代码放在src目录。整个项目根目录下放docker-compose.yml。这样代码和配置分离提交仓库时一目了然也不容易把配置文件混进业务代码里。挂载约定里有一条是铁律Nginx 容器和 PHP 容器挂载的代码路径必须完全一致。比如两边都挂到/var/www/html。因为 Nginx 传给 php-fpm 的SCRIPT_FILENAME是一个容器内的绝对路径PHP 容器要能按这个路径找到文件。很多人图省事Nginx 挂到/usr/share/nginx/htmlPHP 挂到/var/www/html结果就是 404 或者“Primary script unknown”而且日志里看不出明显线索能查很久。还有一个细节是挂载的读写权限。Nginx 只需要读挂载时加:ro更安全PHP 容器需要读写比如写日志、写缓存、上传文件不加。数据库的数据目录不要用本地目录挂载用命名卷named volume因为本地目录挂载在数据库场景下容易遇到权限和文件系统兼容问题而且删除时容易误删数据。3. 动手实现一份能跑起来的完整配置3.1 Dockerfile扩展、Composer 与用户 IDPHP 的 Dockerfile 大概分三段装系统依赖和扩展、调整用户 ID、复制配置。装扩展这块docker-php-ext-install能装的直接装需要pecl的单独装。要注意gd需要先docker-php-ext-configure指定 jpeg 和 freetype 的路径否则装出来没有图像处理能力。装完之后清理 apt 缓存不然镜像会白白大出几百兆。FROM php:8.2-fpm ENV TZUTC \ COMPOSER_ALLOW_SUPERUSER1 \ COMPOSER_HOME/tmp/composer RUN apt-get update apt-get install -y --no-install-recommends \ libzip-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev \ libicu-dev libonig-dev unzip git \ docker-php-ext-configure gd --with-freetype --with-jpeg \ docker-php-ext-install -j$(nproc) pdo_mysql mysqli gd zip intl bcmath opcache pcntl \ pecl install redis \ docker-php-ext-enable redis \ apt-get purge -y --auto-remove \ rm -rf /var/lib/apt/lists/* /tmp/pear # 把 Composer 从官方镜像里拷过来避免在容器内跑安装脚本 COPY --fromcomposer:2 /usr/bin/composer /usr/local/bin/composer # 关键一步让容器内 www-data 的 uid/gid 和宿主机当前用户一致 ARG UID1000 ARG GID1000 RUN usermod -u ${UID} www-data groupmod -g ${GID} www-data COPY php.ini /usr/local/etc/php/conf.d/zz-custom.ini COPY www.conf /usr/local/etc/php-fpm.d/zz-www.conf WORKDIR /var/www/htmlusermod那一段是很多人忽略的。Linux 宿主上你的用户 ID 可能是 1000容器里www-data是 33PHP 写storage或者runtime目录时就会失败报权限拒绝。把容器内的 uid 改成和宿主一致挂载目录下的文件归属就自然对上了。macOS 上宿主用户 ID 一般是 501所以构建时要传参UID$(id -u)。配置文件用zz-前缀是为了保证它在官方默认配置之后加载后加载的覆盖先加载的这个命名约定值得记住否则你改的memory_limit可能被默认值盖掉。3.2 docker-compose.yml服务编排逐段说明编排文件是整个环境的中枢重点不在“能起来”而在“起来得对”。下面这份配置我用了很久几个服务之间的依赖关系、健康检查、网络隔离都做了处理。services: php: build: context: ./docker/php args: UID: ${UID:-1000} GID: ${GID:-1000} volumes: - ./src:/var/www/html - /var/www/html/vendor working_dir: /var/www/html environment: TZ: UTC XDEBUG_MODE: ${XDEBUG_MODE:-off} XDEBUG_CONFIG: client_hosthost.docker.internal extra_hosts: - host.docker.internal:host-gateway depends_on: mysql: condition: service_healthy redis: condition: service_started networks: [backend] nginx: image: nginx:1.25-alpine ports: - 8080:80 volumes: - ./src:/var/www/html:ro - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: [php] networks: [backend] mysql: image: mysql:8.0 command: --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci --default-authentication-pluginmysql_native_password --default-time-zone00:00 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: app MYSQL_USER: app MYSQL_PASSWORD: app ports: - 3307:3306 volumes: - mysql-data:/var/lib/mysql - ./docker/mysql/init:/docker-entrypoint-initdb.d:ro healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -uroot, -proot] interval: 10s timeout: 5s retries: 12 networks: [backend] redis: image: redis:7-alpine volumes: - redis-data:/data networks: [backend] networks: backend: volumes: mysql-data: redis-data:几个地方值得解释。PHP 服务里多了一行- /var/www/html/vendor这是匿名卷作用是把vendor目录从宿主机挂载中“遮蔽”掉让 Composer 依赖只存在于容器文件系统里。原因很实际vendor目录动辄几万个文件宿主和容器之间同步这些文件极慢尤其在 macOS 上光这一项就能让页面响应时间翻倍。MySQL 的端口映射成3307:3306是因为宿主机上经常已经装了本地 MySQL 占着 3306。开发阶段用非默认端口连客户端工具里配一次就行。健康检查是为了让 PHP 容器别在数据库还没初始化完的时候启动否则应用一启动就连库失败。default-time-zone00:00是为了让数据库时区和 PHP 对齐避免出现存储时间比真实时间差八小时这种经典问题——用偏移量写比写时区名更省事也能避开容器里没有时区数据库的情况。3.3 Nginx 与 php-fpm 的通信细节Nginx 和 PHP 之间的通信是这套环境里最容易出环节的地方。核心就一句Nginx 收到.php结尾的请求通过 FastCGI 协议转发给 PHP-FPM 容器的 9000 端口并告诉它“要执行的文件在容器里的绝对路径是什么”。server { listen 80; server_name _; root /var/www/html/public; index index.php index.html; client_max_body_size 64m; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { # 用变量 Docker 内置 DNS避免容器重启后 IP 变化导致 502 resolver 127.0.0.11 valid10s ipv6off; set $php_upstream php:9000; fastcgi_pass $php_upstream; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_read_timeout 300s; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; } location ~ /\.(?!well-known) { deny all; } }resolver那一行是个很有价值的技巧。Docker 的内置 DNS 服务地址固定是127.0.0.11。当你写fastcgi_pass php:9000时Nginx 在启动时就解析一次主机名并把 IP 缓存下来之后 PHP 容器重启换了 IPNginx 还往旧地址发请求表现就是持续 502重启 Nginx 才好。改成变量形式后Nginx 每次请求都重新解析配合valid10s缓存十秒问题就消失了。这个是排查过多次才总结出来的值得直接抄。SCRIPT_FILENAME这一行必须显式写。fastcgi_params文件里默认没有这个参数或者值不对不写的话 PHP-FPM 找不到要执行的文件日志里就是“Primary script unknown”。root指向public目录是常见框架的入口结构如果你的项目入口在根目录改成/var/www/html即可但要和挂载路径保持一致。3.4 php.ini 与 www.conf 关键参数PHP 侧有两份配置要改一份是php.ini层面的内存、上传、时区、OPcache一份是php-fpm层面的进程模型、超时、日志。memory_limit 256M upload_max_filesize 64M post_max_size 64M max_execution_time 300 max_input_time 300 date.timezone UTC display_errors On error_reporting E_ALL log_errors On error_log /proc/self/fd/2 opcache.enable 1 opcache.enable_cli 0 opcache.memory_consumption 256 opcache.interned_strings_buffer 32 opcache.max_accelerated_files 20000 opcache.validate_timestamps 1 opcache.revalidate_freq 2error_log /proc/self/fd/2是让 PHP 的错误直接写到标准错误这样docker compose logs -f php就能看到不用进容器翻日志文件。upload_max_filesize和post_max_size要一起改只改一个的话上传大文件时 PHP 仍然会因为 POST 体积超限而丢掉整个请求体表现是$_FILES为空很难查。OPcache 在开发环境要格外注意opcache.validate_timestamps 1。有些团队图省事直接把生产的 OPcache 配置抄过来设置成 0结果改完代码刷新页面没反应以为是挂载没同步其实是字节码缓存没失效。开发环境让它每秒检查一次文件时间戳就够了性能损失可以忽略。[www] user www-data group www-data listen 9000 pm dynamic pm.max_children 20 pm.start_servers 4 pm.min_spare_servers 4 pm.max_spare_servers 8 pm.max_requests 500 request_terminate_timeout 300s catch_workers_output yes clear_env no access.log /proc/self/fd/2clear_env no这一项容易被忽略。默认情况下 php-fpm 会清空环境变量导致你在 compose 里设置的一些变量在 PHP 里读不到尤其是在容器里跑命令行脚本时表现就是“明明环境变量配了代码里getenv返回空”。catch_workers_output yes是把子进程的输出也转发到主进程日志否则 PHP 的警告信息可能完全看不到。4. 调优与验证让它快起来、稳下来4.1 文件挂载性能这个老大难文件挂载的性能差异是容器化开发环境最容易被抱怨的点。Linux 上 bind mount 几乎是原生性能感知不到差别。macOS 和 Windows 上要经过一层文件系统转换早期方案下一个页面的响应时间能差出五到十倍。现在新版本桌面端默认用 VirtioFS情况好了很多但海量小文件仍然会拖慢。具体的优化手段有三个。一是挂载参数./src:/var/www/html:cached在部分平台上能减少同步开销写上去没坏处。二是用匿名卷遮蔽高密度目录前面提到的vendor就是典型node_modules、缓存目录同理这些目录不需要在宿主机上看到让它们只活在容器里就行。三是别把项目放在很深的路径下也尽量别放在需要额外同步的目录里比如某些云盘同步目录否则文件变更事件会明显变多。实测上的量级可以参考同一套代码在旧方案下首屏请求要七八百毫秒切到 VirtioFS 之后降到一百多毫秒再把vendor用匿名卷遮蔽能压到几十毫秒和裸机差别不大了。这个优化对日常开发的体验影响极大因为你每次刷新页面都在等它。4.2 进程数与内存的参数计算pm.max_children是 php-fpm 的关键参数设小了并发上来就排队设大了容易把容器内存撑爆被系统杀掉。计算方法不复杂先确定 PHP 容器可用内存减去系统和其他进程的开销再除以单个 PHP 进程的平均常驻内存。单进程内存可以用docker stats观察也可以用pmap看某个 worker 的 RSS。一般业务代码在 30 到 60MB 之间框架重、加载扩展多的会更高。假设给 PHP 容器 1GB 上限预留 300MB 给系统、日志、临时文件剩下 700MB按单进程 60MB 算max_children取 11 比较稳妥。如果设成 20理论上内存峰值会超过 1GB容器可能被 OOM 干掉或者整个宿主变卡。还要注意max_children只是 php-fpm 的并发处理上限实际并发还受 Nginx 的worker_connections和系统文件描述符限制约束。开发环境一般不会碰到这些边界但如果你把环境拿去做压测或者当临时测试服就得一起看。pm.max_requests 500是让 worker 处理一定请求数后自动重启防止内存泄漏累积开发环境设小一点也没关系。4.3 Xdebug 断点链路打通Xdebug 是这套环境里仅次于文件挂载的“事故高发区”。关键点有三个容器里的 Xdebug 要能主动连到宿主机的 IDEIDE 要知道容器内路径对应本地哪个路径触发方式要可控不能每个请求都尝试连接。zend_extensionxdebug xdebug.modedebug xdebug.client_hosthost.docker.internal xdebug.client_port9003 xdebug.start_with_requesttrigger xdebug.idekeyPHPSTORM xdebug.discover_client_host0 xdebug.log/tmp/xdebug.loghost.docker.internal在桌面端默认可用Linux 上需要在 compose 里加extra_hosts: [host.docker.internal:host-gateway]这一条不加容器根本解析不到宿主机断点永远连不上。start_with_requesttrigger表示只有带上触发参数或者 Cookie 时才启动调试日常浏览页面不受影响这是必须的否则每个请求都要等 Xdebug 尝试连接页面响应会明显变慢。路径映射在 IDE 里配容器内/var/www/html对应当前项目的src目录。配错了的表现是断点变成灰色或者不生效但 Xdebug 日志里能看到连接已建立这时候别怀疑网络直接检查路径映射。另外xdebug.log是排查神器连不上、端口不对、路径不对日志里都写得清清楚楚比猜快一百倍。4.4 一把梭的验证清单与常用命令环境搭完别急着写业务先按顺序验证一遍每一步都能定位到具体问题是哪一层。# 1. 配置语法检查 docker compose config -q # 2. 启动并看状态 docker compose up -d docker compose ps # 3. PHP 是否装上了需要的扩展 docker compose exec php php -m | sort # 4. PHP 能不能连到数据库 docker compose exec php php -r var_dump((new PDO(mysql:hostmysql;port3306;dbnameapp,app,app))-query(select 1)-fetch()); # 5. 页面是否通 curl -I http://127.0.0.1:8080 # 6. 实时看日志 docker compose logs -f nginx php这几条命令覆盖了 90% 的初次搭建问题。扩展不在列表里说明 Dockerfile 里的docker-php-ext-install名字写错了或者编译失败重建时注意看构建日志。数据库连不上先确认服务名是不是mysqlcompose 里服务名就是容器网络里的主机名再确认数据库已经初始化完成健康检查过了再试。页面报 502 就去看 Nginx 日志里的 upstream 错误报 404 就去看root和挂载路径。日常最常用的几条命令也顺手记一下docker compose exec php composer install装依赖docker compose exec php php -v看版本docker compose restart php改完 ini 重启生效docker compose down停掉但保留数据卷docker compose down -v连数据一起删。最后这条要格外小心-v会把命名卷删掉数据库内容就没了我在早期项目里干过不止一次。5. 问题排查实录502、504、权限与连接失败5.1 502 与“Primary script unknown”的定位路径502 是这套环境里出现频率最高的错误但它的成因其实很集中。第一种是 Nginx 连不上 php-fpm日志里会写connect() failed (111: Connection refused) while connecting to upstream。原因通常是 PHP 容器没起来、fastcgi_pass的主机名写错或者 php-fpm 监听的地址不是9000。排查顺序docker compose ps看 PHP 容器状态docker compose exec php ss -lntp看 9000 有没有在监听docker compose logs php看 fpm 启动日志有没有报错。第二种是容器重启后 IP 变化Nginx 缓存了旧地址。这种情况的特征是环境刚起来好好的中途改了个配置重启了 PHP 容器然后页面全部 502重启 Nginx 就恢复了。解法就是前面写的用resolver 127.0.0.11加变量形式的fastcgi_pass。第三种是请求到了 PHP 但执行失败Nginx 日志里是Primary script unknown。这个错误的本质是 php-fpm 找不到SCRIPT_FILENAME指向的文件。要么是fastcgi_param SCRIPT_FILENAME根本没写要么是 Nginx 的root和 PHP 容器的挂载路径不一致要么是请求的路径在容器里确实不存在。定位方法很简单进 PHP 容器ls一下那个路径看文件在不在。还有一种容易被误判的情况页面返回 502但 Nginx 日志里没有任何 upstream 错误。这时候去看 PHP 容器是不是被 OOM 杀掉了docker compose ps会显示状态异常docker inspect里能看到退出码 137。这种情况通常和max_children设置过大有关。5.2 数据库连接与认证方式踩坑MySQL 8 的认证插件问题坑过很多人。表现是 PHP 报SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client代码里看半天没问题其实是服务端要求用caching_sha2_password而客户端的mysqlnd版本太老不支持。解决办法有两条升级 PHP 版本或者驱动让它支持新认证方式或者在 MySQL 启动参数里加--default-authentication-pluginmysql_native_password让服务端退回到老方式。开发环境我一般直接用第二种省事。生产环境要看服务端版本和客户端驱动能力别盲目照抄。另一个常见问题是字符集。容器里的 MySQL 默认字符集如果没配建库建表可能出现中文乱码。在启动参数里加--character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci连接层面再确保charsetutf8mb4基本就没问题了。还有一个隐蔽的坑是时区数据库里存的时间和页面上显示的时间差八小时八成是 PHP 和 MySQL 时区没对齐两边都设成 UTC 最省心显示的时候在前端或者应用层做转换。5.3 常见问题速查表现象大概率原因处理方式502 Bad Gatewayphp-fpm 未启动、主机名错、Nginx 缓存旧 IP查 PHP 容器状态加 resolver 动态解析404 / File not foundroot 与挂载路径不一致统一挂载点检查 SCRIPT_FILENAMEPrimary script unknownfastcgi_param 未覆盖、文件不存在显式设置 SCRIPT_FILENAME 并进容器确认文件504 Gateway Time-out脚本耗时超过 Nginx 的 fastcgi_read_timeout同时调 Nginx 和 fpm 的 timeout目录写入权限拒绝容器内 uid 与宿主 uid 不一致usermod 调整 uid/gid或构建时传参代码改动不生效OPcache 时间戳校验关闭、挂载路径错validate_timestamps 设 1确认挂载点上传大文件失败client_max_body_size 与 post_max_size 不匹配两边同时调整Xdebug 断点不生效无法解析宿主机、路径映射错、触发方式不对加 extra_hosts检查 idekey 与日志Composer 内存不足memory_limit 或 COMPOSER_MEMORY_LIMIT 太小命令行场景设 COMPOSER_MEMORY_LIMIT-1容器内存被撑爆退出码 137pm.max_children 设置过大按单进程内存反算并发上限表格之外再补两条经验。第一遇到问题先看docker compose logs90% 的线索在这里不要一上来就怀疑代码。第二改动 ini 或者 fpm 配置后必须重启对应容器docker compose restart php改 Nginx 配置可以nginx -s reload或者在容器里nginx -s reload但改挂载文件建议直接重启容器简单可靠。6. 从开发到交付这套环境还能怎么用6.1 多项目并行的隔离方式手上同时跑三个项目时最容易出的问题是端口冲突。每个项目一套 compose都映射 8080第二个起不来。解决办法是每个项目分配不同端口8080、8081、8082数据库端口同理。另一个办法是用 compose 的项目名隔离docker compose -p project-a up -d这样容器名、网络名、卷名都带上前缀互不干扰。网络层面也要注意默认 compose 会创建一个以目录名命名的网络不同项目之间是隔离的互相访问不到这是好事。但如果你想在项目 A 的容器里访问项目 B 的数据库就得手动把容器接到同一个网络上或者干脆不用通过宿主端口访问。开发环境我建议保持隔离减少“一个项目出问题拖累全部”的概率。资源占用也要盯一下。多个项目同时跑内存很容易吃满docker stats看看哪个容器占得多把不用的项目docker compose stop掉。开发机不是服务器别指望它能同时跑五套 MySQL。6.2 别把开发配置原样搬上生产开发环境的配置和生产环境有本质区别这一点要反复强调。开发环境开着display_errors、Xdebug、OPcache 时间戳校验这些在生产上都是性能或安全问题。镜像也一样开发镜像里装了 Xdebug 和各种调试工具体积大、开销高生产镜像应该用多阶段构建只保留运行时需要的东西。一个比较实用的做法是拆成两个 Dockerfile 或者用构建参数区分开发镜像加--build-arg INSTALL_XDEBUG1生产镜像不加。compose 文件也拆成docker-compose.yml和docker-compose.override.yml后者放端口映射、代码挂载、调试变量docker compose up会自动合并生产构建时不带 override 文件就行。这样一套代码两边复用差异集中在少量配置里不容易出错。还有一点是数据。开发环境的数据卷可以随便删生产的数据卷是要备份的。docker compose down -v这条命令我在开发机上用过很多次每次都提醒自己别在错误的目录下执行。养成习惯删数据之前先docker compose ps和docker volume ls确认一下当前在哪个项目里。我个人在这套环境上折腾这么久最大的体会是配置文件要写得“能被别人看懂”而不是“能跑就行”。你写的每一行depends_on、每一个extra_hosts、每一处resolver背后都是一个曾经踩过的坑。把这些坑的解法固化进配置里下次换台机器、换个同事、过半年再回来环境还是那条命令就能起来。另外再分享一个小技巧把常用的几条命令写进项目的Makefile或者一个README的开头make up、make sh、make logs比每次翻历史命令快得多团队里也没人会记错参数。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/23 1:08:05
Hadoop ReduceJoin多表关联实现与数据倾斜优化
2026/10/23 1:03:04
AMD端侧大模型部署实战:路线选择、显存测算与HIP编译避坑
2026/10/23 1:03:04
WPF ListView 行与列背景变色方案详解
2026/10/23 3:33:47
AI Agent落地实践:用OpenClaw搭建自主智能体的完整指南
2026/10/23 3:33:47
SpringBoot+Vue+MySQL选课系统源码解析与实战部署指南
2026/10/23 3:33:47
人工智能到底是什么?机器学习与深度学习的底层逻辑
2026/10/23 3:33:47
机床数据采集实战:多协议兼容与MES系统集成方案
2026/10/23 3:33:47
Android离线背单词App开发:基于Kotlin与Room的数据与记忆算法实践
2026/10/23 3:28:46
Spring Boot+微信小程序:阅读打卡平台设计与实践
2026/10/23 0:02:44
Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战
2026/10/23 0:02:44
数据服务监控运维实战:从指标体系到告警治理与故障排查
2026/10/23 0:02:44
FreeRTOS任务通知实战详解:内核原理、API与避坑指南
2026/10/23 0:52:23
OpenResearch实战:Claude Code、Codex、OpenCode、Cursor 工作流搭建与故障排查
2026/10/22 17:15:21
知识管理核弹:用Obsidian构建第二大脑的工程化方案(TaoToken 统一 Key 接入版)
2026/10/23 0:52:11
金融AI落地指南:从调研报告到技术选型与工程实践
2026/10/22 5:06:15
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/22 2:07:18
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/21 19:01:08
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)