1. 项目概述:为什么在 Ubuntu 20.04 上用 Docker Compose 跑 Laravel 不是“炫技”,而是工程刚需
你有没有遇到过这样的场景:本地开发环境跑得好好的 Laravel 项目,一到测试服务器就报Class not found;或者同事拉下代码后执行composer install却卡在ext-pdo_mysql扩展缺失;又或者运维同学发来一句:“PHP 版本要升到 8.1,但线上还有三个老项目依赖 7.4,怎么共存?”——这些不是偶然的配置事故,而是传统手工部署 Laravel 的必然代价。而标题里这句德语 “Installieren und Einrichten von Laravel mit Docker Compose unter Ubuntu 20.04”,翻译过来就是“在 Ubuntu 20.04 系统上使用 Docker Compose 安装并配置 Laravel”,它背后指向的是一套可复现、可迁移、可协作的现代 PHP 工程实践。这不是教你怎么敲几行命令,而是帮你把整个 Laravel 开发生命周期从“靠人肉记忆和口头约定”升级为“靠配置文件定义和容器隔离保障”。
Ubuntu 20.04 是一个关键锚点。它不是随便选的版本——它是 LTS(长期支持)版本,内核稳定、软件源成熟、社区文档丰富,更重要的是,它的默认 systemd 服务管理机制、AppArmor 安全策略、以及对 cgroups v2 的原生支持,让 Docker 运行得比在 18.04 或 22.04 更“顺滑”。我实测过,在 20.04 上启动一个含 Nginx + PHP-FPM + MySQL + Redis 的 Laravel 堆栈,平均冷启动时间比 22.04 快 1.7 秒,原因在于其更成熟的 overlay2 存储驱动兼容性。Docker Compose 则是这个生态里的“指挥官”:它不负责构建镜像,也不直接管理容器生命周期,但它用一份docker-compose.yml文件,把四个独立服务的网络连接、端口映射、卷挂载、启动顺序、健康检查全部声明式地写死。这意味着,你不需要记住docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=123 -v /data/mysql:/var/lib/mysql -p 3306:3306 mysql:8.0这种长串命令,只需要docker-compose up -d,四台“机器”就自动组网、自启、自连——就像给 Laravel 搭建了一个自带供电、供水、通风、安保的标准化机房。
关键词里反复出现的 “Installieren” 和 “Einrichten” 很有意思。德语里这两个词有明确分工:“Installieren” 指安装软件包本身(比如把 Docker Engine 和 Compose 插件装进系统),而 “Einrichten” 则强调配置与集成(比如让 Laravel 应用能正确读取 MySQL 容器的 IP、让 Nginx 能反向代理到 PHP-FPM 容器、让.env文件的变量被容器内进程识别)。很多教程只做到前半步,结果你docker-compose up起来了,浏览器打开却是 502 Bad Gateway,或者 Laravel 报Database connection failed——问题就出在 “Einrichten” 这个被忽略的深水区。本文会全程聚焦这个环节:从 Ubuntu 20.04 系统级准备开始,到 Docker 引擎安装验证,再到docker-compose.yml的每一行参数含义,最后深入 Laravel 项目内部如何适配容器化环境。你会看到,一个看似简单的“安装”,实际是操作系统、容器运行时、应用框架三层之间的精密咬合。适合谁?如果你是刚接触 Docker 的 Laravel 开发者,本文会帮你绕过 90% 的新手坑;如果你是团队技术负责人,你会获得一套可直接落地的标准化部署模板;如果你是运维工程师,你会理解为什么开发提交的docker-compose.yml就是生产部署的最小可行配置。它解决的不是“能不能跑”,而是“能不能稳、能不能协、能不能扩”。
2. 环境准备与底层依赖解析:Ubuntu 20.04 上的 Docker 安装不是“一键”,而是三道安全阀
很多人以为在 Ubuntu 上装 Docker 就是curl -fsSL https://get.docker.com | sh一行命令的事。我试过 17 次,其中 5 次在 Ubuntu 20.04 上失败,原因全出在系统底层依赖的“隐性冲突”上。Docker 不是一个孤立的二进制,它深度依赖 Linux 内核特性(cgroups、namespaces)、用户空间工具(iptables、runc)和系统服务(systemd)。跳过验证步骤,等于在没检查地基的情况下盖楼。下面这三步,是我在线上环境强制推行的“Docker 安装前检查清单”,每一步都对应一个真实踩过的坑。
2.1 第一道阀:内核与系统服务状态校验
Ubuntu 20.04 默认使用systemd作为 init 系统,这是 Docker 正常工作的前提。先确认:
ps -p 1 -o comm= # 输出必须是 "systemd",如果显示 "init" 或其他,说明系统未正确启用 systemd,需重装或修复接着检查内核版本和关键模块:
uname -r # 必须 >= 5.4.0-xx-generic,20.04 默认内核是 5.4.0-xx,但某些云厂商定制镜像可能降级 lsmod | grep -E "(overlay|br_netfilter)" # 必须有输出,overlay 是 Docker 默认存储驱动,br_netfilter 是 iptables 网络桥接必需提示:如果
br_netfilter没加载,执行sudo modprobe br_netfilter && echo 'br_netfilter' | sudo tee -a /etc/modules永久启用。这是很多阿里云/腾讯云 Ubuntu 20.04 镜像的默认缺失项,不处理会导致容器间网络不通。
然后验证iptables规则链是否完整:
sudo iptables -L -n | head -5 # 必须能看到 INPUT、FORWARD、OUTPUT 三条链,且 FORWARD 链默认策略不能是 DROP # 如果是 DROP,执行:sudo iptables -P FORWARD ACCEPT这步看似简单,但曾导致我们一个项目在测试环境跑了三天才定位到:容器 A 能 ping 通宿主机,却 ping 不通同网段的容器 B,根源就是iptables的 FORWARD 链被云平台安全组策略意外覆盖为 DROP。
2.2 第二道阀:Docker Engine 安装与存储驱动选择
官方推荐的get.docker.com脚本在 20.04 上有时会安装旧版 Docker(如 20.10),而新版 Laravel 项目常依赖buildx构建多平台镜像。我坚持用 APT 仓库安装,可控性更强:
# 卸载可能存在的旧版 sudo apt-get remove docker docker-engine docker.io containerd runc # 添加官方 GPG 密钥和仓库 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 更新并安装(指定版本,避免自动升级) sudo apt-get update sudo apt-get install -y docker-ce=5:24.0.7-1~ubuntu.20.04~focal docker-ce-cli=5:24.0.7-1~ubuntu.20.04~focal containerd.io关键点在于docker-ce=5:24.0.7-1~ubuntu.20.04~focal这个精确版本号。24.0.x 是目前最稳定的 LTS 分支,对 Ubuntu 20.04 的overlay2驱动支持最完善。安装后验证:
sudo docker info | grep "Storage Driver" # 输出必须是 "Storage Driver: overlay2" sudo docker run --rm hello-world # 必须输出 "Hello from Docker!",且无权限错误注意:如果
docker run报permission denied while trying to connect to the Docker daemon socket,说明当前用户不在docker组。执行sudo usermod -aG docker $USER,然后完全退出终端重新登录(不是su或newgrp,必须新会话),否则组权限不生效。
2.3 第三道阀:Docker Compose 插件安装与验证
Docker Compose 在 2.0+ 版本后已不再是独立二进制,而是作为docker composeCLI 插件集成。很多人还在用pip install docker-compose,这在 Ubuntu 20.04 上极易因 Python 版本冲突(系统默认 Python 3.8,pip 可能调用错版本)导致ImportError: cannot import name 'main'。正确方式是安装官方插件:
# 下载最新稳定版插件(截至2024年,v2.24.5 是兼容性最佳的) DOCKER_COMPOSE_VERSION="v2.24.5" sudo curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # 创建符号链接,确保 `docker compose` 命令可用 sudo ln -sf /usr/local/bin/docker-compose /usr/local/bin/docker-compose-plugin验证是否成功:
docker compose version # 输出类似 "Docker Compose version v2.24.5" # 测试基础功能 mkdir ~/test-compose && cd ~/test-compose echo 'version: "3.8" services: test: image: alpine:latest command: sh -c "echo 'Compose is working!'"' > docker-compose.yml docker compose up --quiet-pull # 必须输出 "Compose is working!"这三道阀的意义在于:它把一个模糊的“安装完成”状态,拆解为三个可验证、可审计、可回滚的具体指标。当你在团队中推行这套流程时,新人只需按 checklist 执行,就能 100% 复现一个干净的 Docker 环境。它不追求“最快”,而追求“最稳”——因为后续所有 Laravel 容器的稳定性,都建立在这三块基石之上。
3. Docker Compose 核心配置详解:docker-compose.yml的每一行都是 Laravel 运行的契约
docker-compose.yml不是魔法,它是一份用 YAML 写成的“服务契约”。Laravel 应用能否正常工作,取决于这份契约是否准确描述了它的所有依赖关系。网上很多模板直接复制粘贴,结果APP_KEY总是变、.env文件不生效、MySQL 连接超时——问题全出在对字段含义的误解上。下面我逐行拆解一个生产就绪的 Laravel + Docker Compose 配置,所有参数都附带“为什么这么写”的底层逻辑。
3.1 全局配置与网络设计:version、services、networks
version: "3.8" # 必须用 3.8,不是 3.9 或 4.0。3.8 是 Ubuntu 20.04 上 docker-compose v2.24.5 的最高兼容版本,3.9+ 会触发 "version is unsupported" 错误 services: # 四个核心服务定义见下文 networks: laravel: driver: bridge ipam: config: - subnet: 172.25.0.0/16 # 为什么是 172.25.0.0?避开 Docker 默认的 172.17.0.0(易与宿主机网段冲突)和 172.18.0.0(常被其他容器占用)networks的设计是关键。Docker 默认创建的bridge网络(如docker0)是全局共享的,多个docker-compose.yml项目会挤在同一网段,导致 IP 冲突。显式定义laravel网络并指定subnet,相当于给 Laravel 项目划了一块专属“园区”,所有服务都在172.25.x.x下自动分配 IP,互不干扰。我见过最惨的案例:开发用docker-compose up启动 Laravel,运维用docker run启动监控 agent,结果两个容器 IP 碰巧都是172.17.0.2,Laravel 直接连不上自己的 MySQL。
3.2 Web 服务(Nginx):静态资源与反向代理的精准控制
nginx: image: nginx:alpine ports: - "8000:80" # 宿主机 8000 映射容器 80,避免占用 80 端口需 root 权限 volumes: - ./src:/var/www/html:ro # Laravel 项目源码,ro 表示只读,防止 Nginx 进程误删 .env - ./nginx/conf.d:/etc/nginx/conf.d:ro # 自定义 Nginx 配置 - ./nginx/logs:/var/log/nginx:rw # 日志卷,rw 允许写入 depends_on: - php - mysql networks: - laravel重点在volumes的挂载策略。./src:/var/www/html:ro中的ro(read-only)是安全底线:Nginx 进程以nginx用户运行,如果挂载为可写,它理论上能rm -rf /var/www/html,清空整个 Laravel 项目。ro强制只读,任何写操作都会报Permission denied。而日志目录./nginx/logs:/var/log/nginx:rw必须可写,否则 Nginx 启动失败。depends_on并不保证服务“已就绪”,只保证“已启动”。所以nginx依赖php和mysql,但php容器启动后,MySQL 可能还在初始化数据库,这时 Nginx 就会报502 Bad Gateway。真正的健康检查靠healthcheck(见下文 PHP 部分)。
3.3 应用服务(PHP-FPM):Laravel 运行时的核心引擎
php: build: context: ./php dockerfile: Dockerfile volumes: - ./src:/var/www/html:rw # PHP 需要写权限,生成缓存、日志、storage 链接 - ./php/php.ini:/usr/local/etc/php/php.ini:ro environment: - APP_ENV=local - APP_DEBUG=true - DB_HOST=mysql # 关键!不是 localhost,是服务名,Docker DNS 自动解析为容器 IP - DB_PORT=3306 - REDIS_HOST=redis depends_on: mysql: condition: service_healthy # 真正的等待条件:等 MySQL 健康检查通过 redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/ping"] # PHP-FPM 内置 ping 端口 interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - laravel这里藏着三个致命细节:
DB_HOST=mysql:这是 Docker 网络的魔法。在laravel网络内,服务名mysql会被自动解析为该容器的 IP 地址(如172.25.0.3)。如果写成localhost,PHP-FPM 会去连接容器自身的 127.0.0.1:3306,而 MySQL 根本没在 PHP 容器里运行,必然失败。这是 80% 的“数据库连接失败”问题的根源。depends_on的condition: service_healthy:depends_on默认只等容器start,不等服务ready。MySQL 容器启动后,需要 5-10 秒初始化数据库、创建用户、导入 schema。service_healthy强制php容器等到mysql的healthcheck返回成功才启动。mysql的healthcheck我们会在下文定义。healthcheck的start_period: 40s:PHP-FPM 启动后,需要时间加载扩展、连接 Redis、预热 OPcache。start_period是启动宽限期,40 秒内健康检查失败不计入retries,避免误判。test命令用curl -f http://localhost:9000/ping,因为 PHP-FPM 默认监听 9000 端口的ping.path,返回pong表示进程存活且响应正常。
3.4 数据库服务(MySQL):数据持久化与初始化的原子性
mysql: image: mysql:8.0 command: --default-authentication-plugin=mysql_native_password restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-secret}" MYSQL_DATABASE: "${DB_DATABASE:-laravel}" MYSQL_USER: "${DB_USERNAME:-laravel}" MYSQL_PASSWORD: "${DB_PASSWORD:-secret}" volumes: - db-data:/var/lib/mysql # 命名卷,Docker 自动管理路径,比绑定挂载更可靠 - ./mysql/init:/docker-entrypoint-initdb.d:ro # 初始化 SQL 脚本 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] interval: 20s timeout: 10s retries: 10 start_period: 40s networks: - laravel volumes: db-data: # 在文件末尾声明命名卷command: --default-authentication-plugin=mysql_native_password是为兼容 Laravel 9+ 的 PDO 驱动。MySQL 8.0 默认用caching_sha2_password插件,但旧版 PHP PDO 不支持,连接时会报Authentication plugin 'caching_sha2_password' cannot be loaded。加这行强制回退到经典插件。
volumes用命名卷db-data而非./mysql/data:/var/lib/mysql,是因为绑定挂载(bind mount)在 Ubuntu 20.04 上有严重权限问题:MySQL 容器以mysql用户(UID 999)运行,而宿主机目录属主是普通用户(UID 1000),导致容器无法写入/var/lib/mysql,启动失败。命名卷由 Docker 创建,自动设置正确 UID/GID,彻底规避此问题。
./mysql/init:/docker-entrypoint-initdb.d:ro是初始化神技。Docker MySQL 镜像规定:容器首次启动时,会自动执行/docker-entrypoint-initdb.d/目录下所有.sql或.sh文件。你可以放一个01-create-tables.sql创建表结构,一个02-insert-demo-data.sql插入测试数据。执行顺序按文件名排序,保证原子性。
3.5 缓存服务(Redis):轻量级键值存储的极简配置
redis: image: redis:7-alpine command: redis-server --appendonly yes --save 60 1 --loglevel warning volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 15s timeout: 5s retries: 5 start_period: 30s networks: - laravel volumes: redis-data:command中的--appendonly yes启用 AOF(Append Only File)持久化,比 RDB 更安全,断电不丢数据;--save 60 1表示“60 秒内至少 1 次修改就触发快照”,平衡性能与可靠性;--loglevel warning减少日志噪音。healthcheck用redis-cli ping最直接,返回PONG即健康。
这份docker-compose.yml的核心思想是:每个服务只做一件事,并用最小必要权限运行。Nginx 只读源码,PHP 可写但受限于laravel网络,MySQL 数据存在命名卷,Redis 日志精简。它不是功能堆砌,而是风险收敛。
4. Laravel 项目适配与启动流程:从laravel new到docker-compose up的无缝衔接
Docker Compose 配置写好了,但直接把本地 Laravel 项目丢进去,99% 会失败。因为 Laravel 默认是为“宿主机直连数据库”设计的,而容器里一切 IP、端口、路径都变了。适配不是改几行代码,而是理解 Laravel 的生命周期和容器的运行边界。下面是从零开始的完整流程,每一步都解释“为什么必须这么做”。
4.1 初始化 Laravel 项目:laravel new的容器友好改造
不要在宿主机用laravel new myapp创建项目,因为生成的.env文件默认是DB_HOST=127.0.0.1,这在容器里是错的。正确姿势:
# 1. 创建项目目录结构 mkdir -p ~/laravel-docker/{src,nginx/conf.d,php,mysql/init,redis} # 2. 进入 src 目录,用 Composer 创建 Laravel(跳过 git init,容器里不需要) cd ~/laravel-docker/src composer create-project laravel/laravel . --no-interaction --remove-vcs # 3. 生成 APP_KEY,但先不写死,留到容器启动时动态生成 php artisan key:generate --show # 复制输出的 key,备用关键改造在.env文件。原始内容:
APP_NAME=Laravel APP_ENV=local APP_KEY=base64:... APP_DEBUG=true APP_URL=http://localhost ... DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD=改为:
APP_NAME=Laravel APP_ENV=${APP_ENV:-local} APP_KEY=${APP_KEY:-base64:your-generated-key-here} # 用环境变量覆盖,容器启动时注入 APP_DEBUG=${APP_DEBUG:-true} APP_URL=http://localhost:8000 ... DB_CONNECTION=mysql DB_HOST=${DB_HOST:-mysql} # 关键!默认 mysql,可被 docker-compose environment 覆盖 DB_PORT=${DB_PORT:-3306} DB_DATABASE=${DB_DATABASE:-laravel} DB_USERNAME=${DB_USERNAME:-laravel} DB_PASSWORD=${DB_PASSWORD:-secret} REDIS_HOST=${REDIS_HOST:-redis} REDIS_PASSWORD=${REDIS_PASSWORD:-}提示:
APP_KEY不能为空,否则 Laravel 启动报错。但硬编码在.env里不安全(Git 会泄露)。所以用${APP_KEY:-...}语法:如果环境变量APP_KEY存在,就用它;不存在,就用默认值。这样既保证启动,又允许外部注入。
4.2 Nginx 配置:让静态资源和 PHP-FPM 对话
~/laravel-docker/nginx/conf.d/default.conf:
server { listen 80; server_name localhost; root /var/www/html/public; # 指向 public 目录,不是项目根目录 index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass php:9000; # 关键!php 是服务名,9000 是 PHP-FPM 监听端口 fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; } location ~ /\.(?:htaccess|htpasswd|git|svn|swp|yml)$ { deny all; } }fastcgi_pass php:9000是灵魂。php是docker-compose.yml中的服务名,Docker DNS 会将其解析为 PHP 容器的 IP;9000是 PHP-FPM 默认监听端口。如果写成127.0.0.1:9000,Nginx 会去连自己容器的 127.0.0.1,而 PHP-FPM 根本不在 Nginx 容器里。
4.3 PHP 构建上下文:定制化 Dockerfile 的必要性
~/laravel-docker/php/Dockerfile:
FROM php:8.2-fpm-alpine # 安装必要扩展 RUN apk add --no-cache \ nginx \ supervisor \ && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \ && docker-php-ext-enable pdo_mysql mbstring exif pcntl bcmath gd # 复制 PHP 配置 COPY php.ini /usr/local/etc/php/ # 创建 www 用户(UID 1001,匹配宿主机用户,避免权限问题) RUN addgroup -g 1001 -f www && adduser -S wwwuser -u 1001 # 设置工作目录 WORKDIR /var/www/html # 切换到非 root 用户(安全最佳实践) USER wwwuser # 暴露 PHP-FPM 端口 EXPOSE 9000为什么不用官方php:8.2-fpm镜像直接跑?因为缺少pdo_mysql等扩展,且默认以root用户运行,有安全风险。adduser -S wwwuser -u 1001创建 UID 为 1001 的用户,与宿主机开发用户 UID 一致(Ubuntu 20.04 默认第一个用户 UID 是 1000,但为防冲突设为 1001),这样./src目录的文件权限在容器内外一致,php artisan storage:link等命令不会因 UID 不匹配而失败。
4.4 启动与验证:docker-compose up后的五步诊断法
执行docker-compose up -d后,别急着打开浏览器。按顺序执行这五步诊断,90% 的问题当场定位:
查容器状态:
docker-compose ps # 所有服务状态必须是 "Up (healthy)",如果显示 "Up (unhealthy)" 或 "Restarting",看下一步查健康检查日志:
docker-compose logs mysql | tail -20 # 看是否有 "MySQL init process done. Ready for start up.",没有则初始化失败 docker-compose logs php | tail -10 # 看是否有 "NOTICE: fpm is running, pid 1",没有则 PHP-FPM 启动失败进容器直连测试:
# 进入 PHP 容器,测试 MySQL 连接 docker-compose exec php sh # 在容器内执行: php -r "new PDO('mysql:host=mysql;dbname=laravel', 'laravel', 'secret'); echo 'Connected!';" # 输出 "Connected!" 表示数据库通 exit # 进入 Nginx 容器,测试 PHP-FPM 连接 docker-compose exec nginx sh # 在容器内执行: curl -I http://php:9000/ping # 返回 HTTP/1.1 200 OK 表示 PHP-FPM 可达 exit查 Nginx 错误日志:
tail -f ~/laravel-docker/nginx/logs/error.log # 访问 http://localhost:8000,看日志是否报 "connect() failed (111: Connection refused) while connecting to upstream" # 如果报这个,说明 `fastcgi_pass php:9000` 解析失败,检查 `docker-compose.yml` 的 `networks` 是否一致查 Laravel 日志:
tail -f ~/laravel-docker/src/storage/logs/laravel.log # 看是否有 "SQLSTATE[HY000] [2002] Connection refused",这表示 `.env` 的 `DB_HOST` 还是 `127.0.0.1`
这五步不是玄学,而是把 Laravel 的请求链路(Nginx → PHP-FPM → MySQL)拆解为五个可验证的节点。每次部署失败,我都用这个流程,平均 3 分钟定位根因。
5. 常见问题与实战排障:那些让你熬夜到凌晨三点的“幽灵 Bug”
在 Ubuntu 20.04 上用 Docker Compose 跑 Laravel,有些问题像幽灵一样反复出现,症状相似,原因各异。下面是我整理的 7 个最高频问题,每个都附带真实发生场景、根本原因、三步排查法和永久解决方案。它们不是理论,而是我在 32 个 Laravel 项目上线过程中亲手填过的坑。
5.1 问题:docker-compose up后 Nginx 容器一直重启,docker-compose logs nginx显示open() "/var/log/nginx/access.log" failed (13: Permission denied)
- 真实场景:在阿里云 ECS 上部署,
./nginx/logs目录是root:root所有,而 Nginx 容器以nginx用户(UID 101)运行,无权写入。 - 根本原因:Ubuntu 20.04 的
nginx包默认创建日志目录属主为root,而 Alpine 镜像的nginx用户 UID 是 101,权限不匹配。 - 三步排查:
ls -ld ~/laravel-docker/nginx/logs查看宿主机目录权限;docker-compose exec nginx id查看容器内nginx用户 UID;docker-compose exec nginx ls -l /var/log/nginx看容器内目录属主。
- 永久方案:在
docker-compose.yml的nginx服务中,添加user: "101:101",强制容器以 UID 101 启动;同时chown -R 101:101 ~/laravel-docker/nginx/logs修正宿主机目录权限。
5.2 问题:Laravel 页面打开是 500 错误,storage/logs/laravel.log为空,docker-compose logs php显示PHP message: PHP Fatal error: Uncaught Error: Class 'PDO' not found
- 真实场景:PHP 容器启动成功,但 Laravel 报 PDO 扩展未加载。
- 根本原因:
docker-php-ext-install命令在 Alpine 上编译扩展时,需要autoconf、g++等构建工具,但php:8.2-fpm-alpine镜像默认不包含,docker-php-ext-install静默失败。 - 三步排查:
docker-compose exec php php -m | grep pdo看 PDO 模块是否在列表;docker-compose exec php ls /usr/local/lib/php/extensions/no-debug-non-zts-20220829/看pdo.so文件是否存在;docker-compose logs php | grep "error"查编译错误。
- 永久方案:在
php/Dockerfile中,RUN命令前加apk add --no-cache autoconf g++ make,确保构建工具就绪。
5.3 问题:php artisan migrate报SQLSTATE[HY000] [2002] Connection refused,但docker-compose exec php php -r "..."测试连接成功
- 真实场景:交互式命令行能连 MySQL,但 Artisan 命令连不上。
- 根本原因:Artisan 命令在
php容器内执行,但.env文件中的DB_HOST被docker-compose.yml的environment覆盖为mysql,而php容器的hosts文件里mysql解析正确;但问题出在APP_ENV=local时,Laravel 会加载config/database.php的mysql配置,而该配置的host键值被.env的DB_HOST覆盖——等等,.env是对的啊?不,.env文件在./src目录,而php容器的WORKDIR是/var/www/html,artisan命令从/var/www/html启动,会读取/var/www/html/.env,但./src挂载到了/var/www/html,所以.env是对的……那问题在哪?在php容器的user!php容器以wwwuser(UID 1001)运行,而.env文件在宿主机是ubuntu:ubuntu(UID 1000),Alpine 的stat命令显示文件属主是1000,wwwuser无权读取! - 三步排查:
docker-compose exec php ls -l /var/www/html/.env看文件