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 php@7.2 && brew install php@7.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 占满 CPU,MySQL 查询就变蜗牛,但 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-buster (Debian 10): apt-get install nginx 会装 nginx-full ,体积超 200MB,且 nginx -t 在容器里偶尔因 /proc 权限报错;
  • php:7.4-cli-focal (Ubuntu 20.04): mysql-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 loaded
  • php:7.4-cli-bionic (Ubuntu 18.04): mysql-client 5.7.33 nginx 1.14.0 openssl 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_HOST=db --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_ENV=local
      - DB_HOST=db
      - REDIS_HOST=redis
    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.1 docker-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  # 注意:不是 always!unless-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_ENV=local
      - APP_DEBUG=true
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=laravel
      - DB_USERNAME=root
      - DB_PASSWORD=secret
      - REDIS_HOST=redis
      - REDIS_PASSWORD=null
      - MAIL_MAILER=log
    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.2 Dockerfile 编写心法:精简、安全、可复现

./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 TZ=Asia/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 --from=composer: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.json composer install 会按最新兼容版本装,可能引入 BC Break;
  • USER devuser :这是安全底线。 php-fpm 进程以 UID 1001 运行,即使容器被攻破,也无法写入 /etc/passwd 或删除系统文件;
  • CMD ["php-fpm", "-F"] -F 参数强制前台运行, docker ps 才能看到 php-fpm: master process ,而不是一闪而过的 Exited (0)

3.3 php.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.webpackConfig npm 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 [arch=amd64] 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_HOST=db ,但 Laravel 的 .env 还需精确配置:

# 进入 app 容器执行 artisan 命令(关键!所有 artisan 命令必须在容器内执行)
docker-compose exec app bash

# 在容器内生成密钥(不要在宿主机生成!)
php artisan key:generate

# 配置 .env(注意:DB_HOST 必须是服务名 'db',不是 'localhost')
echo "DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=secret" >> .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 真正跑起 Laravel

docker/web 服务需要 Nginx 配置文件 ./docker/web/default.conf

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 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 refused app 容器尝试连接 localhost:3306 ,但 localhost 在容器内指向自身,而非 db 服务 docker-compose exec app cat /etc/hosts 检查 hosts 文件是否含 db 解析;确认 .env DB_HOST=db (不是 127.0.0.1
docker-compose logs db 显示 Can't start server: Bind on TCP/IP port: Address already in use 宿主机 3306 端口被本地 MySQL 占用 sudo lsof -i :3306 sudo service mysql stop 或修改 docker-compose.yml db.ports "3307:3306"
docker-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 。解决方案:

  1. package.json scripts 中修改:
"scripts": {
  "watch": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --progress --config=node_modules/laravel-mix/setup/webpack.config.js --host 0.0.0.0 --port 8080"
}
  1. docker-compose.yml 中暴露 8080 端口,并添加 extra_hosts
app:
  # ... 其他配置
  extra_hosts:
    - "host.docker.internal:host-gateway"  # 允许容器内访问宿主机
  ports:
    - "8080:8080"
  1. 启动后访问 http://localhost:8080 ,而非 http://localhost

5.3 存储目录权限错误: Permission denied 的终极根治法

storage bootstrap/cache 目录在容器内由 devuser (UID 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 -F"

5.4 Docker Compose 启动缓慢: depends_on 不是万能药,健康检查才是关键

depends_on 只控制容器创建顺序,不保证服务就绪。MySQL 容器创建后,要花 10–20 秒初始化数据目录。如果 app 容器启动太快, php artisan migrate 会因 MySQL 未就绪而失败。解决方案是强化 healthcheck

db:
  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 健康检查通过后,才启动 app artisan 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.js(Ubuntu 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 npm@6.14.15

# 复制 package.json 和 package-lock.json
COPY package*.json ./

# 安装前端依赖(利用 Docker 层缓存)
RUN npm ci --only=production

# 复制前端源码(注意:只复制 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/CSS,Laravel 的 mix() 辅助函数才能正常工作。

6.2 生成纯静态文件: php artisan ssr:build 的容器化改造

Laravel 并不原生支持 SSR,但可通过 spatie/laravel-sitemap laravel-zero 生成静态 HTML。更通用的做法是:用 puppeteer 抓取页面。在 app 容器内安装 puppeteer

# 在 Dockerfile 中添加
RUN npm install -g puppeteer@5.5.0  # 指定兼容 Ubuntu 18.04 的版本

编写 docker/app/static-build.sh

#!/bin/bash
# 启动 Laravel 内置服务器(仅用于抓取)
php artisan serve --host=0.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_KEY=base64:your-32-byte-key-here
DB_PASSWORD=secret

这样既保证了安全性,又不影响开发流程。

我在实际项目中用这套方案支撑了 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 别名,这样每次启动都是干净的,避免旧容器残留状态污染新开发。

更多推荐