SSD Nodes Learn
指南 Matt Connor作者: Matt Connor · 更新于 2026-07-19

在 Ubuntu 24.04 上搭建 LAMP 与 PHP-FPM

在 Ubuntu 24.04 上搭建完整的 LAMP 环境:Apache、使用 unix_socket 认证的 MariaDB、配合 PHP-FPM 的 PHP 8.3、基于域名的虚拟主机,以及通过 Certbot 获取的免费 HTTPS。

您将搭建什么

LAMP 环境是在一台 Ubuntu 24.04 服务器上运行的四个部件:底层的 Linux、负责响应 HTTP 的 Apache、保存数据的 MariaDB,以及运行代码的 PHP 8.3。完成之后,您将拥有一个基于域名的虚拟主机来服务真实的应用目录、一个带有专用最小权限用户的数据库、通过 PHP-FPM 接入 Apache 的 PHP,以及覆盖在最外层的一张免费 Let's Encrypt 证书。

安装本身只是四条 apt 命令。本指南中几乎所有内容都是各部件之间的接线,以及那一小撮会让全新环境返回空白页、把源代码当作下载文件交给浏览器,或者拒绝您登录刚刚装好的数据库的错误。这些错误每一个都有可辨认的特征,下面都会点名,并附上您会看到的确切文字。

前置条件与需要如实说明的坑

假设您有一台全新的 Ubuntu 24.04 KVM VPS,带有一个 sudo 用户或 root,以及一个公网 IPv4 地址。最小环境在 1 GB 内存下即可运行;但在放上真正的、依赖数据库的应用之前,请给它 2 GB,因为 MariaDB 的默认缓冲区加上几个 PHP-FPM 工作进程会很快吃掉第一个 GB。

在结尾运行 Certbot 之前,有两件事必须成立,所以现在就把它们准备好。您需要一个 域名,其 A 记录指向 VPS 的公网 IP,因为 Let's Encrypt 通过 HTTP 对该域名进行验证,而裸 IP 地址永远无法获得证书。另外,80 和 443 端口必须能从互联网访问,在许多服务商那里这意味着既要在控制面板里的网络防火墙中开放它们,也要在机器上的 ufw 中开放。DNS 变更最长可能需要一小时才能生效,所以先设好 A 记录,等您用到时它就已经生效了。

第 1 步 - 安装 Apache 并确认默认页面

sudo apt update
sudo apt install -y apache2

apt 会为您启动并设为开机自启。检查一下:

systemctl status apache2

您需要看到一行 active (running)。现在在浏览器中打开 http://YOUR_SERVER_IP/。带有大幅 "It works!" 横幅的 Apache2 Ubuntu Default Page 就是正确的结果,它证明 Apache 正在服务,而不是出错。该页面位于 /var/www/html/index.html,由随包附带的默认虚拟主机 000-default.conf 提供服务。稍后您会把这两者都停用;但现在它们的存在正是您想看到的。

如果页面完全无法加载,但 systemctl 显示进程正在运行,那么就是防火墙挡住了。那正是下一步要处理的。

第 2 步 - 为 HTTP 和 HTTPS 开放防火墙

apache2 软件包注册了三个 ufw 应用配置。列出它们:

sudo ufw app list

您会看到 ApacheApache FullApache SecureApache 只开放 80 端口,Apache Secure 只开放 443 端口,而 Apache Full 两者都开放,这正是您想要的,因为您会在最后加上 TLS。

sudo ufw allow OpenSSH
sudo ufw allow "Apache Full"
sudo ufw enable

在运行 ufw enable 之前 先允许 OpenSSHufw 默认拒绝所有入站流量,在没有 SSH 规则的情况下启用它,会在它生效的那一刻切断您自己的连接,您能保住当前会话,但再也无法重连。用 sudo ufw status 确认;您需要看到 OpenSSHApache Full 以及它们对应的 v6 条目都显示为 ALLOW

第 3 步 - 安装 MariaDB 并对其加固

sudo apt install -y mariadb-server
systemctl status mariadb

Ubuntu 24.04 附带的是 MariaDB 10.11,一个长期支持版本,所以您不需要外部软件源。服务运行起来后,对它进行加固:

sudo mysql_secure_installation

这些提示值得认真读,而不是一路狂按回车。当它询问 当前 root 密码 时,按回车,因为现在还没有密码。当它询问 "Switch to unix_socket authentication?" 时,答案无所谓,因为该软件包上它已经启用了,所以按 n。出于下一段的原因,对 "Change the root password?" 回答 n,然后对其余问题都回答 Y:删除匿名用户、禁止远程 root 登录、删除测试数据库、重新加载权限表。

下面这部分让所有人都困惑。在 Ubuntu 的 MariaDB 上,root 数据库账户使用 unix_socket 认证,而不是密码。这意味着数据库信任您已经通过身份验证的那个 操作系统 用户。所以从 root shell 里这样就能用:

sudo mysql

……它会把您带到一个 MariaDB [(none)]> 提示符,不问密码。同一条命令以非特权用户身份运行则会被拒绝,而这正是重点所在:对数据库 root 的访问被绑定到机器上的 sudo,没有密码可供窃取、钓鱼或暴力破解。这比密码更安全,所以别去动它。由此得出的规则是:永远不要 把应用指向 root 账户。为每个应用创建一个专用用户(第 7 步),因为通过 TCP 带用户名和密码连接的应用本来就无法使用 socket 认证,而且您希望每个应用都被限制在自己的数据库内。

第 4 步 - 安装 PHP 8.3 与 PHP-FPM

Ubuntu 24.04 的默认 PHP 是 8.3。安装 FPM 进程管理器,以及一个典型应用所需的扩展:

sudo apt install -y php8.3-fpm php8.3-mysql php8.3-cli \
  php8.3-curl php8.3-xml php8.3-mbstring php8.3-zip

注意那份列表里 没有 什么:libapache2-mod-php。那个较旧的软件包会把一个 PHP 解释器嵌入到每个 Apache 进程里。它很简单,但每个工作进程无论在服务脚本还是静态图片都要携带一份 PHP 副本,二者共享同一生命周期,而且它只能配合 Apache 的 prefork MPM,也就是效率最低的那种。PHP-FPM 则把 PHP 作为它自己的一组进程池运行,Apache 通过 socket 与之通信。这样 Apache 就能对静态文件使用线程化的 event MPM,只把 PHP 请求转交过去,进程池的调优独立于 web 服务器,而且日后若改用 nginx 放在前面,完全相同的 FPM 配置依然适用。它成为当前默认是有充分理由的。

Apache 通过 proxy_fcgi 模块连到 FPM。启用它,启用 FPM 软件包放进来的配置,然后重启:

sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo systemctl restart apache2

a2enconf php8.3-fpm 会激活 /etc/apache2/conf-available/php8.3-fpm.conf,其中包含把 PHP 文件路由到 FPM socket 的规则。它的核心是匹配任何 .php 文件并把它转发到位于 /run/php/php8.3-fpm.sock 的 socket:

<FilesMatch ".+\.ph(ar|p|tml)$">
    SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>

您不需要编辑那个文件,它出厂就是正确的。但知道这个 socket 路径,正是您日后诊断 "PHP 被下载而不是运行" 和 "Primary script unknown" 故障的关键,这两者都归结为 Apache 和 FPM 对这个 socket 或它背后的文件产生了分歧。

第 5 步 - 为您的应用建立基于域名的虚拟主机

基于域名的虚拟主机让一个 IP 服务多个站点;Apache 根据请求中的 Host: 头挑选站点。为应用建一个目录,远离默认的 /var/www/html

sudo mkdir -p /var/www/testapp
sudo chown -R www-data:www-data /var/www/testapp
sudo chmod -R 755 /var/www/testapp

属主很重要。在 Ubuntu 上 Apache 和 PHP-FPM 都以 www-data 用户运行,所以 web 服务器必须读取的文件,以及应用必须写入的目录(例如上传文件夹),都应归 www-data 所有。如果您还会以登录用户身份编辑文件,一种常见做法是让文件归您自己所有,并把您的用户加入 www-data 组;对于单纯的部署,www-data:www-data 最不容易出意外。

/etc/apache2/sites-available/testapp.conf 创建虚拟主机:

<VirtualHost *:80>
    ServerName app.example.com
    DocumentRoot /var/www/testapp

    <Directory /var/www/testapp>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/testapp-error.log
    CustomLog ${APACHE_LOG_DIR}/testapp-access.log combined
</VirtualHost>

ServerName 设为您真实的域名。Options -Indexes 会在没有索引文件时阻止 Apache 列出目录,否则访问者就能浏览您的源代码树。AllowOverride All.htaccess 文件生效,大多数 PHP 应用为了美化 URL 都需要它;如果您的应用不需要,把它降为 None 可换来一点速度提升。启用这个站点,停用默认站点,检查配置,然后重新加载:

sudo a2ensite testapp
sudo a2dissite 000-default
sudo apache2ctl configtest
sudo systemctl reload apache2

apache2ctl configtest 应当打印 Syntax OKa2dissite 000-default 这一行是大家最容易忘的,也正是日后默认页面看起来"卡住不走"的原因,这在故障排查一节会讲到。

第 6 步 - 证明 PHP 能运行,然后删掉这个证据

往应用根目录放一个单行 PHP 文件:

echo "<?php phpinfo();" | sudo tee /var/www/testapp/info.php

访问 http://app.example.com/info.php。正确的结果是那张紫灰相间的 PHP Version 8.3.x 长表格,列出您已加载的模块,其中 Server API 一行显示为 FPM/FastCGI。最后这一行确认请求正经由 PHP-FPM,而不是 mod_php。

现在立刻删掉它:

sudo rm /var/www/testapp/info.php

phpinfo() 会暴露您确切的 PHP 版本、每一个已加载的扩展、文件路径和环境细节,这对任何在探测该服务器、寻找带有已知漏洞版本的人来说都是一份大礼。它是测试,不是功能。看到页面的那一刻就删掉它。如果浏览器不是显示表格,而是要 下载 info.php,那么 PHP 没有接入 Apache;在做任何其他事之前先跳到故障排查一节。

第 7 步 - 创建应用数据库和一个最小权限用户

以 socket 认证的 root 身份打开数据库:

sudo mysql

然后创建一个数据库和一个仅限于该数据库的用户:

CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'a-long-random-password';
GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;

这里有三处有意的选择。utf8mb4 才是真正的四字节 UTF-8,旧的 utf8 别名会悄悄截断表情符号和部分 CJK 字符,所以请始终使用 utf8mb4。授权是在 appdb.* 上,不是 *.*:这个用户只能操作自己的数据库而不能碰其他任何东西,所以应用中的一个 SQL 注入漏洞无法读取其他所有站点的表。而 'appuser'@'localhost' 把这个账户限制为只接受源自本机的连接。

以该用户身份测试:

mysql -u appuser -p appdb

它会询问密码,然后把您带到一个 MariaDB [appdb]> 提示符。注意没有 -h 标志,不加它,客户端就通过本地 Unix socket 连接,而这正是 MariaDB 所认定的 localhost。有一个值得了解的坑:对 MySQL 和 MariaDB 来说,localhost 指的是 Unix socket,而 127.0.0.1 指的是 TCP 连接。 在标准的 Ubuntu 24.04 MariaDB 上,服务器仍会把来自 127.0.0.1 的 TCP 连接解析回 localhost,所以两者都能匹配该账户;但在启用了 skip-name-resolve 的服务器上(一个常见的性能调整,在许多容器镜像里更是常态),两者会被当作不同的主机匹配,于是一个拨向 127.0.0.1 的应用即使密码正确,也会被拒绝并报 ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1' (using password: YES)

所以请把您的应用指向主机 localhost、用户 appuser、数据库 appdb,永远不要指向 root。PHP 的 mysqli 和 PDO 在主机是字面字符串 localhost 时都会切换到 Unix socket,正好匹配您刚创建的账户。如果某个框架坚持要用数字型 TCP 主机,就按它实际的连接方式来创建用户,用 'appuser'@'127.0.0.1',或者只有在它必须从另一台机器访问数据库时才用 @'%'(并配上一条防火墙规则)。

第 8 步 - 用 Certbot 加上 HTTPS

通过纯 HTTP 提供登录表单,会以明文发送密码,而且每一款现代浏览器都会把该页面标为 "Not secure"。Certbot 用一条命令就能修好这一点。用 Apache 插件安装它:

sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache

Certbot 在这里用到两个插件。apache 认证器 通过您正在运行的 Apache 短暂地提供一个质询文件,以此证明您控制该域名;apache 安装器 随后改写您的虚拟主机,加上 443 块,把它指向新证书,并默认把所有 HTTP 流量重定向到 HTTPS,自 Certbot 2.0 起就不再有重定向询问;如果您需要继续提供纯 HTTP,就加上 --no-redirect。因为您在第 5 步设了真实的 ServerName,Certbot 会自动检测到域名。证书有效期为 90 天,该软件包会安装一个 systemd 定时器来续期;用 sudo certbot renew --dry-run 验证定时器,它应当以 Congratulations, all simulated renewals succeeded 结尾。

关于质询过程、续期定时器,以及 DNS 和防火墙要求的完整讲解,请参见配套指南 在 Apache 上用 Certbot 签发免费的 Let's Encrypt TLS 证书

备份、升级与加固

备份两样承载您状态的东西:数据库和 web 根目录。每夜一次的逻辑转储是最简单可靠的做法,即 sudo sh -c 'mysqldump --all-databases --single-transaction | gzip > /root/db-$(date +%F).sql.gz',然后把它复制到机器之外。把整条管线包在 sudo sh -c 里很关键:没有它,shell 就会以您自己的用户身份运行 > /root/... 重定向并因 Permission denied 而失败,因为只有 mysqldump 继承了 sudo--single-transaction 在不锁表的情况下给出 InnoDB 表的一致快照。再配上对 /var/www/etc/apache2/sites-available 做一次 tar,您就能在一台全新的 VPS 上从这些文件重建整套环境。

升级就是普通的 sudo apt update && sudo apt upgrade。真正会咬人的是 PHP 版本跃升:当未来某个 Ubuntu 把默认 PHP 挪到 8.4 时,apt 可能会在 8.3 旁边装上 php8.4-fpm,socket 变成 /run/php/php8.4-fpm.sock,而您的 Apache 配置仍指向 8.3 的 socket。启用新配置(sudo a2enconf php8.4-fpm)并停用旧的,否则在一次原本例行的升级之后,您的站点就开始返回 Primary script unknown。由于 PHP 的发布节奏比 LTS 发行版更快,请查阅当前的 PHP 发布说明,而不要把补丁版本写死。

有两项加固步骤值得在第一天就做。首先,在机器上放一个 看守 SSH 的 Fail2ban,公网 VPS 几分钟内就会招来自动化登录尝试,而一个小小的 jail 能在封禁之前把成千上万次尝试压成寥寥几次。其次,如果您宁愿通过浏览器而不是手工编辑文件来管理 Apache 虚拟主机、MariaDB 数据库和用户,那么 基于网页的控制面板 Webmin 就坐落在这套完全相同的环境之上,并驱动您刚刚写下的那些配置文件。两者都不能替代对各部件的理解,但都能减少日常摩擦。

故障模式,附上您会看到的字符串

默认页面赶不走。 您改了虚拟主机、重新加载了,浏览器却仍然显示 "Apache2 Ubuntu Default Page" 和它的 "It works!" 横幅。Apache 服务的是第一个匹配的虚拟主机,当没有 ServerName 与请求匹配时,按字母顺序排在最前的配置胜出,而 000-default.conf 排在 testapp.conf 之前。要么是请求的主机名与您的 ServerName 不匹配,要么是您从没运行过 sudo a2dissite 000-default。停用默认站点,sudo systemctl reload apache2,再用 apache2ctl -S 确认,它会打印虚拟主机映射并显示哪份配置占据了默认位置。也清一下浏览器缓存;旧页面留下的一个已缓存 200 会安然地留存下来。

.php 文件被下载而不是运行。 您打开 info.php,浏览器下载了一个包含原始 <?php 源码的文件,或者把它显示为纯文本,而不是运行它。Apache 把该文件当作静态资源来服务,因为 PHP 处理器没有挂上去,您漏掉了 sudo a2enmod proxy_fcgi,或 sudo a2enconf php8.3-fpm,或者之后没有重启 Apache。把这三步都做一遍(第 4 步)并重新加载。用 apache2ctl -M | grep fcgi 确认模块已加载,它应当列出 proxy_fcgi_module。这是一次源代码泄露,不是外观上的小毛病,所以在往服务器上放任何真实内容之前先修好它。

ERROR 1698 (28000): Access denied for user 'root'@'localhost' 您在不加 sudo 的情况下运行了 mysql -u rootmariadb -u root。root 账户使用 unix_socket 认证,所以只有当您的操作系统用户确实是 root 时它才接受您。解决办法是 sudo mysql,不加 -u root,不用密码。这条消息是 socket 认证正常工作的预期行为,而不是安装坏了。

ERROR 1045 (28000): Access denied for user 'appuser'@'127.0.0.1',来自应用,且密码正确。该账户以 'appuser'@'localhost' 存在,但您的应用是在一台禁用了主机名解析(skip-name-resolve)的服务器上通过 TCP 连到 127.0.0.1,所以 MariaDB 把这两者当作不同的主机匹配,localhost 是 Unix socket,127.0.0.1 是 TCP。把应用指向主机 localhost 让它走 socket 从而匹配该账户,或者如果框架只会讲 TCP,就再创建一个账户 'appuser'@'127.0.0.1'

AH01071: Got error 'Primary script unknown' 出现在 /var/log/apache2/testapp-error.log,同时浏览器显示 File not found.。Apache 把请求交给了 PHP-FPM,但 FPM 在 Apache 给出的路径上找不到脚本。两个常见原因:您配置里的 FPM socket 指向了一个未安装的 PHP 版本(升级后只有 8.3 在跑却写了 php8.4 的 socket),或者文件确实不在那里,因为 DocumentRoot 与真实目录不一致。用 ls -l /run/php/ 检查 socket 是否存在,确认 DocumentRoot 与文件所在位置一致,然后同时重启 php8.3-fpmapache2

AH00558: apache2: Could not reliably determine the server's fully qualified domain name 在每次重启时出现。这是一条无害的警告,不是错误,Apache 在告诉您没有设置全局 ServerName。把 ServerName your.domain 写入 /etc/apache2/conf-available/servername.conf 并运行 sudo a2enconf servername 即可消掉它。

(98)Address already in use: AH00072: make_sock: could not bind to address 0.0.0.0:80 在 Apache 启动时出现。另一个 web 服务器已经占用了 80 端口,往往是先前实验遗留下来的一个 nginx。用 sudo ss -ltnp | grep :80 找到它,然后在启动 Apache 之前停止并禁用另一个服务。

FAQ

mod_php 还是 PHP-FPM,我该用哪个?

用 PHP-FPM。mod_php 把一个解释器嵌进每个 Apache 进程,并强制使用慢速的 prefork MPM,所以哪怕在服务一张静态图片时 Apache 也背着 PHP 的开销。PHP-FPM 把 PHP 作为一个独立的、可单独调优的进程池运行,Apache 通过 socket 与之相连,能配合更快的线程化 event MPM,日后原封不动地迁到 nginx 也没问题。它是现代默认;mod_php 只在依赖某种进程内行为的遗留应用上才说得通。

为什么我的浏览器下载 PHP 文件而不是运行它?

Apache 把这个 .php 文件当成静态下载,因为没有 PHP 处理器挂到它上面。在带 FPM 的 Ubuntu 24.04 上,这意味着您漏掉了 sudo a2enmod proxy_fcgisudo a2enconf php8.3-fpm,或之后的 Apache 重启之一。把这三步都做一遍并重新加载,然后用 apache2ctl -M | grep fcgi 验证 proxy_fcgi_module 已列出。在您修好之前,服务器一直在泄露源代码,所以要当作紧急事项处理。

为什么密码正确,MariaDB 里的 root 访问还是被拒绝?

因为根本没有密码,Ubuntu 的 MariaDB 通过 unix_socket 认证 root 账户,把它绑定到操作系统的 root 用户。从普通 shell 运行 mysql -u root 会按设计返回 ERROR 1698 (28000): Access denied for user 'root'@'localhost'。改用 sudo mysql 连接,并为任何应用创建一个独立的、以密码认证的用户,而不是复用 root。

我该怎么给 LAMP 站点加上 HTTPS?

安装 certbotpython3-certbot-apache,把一个域名的 A 记录指向服务器,然后运行 sudo certbot --apache。apache 认证器通过您正在运行的 Apache 证明您对域名的控制,安装器则改写虚拟主机以支持 443 端口并设置自动续期。完整的 Certbot 与 Apache 讲解 涵盖了质询、续期定时器和常见故障模式。

#lamp#apache#mariadb#php-fpm#ubuntu