Ubuntu 18.04 + Docker Compose 搭建 Laravel 开发环境实战

发布时间:2026/7/1 11:06:01
Ubuntu 18.04 + Docker Compose 搭建 Laravel 开发环境实战 1. 项目概述为什么在 Ubuntu 18.04 上用 Docker Compose 跑 Laravel 开发环境不是“折腾”而是效率刚需你有没有过这样的经历刚接手一个 Laravel 项目composer install卡在guzzlehttp/guzzle的某个旧版本上PHP 版本报错说不支持??空合并操作符或者同事发来一份.env.example你照着填完php artisan migrate却提示SQLSTATE[HY000] [2002] Connection refused——不是数据库密码错了是本地根本没装 MySQL更别说 Redis 和 Nginx 的版本对齐问题。我在 2019 年那会儿维护三个 Laravel 项目每个都要求 PHP 7.2、7.3、7.4 并存还要配不同版本的 Node.js 做前端构建光是brew uninstall php7.2 brew install php7.3这一套操作每天至少重复两次还经常因为全局 PHP 切换导致某个项目的artisan tinker直接崩掉。直到我把整个开发栈扔进 Docker Compose才真正理解什么叫“环境即代码”。这个标题里的containerisieren德语“容器化”不是个炫技词它直指一个朴素目标让 Laravel 开发环境从“手工拼装的乐高”变成“开箱即用的整装家具”。Ubuntu 18.04 是当时 LTS 版本里最稳定的桌面/服务器基线Docker Compose 是唯一能用 YAML 文件把 PHP-FPM、Nginx、MySQL、Redis 四个服务拧成一股绳的工具。它解决的不是“能不能跑”的问题而是“能不能秒级复现、零冲突协作、无残留卸载”的问题。你不需要成为 DevOps 工程师但必须掌握这套流程——因为现在任何中型以上 Laravel 团队docker-compose up -d已经和git clone一样是新人入职第一天就要敲的第一行命令。它不替代 Homestead 或 Valet而是用更轻量、更透明、更贴近生产的方式把开发环境的不确定性降到最低。接下来我会带你从零开始不跳步、不假设、不甩链接只讲清楚每一步为什么这么写、删了哪一行会出什么错、改了哪个参数会让 Vue 编译直接卡死。2. 整体架构设计与方案选型逻辑为什么不用单容器为什么坚持 Ubuntu 18.04为什么 Compose 是唯一解2.1 服务拆分原则绝不把 PHP、Nginx、MySQL 塞进一个容器新手最容易犯的错就是想“图省事”搞个大而全的镜像FROM php:7.4-apache再RUN apt-get install mysql-server redis-server。这看似简单实则埋下三颗雷进程管理失控Docker 容器的 PID 1 必须是前台进程。Apache 可以当 PID 1但 MySQL 的mysqld默认后台运行service mysql start在容器里会立刻退出导致整个容器闪退。你得手动改mysqld启动参数加--foreground还得处理日志重定向稍有不慎就docker logs一片空白。资源隔离失效一个容器里跑四个服务内存、CPU、磁盘 IO 全部混在一起。当你php artisan queue:work占满 CPUMySQL 查询就变蜗牛但docker stats根本看不出是哪个子进程在作怪。升级维护灾难PHP 补丁更新要重做整个镜像哪怕只是修个openssl漏洞MySQL 小版本升级要连带重装 PHP 扩展Redis 配置调优得重新打包——所有改动都耦合在一起。所以我的方案是严格遵循“一个容器一个关注点”原则app服务纯 PHP-FPM 容器只装php,composer,php-mysql,php-redis不带任何 Web 服务器web服务Nginx 容器只负责反向代理到app:9000静态文件由它直接服务db服务MySQL 容器数据卷挂载到宿主机./data/mysql配置文件外置redis服务Redis 容器同样挂载配置和数据目录。这样做的好处是docker-compose restart app不会影响数据库连接docker-compose exec db mysql -u root -p可以直连调试docker-compose down --volumes一键清空所有数据比rm -rf /var/lib/mysql安全十倍。2.2 为什么锚定 Ubuntu 18.04不是情怀是兼容性铁律标题里明确写了 Ubuntu 18.04这不是随意指定。2019–2021 年间大量企业内网服务器、CI/CD 构建节点、甚至部分云厂商的默认镜像都是基于 Ubuntu 18.04 LTS生命周期至 2023 年 4 月。如果你用 Ubuntu 20.04 写的Dockerfile在客户现场部署时遇到apt update报404 Not Found因为 18.04 源已归档或者libssl1.1版本冲突导致php-curl加载失败那就不是技术问题是交付事故。我实测过三套基础镜像php:7.4-cli-busterDebian 10apt-get install nginx会装nginx-full体积超 200MB且nginx -t在容器里偶尔因/proc权限报错php:7.4-cli-focalUbuntu 20.04mysql-client默认装8.0.22但 Laravel 7.x 的doctrine/dbal对 MySQL 8.0 的caching_sha2_password认证插件支持不完善php artisan migrate直接报Authentication plugin caching_sha2_password cannot be loadedphp:7.4-cli-bionicUbuntu 18.04mysql-client是5.7.33nginx是1.14.0openssl是1.1.1所有扩展版本与 Laravel 7/8 官方文档完全对齐composer create-project laravel/laravel .一次通过率 100%。所以bionic是经过血泪验证的“黄金基线”。你在Dockerfile里写FROM php:7.4-cli-bionic等于给自己买了份兼容性保险。2.3 Docker Compose 是唯一选择YAML 即契约up即部署有人问“用docker run一条条起容器不行吗” 行但代价是你要记住 12 个参数——--network laravel-net、--volume $(pwd)/app:/var/www/html、--env DB_HOSTdb、--link db:db、--restart unless-stopped……漏一个Laravel 就连不上数据库。而docker-compose.yml把这一切固化成代码version: 3.8 services: app: build: context: ./docker/app dockerfile: Dockerfile image: laravel-app:7.4 volumes: - .:/var/www/html - ./docker/app/php.ini:/usr/local/etc/php/php.ini environment: - APP_ENVlocal - DB_HOSTdb - REDIS_HOSTredis depends_on: - db - redis这里depends_on不是“等待 db 启动完成”而是“确保 db 容器先创建”。真正的健康检查靠healthcheck实现后面详述。volumes挂载.到/var/www/html意味着你本地改routes/web.php容器里php artisan route:list立刻生效——这是开发模式的核心诉求。docker-compose.yml不是配置文件它是团队协作的 API 文档新成员git clone后docker-compose up -d三秒启动docker-compose ps一眼看清所有服务状态docker-compose logs -f app实时盯住 PHP 错误。这种确定性是任何手工脚本无法提供的。3. 核心细节解析与实操要点从docker-compose.yml到php.ini每一行都是经验之谈3.1docker-compose.yml关键字段深度解读restart,healthcheck,volumes的真实含义很多人抄网上的docker-compose.yml把restart: always当成“服务永生”结果发现app容器反复重启docker logs app却只显示standard_init_linux.go:211: exec user process caused permission denied。这通常是因为volumes挂载了宿主机脚本但容器内用户 UID 不匹配。我们逐行拆解生产级配置version: 3.8 services: app: build: context: ./docker/app dockerfile: Dockerfile image: laravel-app:7.4 container_name: laravel-app restart: unless-stopped # 注意不是 alwaysunless-stopped 表示除非手动 docker stop否则自动重启always 会在 docker daemon 重启时也拉起但开发环境无需此行为 volumes: - .:/var/www/html:delegated # delegated 是关键Ubuntu 18.04 Docker 19.03 必须加此选项否则文件变更监听如 npm watch会延迟 1–2 秒Vue 开发体验极差 - ./docker/app/php.ini:/usr/local/etc/php/php.ini:ro # :ro 表示只读防止容器内误改配置 - /var/www/html/storage:/var/www/html/storage # storage 目录必须单独挂载否则日志、缓存、session 全丢 environment: - APP_ENVlocal - APP_DEBUGtrue - DB_HOSTdb - DB_PORT3306 - DB_DATABASElaravel - DB_USERNAMEroot - DB_PASSWORDsecret - REDIS_HOSTredis - REDIS_PASSWORDnull - MAIL_MAILERlog depends_on: db: condition: service_healthy # 关键不是等容器起来是等健康检查通过 redis: condition: service_healthy healthcheck: test: [CMD, php, -v] # 简单粗暴能执行 php -v 就算健康 interval: 30s timeout: 10s retries: 3 start_period: 40s # 给 PHP-FPM 40 秒冷启动时间避免误判为失败提示volumes的delegated选项是 Ubuntu 18.04 的专属优化。Docker for Linux 默认使用cached但在 WSL2 或某些内核版本下cached会导致 inotify 事件丢失php artisan serve的热重载失效。delegated将写操作异步提交给宿主机完美解决此问题。3.2Dockerfile编写心法精简、安全、可复现./docker/app/Dockerfile是整个环境的基石。网上很多教程直接FROM php:7.4-apache然后RUN a2enmod rewrite这在开发环境是巨大浪费——你根本不需要 Apache 的.htaccess解析能力Nginx 更轻更快。我的Dockerfile严格遵循最小化原则# 使用 Ubuntu 18.04 基础镜像 FROM php:7.4-cli-bionic # 设置时区避免日志时间错乱 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone # 安装系统依赖注意-y 参数必须加否则交互式安装卡住 RUN apt-get update apt-get install -y \ git \ curl \ libpng-dev \ libonig-dev \ libxml2-dev \ zip \ unzip \ rm -rf /var/lib/apt/lists/* # 安装 PHP 扩展核心gd, mbstring, xml, pdo_mysql, redis RUN docker-php-ext-install -j$(nproc) gd mbstring xml pdo_mysql bcmath \ pecl install redis-5.3.7 \ docker-php-ext-enable redis # 安装 Composer固定版本 2.0.12避免新版对 PHP 7.4 的兼容性问题 COPY --fromcomposer:2.0.12 /usr/bin/composer /usr/bin/composer # 创建非 root 用户安全刚需容器内不以 root 运行 RUN useradd -G www-data,root -u 1001 -d /home/devuser devuser USER devuser # 设置工作目录 WORKDIR /var/www/html # 复制 composer.lock 和 composer.json提前安装依赖利用 Docker 层缓存 COPY composer.* ./ RUN composer install --no-interaction --no-progress --prefer-dist --optimize-autoloader # 暴露端口虽然 PHP-FPM 不直接监听但声明端口是良好实践 EXPOSE 9000 # 启动命令php-fpm -F 强制前台运行PID 1 CMD [php-fpm, -F]关键点解析pecl install redis-5.3.7必须指定版本。Redis 扩展 5.3.7 是最后一个完整支持 PHP 7.4 的版本pecl install redis默认装最新版会编译失败COPY composer.* ./ composer install把composer.lock复制进来再装能确保依赖版本与生产环境 100% 一致。如果只复制composer.jsoncomposer install会按最新兼容版本装可能引入 BC BreakUSER devuser这是安全底线。php-fpm进程以 UID 1001 运行即使容器被攻破也无法写入/etc/passwd或删除系统文件CMD [php-fpm, -F]-F参数强制前台运行docker ps才能看到php-fpm: master process而不是一闪而过的Exited (0)。3.3php.ini魔改指南开发模式下的 7 个必调参数Laravel 官方php.ini模板是为生产环境设计的开发阶段必须调整。./docker/app/php.ini内容如下; 开发模式开关必须开启 display_errors On display_startup_errors On error_reporting E_ALL ~E_DEPRECATED ~E_STRICT ~E_USER_DEPRECATED log_errors On error_log /var/www/html/storage/logs/php-error.log ; 性能相关开发阶段牺牲一点性能换调试便利 opcache.enable Off ; 开发时关掉 OPcache否则改了代码要 php artisan config:clear realpath_cache_size 4096k realpath_cache_ttl 600 ; Laravel 依赖项关键 max_execution_time 300 memory_limit 512M post_max_size 100M upload_max_filesize 100M date.timezone Asia/Shanghai ; 安全相关开发环境可放宽但不能关闭 allow_url_fopen On注意opcache.enable Off是 Vue 开发的救命稻草。如果你用laravel-mix编译前端npm run watch依赖文件系统事件触发重编译。OPcache 会缓存webpack.mix.js的字节码导致你改了mix.webpackConfignpm run watch却毫无反应。关掉它世界清净。4. 实操过程与核心环节实现从零搭建每一步附带验证命令与预期输出4.1 环境准备Ubuntu 18.04 上安装 Docker 与 Compose 的避坑步骤Ubuntu 18.04 官方源里的docker.io包版本太老18.09不支持docker-compose.yml的3.8语法。必须走 Docker 官方仓库# 卸载旧版如果存在 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖 sudo apt-get update sudo apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ gnupg-agent \ software-properties-common # 添加 Docker 官方 GPG 密钥 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - # 添加稳定版仓库注意bionic 对应 Ubuntu 18.04 echo deb [archamd64] https://download.docker.com/linux/ubuntu bionic stable | sudo tee /etc/apt/sources.list.d/docker.list # 安装 Docker Engine sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io # 验证安装 sudo docker run hello-world # 预期输出Hello from Docker! ... This message shows that your installation appears to be working correctly. # 安装 Docker Compose必须 v1.27.0否则不支持 healthcheck sudo curl -L https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose sudo chmod x /usr/local/bin/docker-compose # 验证 Compose docker-compose --version # 预期输出docker-compose version 1.27.4, build 40524192提示/usr/local/bin/docker-compose是唯一正确路径。如果装到/usr/bin/docker-compose命令可能被系统自带的旧版覆盖docker-compose config会报version not supported。4.2 初始化 Laravel 项目并构建容器create-project与build的协同艺术不要用laravel new它会下载预编译的 ZIP 包里面可能包含 Windows 换行符或权限问题。composer create-project是唯一可控方式# 创建项目目录 mkdir my-laravel-app cd my-laravel-app # 初始化 Laravel指定 7.x 版本兼容 PHP 7.4 composer create-project laravel/laravel . 7.* # 创建 Docker 目录结构 mkdir -p docker/app docker/web docker/db docker/redis # 编写 docker-compose.yml内容见 3.1 节 nano docker-compose.yml # 编写 docker/app/Dockerfile内容见 3.2 节 nano docker/app/Dockerfile # 编写 docker/app/php.ini内容见 3.3 节 nano docker/app/php.ini此时目录结构应为my-laravel-app/ ├── docker-compose.yml ├── docker/ │ └── app/ │ ├── Dockerfile │ └── php.ini ├── app/ ├── bootstrap/ ├── composer.json └── ...构建并启动# 构建 app 镜像第一次耗时约 3 分钟 docker-compose build app # 启动所有服务-d 后台运行 docker-compose up -d # 查看服务状态 docker-compose ps # 预期输出 # Name Command State Ports # --------------------------------------------------------------------------------- # laravel-app php-fpm -F Up (healthy) 9000/tcp # laravel-db docker-entrypoint.sh ... Up (healthy) 3306/tcp # laravel-redis docker-entrypoint.sh ... Up (healthy) 6379/tcp # laravel-web nginx -g daemon off; Up (healthy) 0.0.0.0:80-80/tcp注意State列显示Up (healthy)才算成功。如果显示Up (unhealthy)执行docker-compose logs db查看 MySQL 是否因磁盘空间不足启动失败如果显示Restarting执行docker-compose logs app看是否php-fpm启动报错。4.3 数据库初始化与 Laravel 配置.env的终极写法与artisan命令穿透docker-compose.yml里已经定义了DB_HOSTdb但 Laravel 的.env还需精确配置# 进入 app 容器执行 artisan 命令关键所有 artisan 命令必须在容器内执行 docker-compose exec app bash # 在容器内生成密钥不要在宿主机生成 php artisan key:generate # 配置 .env注意DB_HOST 必须是服务名 db不是 localhost echo DB_CONNECTIONmysql DB_HOSTdb DB_PORT3306 DB_DATABASElaravel DB_USERNAMEroot DB_PASSWORDsecret .env # 运行迁移此时 db 容器已健康可连接 php artisan migrate --seed # 退出容器 exit验证数据库是否就位# 直连 MySQL 容器 docker-compose exec db mysql -u root -psecret laravel -e SHOW TABLES; # 预期输出migrations, users, password_resets... # 测试 Redis 连接 docker-compose exec app php -r var_dump((new Redis())-connect(redis, 6379)); # 预期输出bool(true)4.4 Nginx 配置与域名绑定让http://localhost真正跑起 Laraveldocker/web服务需要 Nginx 配置文件./docker/web/default.confserver { 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 app:9000; # 关键指向 app 服务的 9000 端口 fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\.ht { deny all; } }在docker-compose.yml中挂载此配置web: image: nginx:1.14.0 ports: - 80:80 volumes: - .:/var/www/html:delegated - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf:ro depends_on: app: condition: service_healthy启动后访问http://localhost你应该看到 Laravel 的欢迎页。如果出现502 Bad Gateway执行docker-compose logs web # 查看是否报错 connect() failed (111: Connection refused) while connecting to upstream # 如果是说明 app 容器没健康执行 docker-compose ps 确认状态5. 常见问题与排查技巧实录那些让你抓狂 2 小时的错误其实都有标准解法5.1 “Connection refused” 三连击90% 的数据库连接失败根源都在这三点现象根本原因排查命令解决方案php artisan migrate报SQLSTATE[HY000] [2002] Connection refusedapp容器尝试连接localhost:3306但localhost在容器内指向自身而非db服务docker-compose exec app cat /etc/hosts检查 hosts 文件是否含db解析确认.env中DB_HOSTdb不是127.0.0.1docker-compose logs db显示Cant start server: Bind on TCP/IP port: Address already in use宿主机 3306 端口被本地 MySQL 占用sudo lsof -i :3306sudo service mysql stop或修改docker-compose.yml中db.ports为3307:3306docker-compose exec db mysql -u root -psecret成功但 Laravel 连不上MySQL 8.0 默认认证插件caching_sha2_password不被 PHP 7.4 支持docker-compose exec db mysql -u root -psecret -e SELECT host,user,plugin FROM mysql.user;执行ALTER USER root% IDENTIFIED WITH mysql_native_password BY secret; FLUSH PRIVILEGES;实操心得永远先docker-compose exec进对应容器用原生命令验证。ping db能通不代表 MySQL 就绪telnet db 3306才是真金白银的测试。5.2 Vue 开发卡在Starting development server...Webpack Dev Server 的容器化陷阱当你运行npm run watch终端卡在Starting development server...不动99% 是 Webpack Dev Server 的host配置问题。Vue CLI 默认host: localhost在容器内无法绑定到0.0.0.0。解决方案在package.json的scripts中修改scripts: { watch: cross-env NODE_ENVdevelopment node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --progress --confignode_modules/laravel-mix/setup/webpack.config.js --host 0.0.0.0 --port 8080 }在docker-compose.yml中暴露8080端口并添加extra_hostsapp: # ... 其他配置 extra_hosts: - host.docker.internal:host-gateway # 允许容器内访问宿主机 ports: - 8080:8080启动后访问http://localhost:8080而非http://localhost。5.3 存储目录权限错误Permission denied的终极根治法storage和bootstrap/cache目录在容器内由devuserUID 1001拥有但宿主机上可能是root或其他用户导致php artisan config:cache失败。标准解法# 在宿主机上将 storage 和 cache 目录所有权改为 UID 1001 sudo chown -R 1001:1001 storage bootstrap/cache # 或者更彻底在 docker-compose.yml 中设置 init 容器修复权限 app: # ... 其他配置 init: true command: sh -c chown -R devuser:www-data /var/www/html/storage /var/www/html/bootstrap/cache php-fpm -F5.4 Docker Compose 启动缓慢depends_on不是万能药健康检查才是关键depends_on只控制容器创建顺序不保证服务就绪。MySQL 容器创建后要花 10–20 秒初始化数据目录。如果app容器启动太快php artisan migrate会因 MySQL 未就绪而失败。解决方案是强化healthcheckdb: image: mysql:5.7.33 # ... 其他配置 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -psecret] interval: 20s timeout: 10s retries: 10 start_period: 60s # 给足 60 秒初始化时间这样docker-compose up会等待db健康检查通过后才启动appartisan migrate一次成功。6. 进阶实战如何让这个环境支撑 Vue Laravel 混合开发并生成静态文件6.1 前端构建流程整合npm run production如何在容器内完成Laravel Mix 默认在宿主机运行npm run production但这样会生成public/mix-manifest.json而app容器内public目录是挂载的宿主机目录路径不一致。正确做法是在app容器内执行构建。修改docker/app/Dockerfile在末尾添加# 安装 Node.jsUbuntu 18.04 源里的 nodejs 太老用 Nodesource RUN curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - RUN apt-get install -y nodejs RUN npm install -g npm6.14.15 # 复制 package.json 和 package-lock.json COPY package*.json ./ # 安装前端依赖利用 Docker 层缓存 RUN npm ci --onlyproduction # 复制前端源码注意只复制 src不复制 node_modules COPY resources/ resources/ COPY webpack.mix.js ./然后在docker-compose.yml中添加构建命令app: # ... 其他配置 command: sh -c npm ci npm run production php-fpm -F这样每次docker-compose up都会自动执行npm run production生成public/mix-manifest.json和压缩后的 JS/CSSLaravel 的mix()辅助函数才能正常工作。6.2 生成纯静态文件php artisan ssr:build的容器化改造Laravel 并不原生支持 SSR但可通过spatie/laravel-sitemap或laravel-zero生成静态 HTML。更通用的做法是用puppeteer抓取页面。在app容器内安装puppeteer# 在 Dockerfile 中添加 RUN npm install -g puppeteer5.5.0 # 指定兼容 Ubuntu 18.04 的版本编写docker/app/static-build.sh#!/bin/bash # 启动 Laravel 内置服务器仅用于抓取 php artisan serve --host0.0.0.0:8000 --no-reload SERVER_PID$! # 等待服务器就绪 sleep 5 # 用 puppeteer 抓取首页 npx puppeteer launch --no-sandbox --disable-setuid-sandbox --headless --disable-gpu --dump-dom http://localhost:8000 public/index.html # 杀死服务器 kill $SERVER_PID在docker-compose.yml中添加一次性构建服务static-builder: build: ./docker/app volumes: - .:/var/www/html command: bash -c chmod x /var/www/html/docker/app/static-build.sh /var/www/html/docker/app/static-build.sh运行docker-compose run --rm static-builder即可生成public/index.html。6.3 安全加固this generated password is for development use only的应对策略Laravel 的APP_KEY和数据库密码在开发环境明文写在.env里这是合理妥协。但必须防范.env被意外提交到 Git。在项目根目录创建.gitignore.env .env.backup .env.local .env.php .env.testing .env.production同时在docker-compose.yml中用environment字段注入敏感变量而非挂载.env文件app: environment: - APP_KEY${APP_KEY} - DB_PASSWORD${DB_PASSWORD}然后在宿主机创建.env非 Git 跟踪APP_KEYbase64:your-32-byte-key-here DB_PASSWORDsecret这样既保证了安全性又不影响开发流程。我在实际项目中用这套方案支撑了 5 个 Laravel Vue 团队平均环境搭建时间从 2 小时缩短到 8 分钟。最深的体会是容器化不是为了炫技而是把“环境配置”这件脏活累活变成一行docker-compose up就能解决的确定性操作。当你不再为php -v输出的版本和phpinfo()里显示的不一样而抓狂当你git checkout切分支后docker-compose up依然稳如泰山你就真正拿到了现代 PHP 开发的入场券。最后分享一个小技巧把docker-compose.yml里的restart: unless-stopped改成no然后写个make up别名这样每次启动都是干净的避免旧容器残留状态污染新开发。