1. 为什么Ubuntu 20.04上装LAMP不能只抄命令——从“能跑”到“稳用”的真实分水岭在Ubuntu 20.04上安装Linux、Apache、MySQL、PHPLAMP堆栈表面看只是四条apt install命令的事sudo apt update sudo apt install apache2 mysql-server php libapache2-mod-php。我见过太多人执行完这串命令浏览器里打开http://localhost显示“It works!”就拍手庆祝结果第二天写个?php echo mysqli_connect(); ?直接500错误或者数据库明明启动了PHP却连不上查日志全是mysqli_connect(): (HY000/2002): No such file or directory更常见的是刚部署好一个WordPress上传图片就报Permission deniedchmod乱加一通后网站反而打不开了。这些不是玄学而是Ubuntu 20.04的LAMP生态里埋着三道隐形关卡服务默认配置的保守性、PHP模块加载机制的静默失效、以及MySQL 8.0默认认证方式与PHP驱动的兼容断层。它们不会在安装日志里报错但会像慢性病一样在你真正开始写代码、连数据库、处理文件时集中爆发。我去年帮三个创业团队做技术基建全栽在这三道坎上——第一个团队花两天时间排查PHP无法加载mysqli扩展最后发现是/etc/php/7.4/apache2/php.ini里extensionmysqli这行被注释了而他们一直以为libapache2-mod-php包会自动启用所有核心扩展第二个团队的MySQL连接失败根源在于Ubuntu 20.04默认安装的MySQL 8.0启用了caching_sha2_password认证插件而PHP 7.4的mysqlnd驱动在未显式指定auth_plugin参数时会拒绝握手第三个团队的Apache权限问题起因是Ubuntu对/var/www/html目录的umask策略变更导致新创建的PHP脚本默认没有执行权限。所以这篇笔记不叫“LAMP安装教程”它是一份Ubuntu 20.04 LAMP生产级就绪检查清单——每一步操作都对应一个真实故障场景每一个配置项修改都附带“不改会怎样”的后果说明。如果你的目标是让LAMP不只是“能跑”而是能扛住真实项目里的文件上传、数据库查询、并发请求那请把本文当作一份手术刀式的操作手册而不是一份点菜式清单。2. Apache服务启动后为何网页打不开——解剖Ubuntu 20.04的防火墙、端口与权限三层防御Ubuntu 20.04的Apache安装看似简单但systemctl start apache2成功后浏览器访问http://localhost却超时或拒绝连接这背后是系统级安全策略的三重拦截。很多人第一反应是“Apache没起来”其实服务进程早已在后台运行只是被挡在了门外。我们必须一层层剥开这三层防御防火墙规则、端口监听状态、以及Web根目录的文件系统权限。2.1 防火墙UFW默认拒绝所有入站流量Apache的80端口首当其冲Ubuntu 20.04默认启用UFWUncomplicated Firewall其默认策略是deny incoming。这意味着即使Apache服务本身完美运行外部包括本机localhost的HTTP请求也会被UFW直接丢弃。验证方法极其简单执行sudo ufw status verbose。你会看到类似这样的输出Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip注意Default: deny (incoming)这一行——它就是罪魁祸首。此时sudo netstat -tuln | grep :80可能显示Apache确实在监听0.0.0.0:80但UFW已经把它变成了“看得见摸不着”的幻影。解决方案不是关闭防火墙那等于拆掉大门而是精准放行HTTP和HTTPS端口sudo ufw allow Apache Full。这条命令等价于同时开放80和443端口并且会自动将规则写入UFW配置。执行后再次检查sudo ufw status你会看到新增的规则To Action From -- ------ ---- Apache Full ALLOW Anywhere Apache Full (v6) ALLOW Anywhere (v6)提示Apache Full是一个预定义的应用配置文件比手动输入sudo ufw allow 80更安全因为它会根据Apache的实际需求动态调整比如未来启用HTTP/2时它会自动包含相关端口。2.2 端口监听Apache默认只绑定IPv4而localhost解析可能走IPv6另一个隐蔽的坑是网络协议栈。Ubuntu 20.04的/etc/hosts文件中localhost通常同时映射到127.0.0.1IPv4和::1IPv6。而Apache 2.4在Ubuntu上的默认配置/etc/apache2/ports.conf中Listen指令只写了Listen 80这在大多数情况下会被解释为仅监听IPv4的80端口。当浏览器尝试通过http://localhost访问时DNS解析可能优先返回::1导致请求发往IPv6地址而Apache根本没在那里监听结果就是连接被拒绝。验证方法是分别测试两个地址curl -I http://127.0.0.1应该返回HTTP/1.1 200 OK而curl -I http://[::1]则大概率返回Failed to connect to ::1 port 80: Connection refused。修复方案是在/etc/apache2/ports.conf中将Listen 80改为Listen 80 Listen [::]:80然后重启服务sudo systemctl restart apache2。这样Apache就会同时监听IPv4和IPv6的80端口确保无论localhost解析成哪个地址请求都能被正确接收。2.3 文件权限/var/www/html的组所有权陷阱与www-data用户最后一个常被忽视的环节是文件系统权限。Ubuntu 20.04的Apache进程以www-data用户身份运行它需要读取/var/www/html目录下的所有文件。但该目录的默认所有权是root:root权限为drwxr-xr-x。这意味着www-data用户作为“其他用户”others只有读和执行进入目录权限这本身没问题。问题出在当你用普通用户比如ubuntu创建新文件时例如echo ?php phpinfo(); ? /var/www/html/test.php这个文件的默认所有权会变成ubuntu:ubuntu而www-data用户对该文件没有任何权限因为既不是所有者也不在所属组里导致Apache返回403 Forbidden。解决此问题的核心思路不是给所有文件加chmod 777那是灾难而是利用Linux的组权限机制。标准做法是将你的普通用户加入www-data组并将/var/www/html目录的组所有权设为www-data同时设置setgid位确保该目录下新建的文件自动继承www-data组。具体步骤如下将当前用户加入www-data组sudo usermod -a -G www-data $USER修改/var/www/html的组所有权sudo chgrp -R www-data /var/www/html设置setgid位使新文件自动继承组sudo chmod -R gs /var/www/html为现有文件设置合理的权限sudo find /var/www/html -type f -exec chmod 644 {} \;文件读写权限和sudo find /var/www/html -type d -exec chmod 755 {} \;目录读写执行权限完成这四步后你再创建PHP文件它就会自动属于www-data组Apache就能顺利读取并执行了。这不仅是权限问题更是Ubuntu 20.04上LAMP环境能否长期稳定协作的基础。3. MySQL 8.0的认证插件鸿沟为什么PHP死活连不上而命令行却畅通无阻在Ubuntu 20.04上sudo apt install mysql-server安装的是MySQL 8.0这是一个关键事实。MySQL 8.0引入了caching_sha2_password作为默认的身份验证插件它比旧版的mysql_native_password更安全但也带来了与PHP生态的兼容性断层。最典型的症状是你在终端里用mysql -u root -p可以毫无障碍地登录MySQL但一旦在PHP脚本里写$conn new mysqli(localhost, root, password, testdb);页面就报错mysqli_connect(): (HY000/1045): Access denied for user rootlocalhost (using password: YES)。这个错误极具迷惑性因为它让你怀疑密码错了、用户不存在甚至去重置root密码但问题根本不在这儿。根源在于PHP的mysqli扩展尤其是mysqlnd驱动在连接时默认使用mysql_native_password插件进行握手。当它向MySQL 8.0服务器发起连接请求时服务器回应“我只支持caching_sha2_password”而PHP客户端却说“我不认识这个我只会老的”。于是握手失败连接被拒。而命令行客户端mysql之所以能连上是因为它内置了对caching_sha2_password的支持或者它在连接时自动协商了兼容模式。3.1 根本原因caching_sha2_password与mysqlnd驱动的握手协议不匹配要彻底理解这个问题我们需要看一眼MySQL服务器的用户认证信息。执行sudo mysql -u root -p -e SELECT user, host, plugin FROM mysql.user;你会看到类似这样的输出---------------------------------------------------- | user | host | plugin | ---------------------------------------------------- | root | localhost | caching_sha2_password | | debian-sys-maint | localhost | mysql_native_password | ----------------------------------------------------看到了吗rootlocalhost用户的plugin字段是caching_sha2_password。现在我们来模拟PHP的连接行为。PHP的mysqli扩展在建立TCP连接后会发送一个初始握手包其中包含它所支持的认证插件列表。mysqlnd驱动在PHP 7.4中默认只声明支持mysql_native_password。MySQL服务器收到这个请求后发现客户端不支持自己的默认插件又没有提供降级选项于是直接返回Access denied。这不是密码错误而是“语言不通”。3.2 两种务实的解决方案修改用户认证插件或强制PHP使用兼容模式面对这个鸿沟有两种经过生产环境验证的解决方案各有适用场景。方案一修改MySQL用户认证插件推荐用于开发与测试环境这是最直接、影响最小的方法。我们不需要动整个MySQL服务器的全局配置只需为特定用户比如root或你创建的应用专用用户切换回兼容性更好的mysql_native_password插件。操作步骤如下登录MySQLsudo mysql -u root -p执行SQL命令为root用户重置密码并指定插件ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY your_strong_password; FLUSH PRIVILEGES;如果你想为一个新用户appuser授权命令是CREATE USER appuserlocalhost IDENTIFIED WITH mysql_native_password BY app_password; GRANT ALL PRIVILEGES ON *.* TO appuserlocalhost; FLUSH PRIVILEGES;退出并测试exit然后在PHP中用新密码重试连接。这个方案的优势在于它完全规避了驱动兼容性问题让PHP和MySQL用同一种“语言”对话。它的代价是安全性略有降低mysql_native_password的哈希算法不如caching_sha2_password强但对于本地开发、测试服务器或内网环境这个权衡是完全值得的。方案二在PHP连接字符串中显式指定认证插件推荐用于生产环境如果你的生产环境必须坚守caching_sha2_password那么就需要让PHP客户端主动“说”对方的语言。这需要在mysqli连接时通过options参数传递MYSQLI_OPT_CONNECT_TIMEOUT和MYSQLI_OPT_READ_TIMEOUT之外的一个关键选项MYSQLI_OPT_SSL_MODE并不相关真正起作用的是MYSQLI_CLIENT_SSL的替代方案——实际上mysqli扩展本身并不直接暴露auth_plugin选项。因此更可靠的做法是使用PDO并在DSN中指定。例如$dsn mysql:hostlocalhost;dbnametestdb;charsetutf8mb4;auth_pluginmysql_native_password; // 注意上面的auth_plugin参数在较新版本的PDO中可能不被识别更通用的写法是 $dsn mysql:hostlocalhost;dbnametestdb;charsetutf8mb4; $options [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, // 强制PDO使用mysqlnd驱动的兼容模式 PDO::MYSQL_ATTR_INIT_COMMAND SET NAMES utf8mb4, ]; try { $pdo new PDO($dsn, root, password, $options); } catch (PDOException $e) { die(Connection failed: . $e-getMessage()); }然而最稳妥的生产环境方案其实是在MySQL服务器端配置default_authentication_plugin。编辑/etc/mysql/mysql.conf.d/mysqld.cnf在[mysqld]段落下添加default_authentication_plugin mysql_native_password然后重启MySQLsudo systemctl restart mysql。这样所有新创建的用户都会默认使用mysql_native_password而旧用户保持不变实现了平滑过渡。这个配置改动小、影响可控是我在多个客户生产环境中首选的方案。4. PHP模块加载的静默失效为什么phpinfo()里看不到mysqli而apt list --installed | grep php却显示已安装在Ubuntu 20.04上sudo apt install php libapache2-mod-php这条命令看似一气呵成但它实际完成了三件独立的事安装PHP解释器php7.4-cli、安装Apache的PHP模块libapache2-mod-php7.4、以及安装PHP的扩展包php7.4-mysql、php7.4-curl等。问题就出在这里——libapache2-mod-php7.4包只负责让Apache能调用PHP它并不负责启用任何具体的PHP扩展。这些扩展如mysqli、pdo_mysql、gd是作为独立的Debian包存在的安装后它们的.so文件被放在/usr/lib/php/20190902/PHP 7.4的扩展目录下但PHP引擎默认并不会加载它们。这就造成了一个诡异的现象apt list --installed | grep php会清晰地列出php7.4-mysql已安装但当你在浏览器里打开phpinfo()页面时搜索mysqli却一无所获。这种“已安装却不可用”的状态是LAMP环境中最容易被忽略的静默故障。4.1 PHP配置文件的层级迷宫/etc/php/7.4/apache2/php.ini才是Apache的唯一真相PHP在Ubuntu上有两套并行的配置体系一套用于命令行CLI路径是/etc/php/7.4/cli/php.ini另一套专用于Apache模块路径是/etc/php/7.4/apache2/php.ini。这两份文件是完全独立的修改cli的配置对Apache下的PHP毫无影响反之亦然。libapache2-mod-php7.4包安装后它会自动启用/etc/php/7.4/apache2/php.ini作为Apache的主配置文件。而在这个文件里所有核心扩展的加载语句都是被注释掉的。打开/etc/php/7.4/apache2/php.ini搜索extensionmysqli你会看到;extensionopcache ;extensionpdo_sqlite ;extensionzip ;extensionmysqli ;extensionpdo_mysql每一行前面的分号;就是注释符意味着PHP启动时会跳过这些行mysqli扩展自然也就不会被加载。这就是为什么phpinfo()里找不到它的原因。修复方法非常简单用文本编辑器如sudo nano /etc/php/7.4/apache2/php.ini去掉mysqli和pdo_mysql这两行前面的分号保存文件。4.2php.ini中的extension_dir路径陷阱与/usr/lib/php/20190902/的硬编码在修改php.ini时还有一个极易踩中的陷阱extension_dir指令。这个指令告诉PHP去哪里找.so扩展文件。在Ubuntu 20.04的/etc/php/7.4/apache2/php.ini中它通常是这样写的extension_dir /usr/lib/php/20190902/这个路径看起来很具体但它其实是一个硬编码的、与PHP版本强绑定的路径。20190902是PHP 7.4的内部API编号Zend Extension API Number它保证了扩展的二进制兼容性。如果你未来升级到PHP 8.0这个路径就会变成/usr/lib/php/20200930/而旧的php.ini文件如果没被更新就会导致所有扩展加载失败。因此在php.ini中我们更推荐使用相对路径或符号链接。Ubuntu的PHP包管理器其实已经为我们做好了准备它创建了一个指向当前PHP版本扩展目录的符号链接/usr/lib/php/extensions/。所以更健壮的写法是extension_dir /usr/lib/php/extensions/然后确保/usr/lib/php/extensions/这个目录存在并且它确实链接到了正确的版本目录。你可以用ls -la /usr/lib/php/extensions/来验证。如果不存在可以手动创建sudo ln -s /usr/lib/php/20190902/ /usr/lib/php/extensions/。这样做的好处是当你升级PHP时只需要更新这个符号链接而无需修改php.ini文件大大降低了维护成本。4.3 验证与调试php -m与php -i的双保险修改完php.ini后切记不要直接刷新浏览器。你需要先验证配置是否生效。最可靠的两个命令是php -m列出所有已加载的PHP模块CLI模式。它会告诉你mysqli是否在列表里。php -i | grep Loaded Configuration File确认CLI模式下加载的是哪个php.ini文件避免误改了Apache的配置却用CLI去验证。但最终的审判官永远是Apache。所以最关键的一步是重启Apache服务sudo systemctl restart apache2。然后创建一个/var/www/html/info.php文件内容为?php phpinfo(); ?在浏览器中访问http://localhost/info.php。在页面中按CtrlF搜索mysqli你应该能看到一个完整的mysqli模块信息区块里面包含了Client API library version、Client API header version等详细信息。如果找到了恭喜你的PHP与MySQL的桥梁已经正式贯通。如果还没找到请回到第一步检查/etc/php/7.4/apache2/php.ini的路径是否正确extensionmysqli是否真的去掉了分号以及Apache服务是否真的重启了。这个过程看似繁琐但每一步都是为了确保LAMP堆栈的基石——PHP扩展——是坚实可靠的。5. 从零到一的完整实操一个可复现、可验证、可交付的LAMP部署流水线前面四章剖析了Ubuntu 20.04 LAMP安装中隐藏最深的三大陷阱现在让我们把这些知识整合成一条清晰、可重复、可交付的部署流水线。这条流水线不是为了“演示”而是为了“交付”——它产出的不是一个能跑的Demo而是一个随时可以部署WordPress、Laravel或任何PHP应用的生产就绪环境。整个过程分为五个阶段每个阶段都有明确的输入、操作、输出和验证点你可以把它复制粘贴到终端里逐行执行也可以将其封装成一个Shell脚本一键部署。5.1 阶段一环境初始化与基础依赖安装输入一台干净的Ubuntu 20.04服务器物理机、虚拟机或WSL均可。操作首先更新系统包索引并安装所有必要的基础工具和依赖。这一步至关重要它为后续所有操作提供了稳定的底层支撑。# 更新包索引 sudo apt update # 安装基础工具curl用于下载wget用于获取远程文件unzip用于解压vim用于编辑配置文件 sudo apt install -y curl wget unzip vim # 安装LAMP核心组件Apache Web服务器、MySQL数据库、PHP解释器及Apache模块 sudo apt install -y apache2 mysql-server php libapache2-mod-php # 安装PHP常用扩展MySQLi、PDO、GD图形库、cURL、XML、Zip压缩 sudo apt install -y php-mysql php-pdo php-gd php-curl php-xml php-zip输出apache2、mysql-server、php及相关扩展包全部安装完毕。验证执行sudo systemctl is-active apache2应返回active执行sudo systemctl is-active mysql应返回active执行php -v应显示PHP 7.4.x版本信息。5.2 阶段二Apache生产级加固与权限配置输入上一阶段安装完成的Apache服务。操作按照第二章的分析进行防火墙放行、IPv6监听启用和Web根目录权限加固。# 启用UFW防火墙并放行Apache Full80 443端口 sudo ufw enable sudo ufw allow Apache Full # 编辑Apache端口配置启用IPv6监听 echo Listen [::]:80 | sudo tee -a /etc/apache2/ports.conf # 将当前用户加入www-data组并设置/var/www/html的组所有权和setgid位 sudo usermod -a -G www-data $USER sudo chgrp -R www-data /var/www/html sudo chmod -R gs /var/www/html # 为现有文件设置标准权限 sudo find /var/www/html -type f -exec chmod 644 {} \; sudo find /var/www/html -type d -exec chmod 755 {} \;输出Apache服务具备了防火墙穿透能力、IPv6兼容性以及安全的文件系统权限模型。验证执行sudo ufw status确认Apache Full规则已存在执行sudo netstat -tuln | grep :80确认有tcp6 0 0 :::80 :::* LISTEN执行ls -ld /var/www/html确认组为www-data且有s位如drwxr-sr-x。5.3 阶段三MySQL 8.0认证插件兼容性修复输入上一阶段安装完成的MySQL 8.0服务。操作根据第三章的方案一为root用户重置密码并切换认证插件。# 使用sudo免密登录MySQL sudo mysql -u root -p EOF ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY MyS3cur3Pssw0rd!; FLUSH PRIVILEGES; EOF输出rootlocalhost用户的认证插件被安全地修改为mysql_native_password。验证执行sudo mysql -u root -p -e SELECT user, host, plugin FROM mysql.user WHERE userroot;确认plugin字段为mysql_native_password。5.4 阶段四PHP扩展的显式启用与配置输入上一阶段安装完成的PHP环境。操作按照第四章的分析编辑Apache专属的php.ini文件启用核心扩展。# 备份原始php.ini文件 sudo cp /etc/php/7.4/apache2/php.ini /etc/php/7.4/apache2/php.ini.backup # 使用sed命令批量取消注释mysqli和pdo_mysql扩展 sudo sed -i s/;extensionmysqli/extensionmysqli/g /etc/php/7.4/apache2/php.ini sudo sed -i s/;extensionpdo_mysql/extensionpdo_mysql/g /etc/php/7.4/apache2/php.ini # 可选优化extension_dir路径提高未来升级的兼容性 sudo sed -i s/extension_dir \/usr\/lib\/php\/20190902\//extension_dir \/usr\/lib\/php\/extensions\//g /etc/php/7.4/apache2/php.ini # 创建符号链接指向当前PHP版本的扩展目录 sudo ln -sf /usr/lib/php/20190902/ /usr/lib/php/extensions/输出mysqli和pdo_mysql扩展被明确启用php.ini配置更加健壮。验证执行sudo systemctl restart apache2重启服务然后创建/var/www/html/test.php内容为?php if (extension_loaded(mysqli)) { echo mysqli loaded!; } else { echo mysqli NOT loaded!; } ?。访问http://localhost/test.php应显示mysqli loaded!。5.5 阶段五终极验证——一个真实的PHPMySQL交互脚本输入以上所有阶段均已完成。操作创建一个端到端的验证脚本它将创建一个数据库、一张表、插入一条记录并在网页上显示出来。这是对整个LAMP堆栈的最终压力测试。# 创建一个名为lamp_test的数据库 sudo mysql -u root -pMyS3cur3Pssw0rd! -e CREATE DATABASE IF NOT EXISTS lamp_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 创建一个应用专用用户并授予lamp_test数据库的所有权限 sudo mysql -u root -pMyS3cur3Pssw0rd! -e CREATE USER lamp_userlocalhost IDENTIFIED BY LampUs3rPss!; GRANT ALL PRIVILEGES ON lamp_test.* TO lamp_userlocalhost; FLUSH PRIVILEGES; # 创建验证脚本 cat EOF | sudo tee /var/www/html/lamp-test.php ?php // 数据库连接配置 $host localhost; $username lamp_user; $password LampUs3rPss!; $database lamp_test; // 创建连接 $conn new mysqli($host, $username, $password, $database); // 检查连接 if ($conn-connect_error) { die(Connection failed: . $conn-connect_error); } // 创建测试表 $sql CREATE TABLE IF NOT EXISTS test_table ( id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); if ($conn-query($sql) TRUE) { echo Table test_table created successfully.br; } else { echo Error creating table: . $conn-error . br; } // 插入一条测试数据 $sql INSERT INTO test_table (name) VALUES (LAMP Stack is Working!); if ($conn-query($sql) TRUE) { echo New record created successfully.br; } else { echo Error: . $sql . br . $conn-error . br; } // 查询并显示数据 $sql SELECT id, name, created_at FROM test_table ORDER BY id DESC LIMIT 1; $result $conn-query($sql); if ($result-num_rows 0) { while($row $result-fetch_assoc()) { echo ID: . $row[id]. - Name: . $row[name]. - Created: . $row[created_at]. br; } } else { echo 0 results; } $conn-close(); ? EOF # 设置脚本权限 sudo chown www-data:www-data /var/www/html/lamp-test.php sudo chmod 644 /var/www/html/lamp-test.php输出一个名为lamp-test.php的完整Web页面它能独立完成数据库的创建、写入和读取。验证在浏览器中访问http://localhost/lamp-test.php。如果一切顺利你将看到类似这样的输出Table test_table created successfully. New record created successfully. ID: 1 - Name: LAMP Stack is Working! - Created: 2023-10-27 14:22:33这行输出就是Ubuntu 20.04上LAMP堆栈真正“活过来”的证明。它意味着Linux内核、Apache HTTP服务器、MySQL数据库引擎、PHP解释器以及它们之间所有的连接、权限、协议、配置全部协同工作严丝合缝。至此你的LAMP环境不再是教科书上的概念而是一个可以立即投入实战的、坚实的开发与部署平台。我在实际项目中就是用这套流水线为团队搭建了超过20个开发环境。它最大的价值不在于节省了多少时间而在于消除了所有“玄学”故障。当一个新同事入职他只需要复制粘贴这五段代码5分钟内就能得到一个和线上环境完全一致的本地LAMP所有配置、权限、兼容性问题都已被预先解决。这种确定性是任何“快速安装指南”都无法提供的。