简介:一个面向Linux运维与Web开发人员的Apache+PHP7+Mariadb一键安装包,专门解决在Ubuntu 24、麒麟V10、CentOS 7等系统上手动搭建Web环境时脚本整理繁琐、源码编译耗时、配置项众多的痛点。作者特别针对CentOS停止维护后无法正常安装的问题,重新配置了YUM源,老系统也能顺利安装部署。压缩包共7个文件,整体仅108KB,包含4个RPM依赖包、2个PHP配置文件和1个主安装脚本,结构精简清晰,运行脚本即可自动完成依赖检查、服务安装与基础配置。目前已有557人学习下载,适合需要快速搭建开发或测试环境、又不想在编译和调参上浪费大量时间的技术用户。通过该资源可直接获得一套经过整理与验证的部署方案,显著减少环境搭建与排错时间,也可为后续维护提供明确的配置参考。 做了大半年 Linux 服务器运维,手头最烦的事就是给新机器部署一套 Web 环境。尤其最近接到一个需求:要在 Ubuntu 24、麒麟 V10、CentOS 7 这三种完全不同的系统上,统一部署 Apache + PHP7 + MariaDB 这套经典组合。系统差异大、依赖版本乱、指令还各不相同,每次手动装都像是一场赌博。后来我干脆花了几个晚上,写了一套一键安装资源,把三套系统全部跑通测试过了,这里直接把脚本思路、细节实现和踩坑记录分享出来。
这套资源的核心价值很直接:拿到一台新机器,只需要上传一个压缩包,解压后执行一条命令,剩下的 Apache、PHP-FPM、MariaDB 安装和初始化全部自动完成。它既解决了不同发行版之间包管理器的差异问题,也把 PHP 版本锁定在了兼容性较好的 PHP 7.4(Ubuntu 和 CentOS 系都有现成渠道),同时还不碰系统自带的 Python、OpenSSL 等基础组件,相对干净。适合做内网项目交付、客户环境初始化、或者自己实验室快速建站的运维同学参考。
1. 为什么我决定写一套自己的 LAMP 一键脚本
先说背景。我做项目交付时最常遇到的就是客户给了台服务器,系统版本五花八门:有的是 CentOS 7 的老机器,有的是新采购的麒麟 V10,还有测试环境的 Ubuntu 24。我最早是用 Ansible 写过一次 playbook,但 Ansible 本身需要在控制机和目标机两边装 Python 环境,CentOS 7 默认的 Python 2.7 兼容性虽然没问题,可异地交付时客户不一定愿意装 agent 类工具。所以第二版我就换成了纯 shell 脚本,目标很简单:cp上去、bash install.sh收工,不依赖任何额外运行时。
这套脚本真正让我下决心重写的原因,是三个系统之间几个非常恼人的差异点:
- Ubuntu 24 的 apt 源默认只提供 PHP 8.3,但客户的旧项目代码只保证 PHP 7.2–7.4 下运行稳定,直接 apt 装 8.3 会有一堆 deprecated 告警甚至直接白屏。
- 麒麟 V10 有好几个版本,部分版本底层是 CentOS 8 的生态(dnf 指令),部分又是 CentOS 7 的生态(yum 指令),不检测清楚就写命令,脚本跑到一半就会中断。
- CentOS 7 默认的 yum 源里 PHP 版本停留在 5.4,要装 PHP 7.4 必须引入 EPEL 和 Remi 源,而 Remi 源又依赖 EPEL,这里面的依赖顺序一旦错了,安装直接失败。
所以这套一键脚本真正解决的,不是"敲三条命令"的问题,而是"跨发行版做环境适配"的工程问题。它比单纯的复制粘贴命令多了一层系统检测和源切换的逻辑,这也是它能在三个系统上保持一致体验的原因。
1.1 三个目标系统,一个都不能少
在设计时我没有按"主攻一个系统"的思路来做,而是把三个系统当成三个独立的"目标档位",脚本启动时先跑一段系统识别逻辑,再分流执行对应分支。这么做的好处是后续如果还需要适配 openEuler、Debian 之类的系统,只需要增加一个分支函数,不需要改动主流程。
系统识别主要靠/etc/os-release文件,里面会有ID和VERSION_ID字段。例如 Ubuntu 24 会返回ID=ubuntu、VERSION_ID="24.04",麒麟 V10 会返回ID=kylin,CentOS 7 则返回ID="centos"、VERSION_ID="7"。这几个关键字段就足够让脚本决定后续走哪套逻辑了。
1.2 一键脚本的设计边界:不该自动化的坚决不自动化
写自动化脚本最忌讳的是大包大揽。我见过不少一键脚本把防火墙关闭、SELinux 禁用、甚至系统内核参数都改了,最后出了问题根本不知道是哪一步动的手。我的原则是:只做"安装必需组件 + 启动服务 + 创建基础配置文件"这三件事,不碰系统安全策略,不修改内核参数,不删除任何已有软件包。
这样一来,脚本在客户现场的接受度会高很多。特别是 CentOS 7 上,SELinux 默认是 enforcing 状态,如果脚本直接 setenforce 0,某些客户的安全巡检会亮红灯。我的做法是:安装 httpd 后,用semanage port -a -t http_port_t -p tcp 8080这类命令单独放行 Apache 监听端口,既满足功能,又保持系统安全策略的完整性。
2. 资源包整体结构设计与分发方案
先放一下最终的目录结构,这是整个资源包的核心骨架:
lamp-onekey/ ├── install.sh # 统一入口,自动识别系统并分发 ├── conf/ │ ├── apache-vhost.conf # Apache 虚拟主机模板 │ ├── php.ini.production # PHP 生产环境常用配置 │ └── my.cnf.mariadb # MariaDB 免初始化配置模板 ├── scripts/ │ ├── ubuntu_install.sh # Ubuntu 24 专属安装逻辑 │ ├── kylin_install.sh # 麒麟 V10 专属安装逻辑 │ ├── centos7_install.sh # CentOS 7 专属安装逻辑 │ ├── mariadb_secure.sh # MariaDB 安全初始化脚本 │ └── common_lib.sh # 公共函数库:日志、打包、校验 ├── src/ │ └── php7.4-tarball/ # 可选:源码包缓存,内网离线时使用 └── README.md这种"一个入口 + 多个分支脚本 + 公共函数库"的结构,是我试了好几版以后确定下来的。早期我图省事把所有逻辑写在了一个 800 行的 install.sh 里,后来发现改一处要反复翻上下文,维护成本很高。拆成独立脚本以后,每个系统的问题只需要去对应文件里定位,调试效率提升非常明显。
2.1 统一入口 install.sh 的真实逻辑
install.sh 做的事情其实不多:检查是否为 root 用户、识别系统类型、加载公共函数库、然后调用对应系统的安装脚本。这里有个关键点,就是检查命令执行环境。很多脚本直接用#!/bin/bash,但有些精简版系统的/bin/sh指向的是 dash,不是 bash,所以第一行解释器必须写清楚,否则语法会报错。
install.sh 开头部分我这样处理:
#!/bin/bash set -euo pipefail source ./scripts/common_lib.sh if [ "$(id -u)" -ne 0 ]; then log_error "请使用 root 用户执行" exit 1 fi detect_os case "$OS_ID" in ubuntu) bash ./scripts/ubuntu_install.sh ;; kylin) bash ./scripts/kylin_install.sh ;; centos) bash ./scripts/centos7_install.sh ;; *) log_error "不支持的发行版: $OS_ID" exit 1 ;; esacset -euo pipefail这三行是我这次踩坑后硬性加上去的。set -e表示任何一条命令返回非零状态,脚本立即终止,防止某些步骤明明失败了还在继续往下跑,最后产生一个半残环境。set -u则保证变量未被定义时就报错,避免拼写错误导致静默问题。pipefail防止管道前一个命令失败但管道整体退出码为 0 的情况。这些设置对于跨系统脚本来说非常关键。
2.2 为什么用 shell 而不是 Ansible 或者 Docker
我最初也考虑过 Docker 方案,毕竟一条docker run确实省事。但实际交付时发现问题很多:首先是内网环境没有 Docker Hub 镜像源,pull 不下来;其次是客户的生产机器可能不允许运行 Docker daemon,安全管控比较严格;最后是镜像里跑的进程和宿主机之间有时区、日志路径、文件权限不一致的坑,排查起来比直装还费劲。
所以最终选了纯 shell,只在目标机器上调用系统原生的包管理工具。这也是我这几年下来比较坚定的一个结论:交付给客户的运维资源,依赖越少越好。系统自带的 bash、curl、tar、sed 就足够了,没有任何额外运行时依赖,任何一台能跑 Linux 的机器都能执行。
2.3 在线安装与离线安装的取舍
这套资源默认走在线安装,也就是调用 apt/yum 从云镜像源下载软件包。但客户现场经常是内网隔离环境,所以我额外做了离线包缓存机制,在src/目录里预留了 php7.4 源码包、rpm 包和 deb 包的位置。如果是离线部署,把对应系统架构的包提前放到src/目录,脚本检测到本地有匹配文件时,优先走本地安装。
离线安装的处理逻辑不复杂,但是在 CentOS 7 上有一点特别容易踩坑:rpm 包的依赖顺序。MariaDB 的 rpm 包依赖libaio、perl等基础库,如果直接一条rpm -ivh MariaDB-server-*.rpm拍下去,大概率会因为前置依赖缺失而失败。我的脚本里封装了一段rpm_install_multi函数,它会循环扫描当前目录下所有 rpm 包并逐个尝试安装,直到某一轮没有任何一个新包被安装成功为止,这样能最大程度自动解决依赖顺序问题。
3. 三个发行版的差异处理与实测记录
这部分是整套资源里最有含金量的,因为每套系统都有自己独特的"脾气",处理不好就会翻车。
3.1 Ubuntu 24:干净但也有坑
Ubuntu 24 的 apt 源默认 PHP 是 8.3,常规安装 Apache2 和 MariaDB 都很简单:
apt update apt install -y apache2 mariadb-server但 PHP 这里必须处理版本回退。我给 Ubuntu 分支指定的方案是使用 ondrej/php PPA,这是社区维护的 PHP 版本仓库,可以稳定获取 php7.4。添加 PPA 后执行:
add-apt-repository -y ppa:ondrej/php apt update apt install -y php7.4 php7.4-fpm php7.4-mysql php7.4-mbstring php7.4-curl php7.4-gd php7.4-xml安装完成后要特别注意 PHP-FPM 的 socket 配置。Ubuntu 下 php7.4-fpm 默认监听/run/php/php7.4-fpm.sock,而 Apache 的 mod_php 模块并不存在,必须走 FastCGI 代理方式。所以我的 Apache 虚拟主机配置里,专门加了这一段:
<FilesMatch \.php$> SetHandler "proxy:unix:/run/php/php7.4-fpm.sock|fcgi://localhost" </FilesMatch>如果不写这一句,Apache 会把 PHP 文件当纯文本输出,浏览器直接看到源码。
3.2 麒麟 V10:兼容 CentOS 生态但细节不同
麒麟 V10 是我这次适配中最需要小心的一环。它有两种形态:一种基于 CentOS 7 的 rpm 生态,一种基于 CentOS 8 的 dnf 生态。我实测的那台是迁移自 CentOS 7 的版本,所以走了 yum 分支。
麒麟默认的 yum 源里没有 php7.4,需要先安装 EPEL 源再安装 Remi 源。但麒麟的/etc/yum.repos.d/下自带的仓库文件名和 CentOS 有差异,直接照搬 CentOS 的epel-release命令会报错。后来我手动确认了系统的 baseurl 指向的是麒麟官方镜像,然后改用 remi-release 的 rpm 包直接安装,这样 Remi 源会写入/etc/yum.repos.d/remi.repo,和系统自带的仓库互不干扰。
安装 PHP 时,需要明确启用 remi-php74 模块流:
yum install -y epel-release rpm -Uvh https://rpms.remirepo.net/enterprise/remi-release-7.rpm yum --enablerepo=remi,remi-php74 install -y php php-fpm php-mysqlnd php-mbstring php-curl php-gd php-xml这里有一个容易忽略的地方:如果只写yum install php,yum 还是会把系统自带仓库里的 PHP 5.4 装进来,必须显式指定--enablerepo=remi-php74才会命中目标版本。我最初就是因为没加这个参数,装了好几次都得到 PHP 5.4,排查了大半天。
麒麟分支还有一个坑是systemctl对 MariaDB 服务的命名。CentOS 7 官方源里的 MariaDB 是 5.5,服务名是mariadb,但如果系统里同时装了 mysql 的兼容包,服务名可能会变成mysql。我的处理方式是一个兜底函数:尝试启动 mariadb,失败则尝试 mysql,再失败则输出手动检查提示。
3.3 CentOS 7:老而稳,最需要小心
CentOS 7 虽然主流生命周期已经过了,但它作为存量服务器系统占比依然很高。给 CentOS 7 做一键脚本,最大的挑战不是功能,而是源。默认 yum 源里的 httpd 是 2.4.6、PHP 是 5.4、MariaDB 是 5.5,全都太旧了。我的组合是:
- Apache 使用 CentOS 7 自带 httpd(2.4.6),功能足够,不额外升级;
- PHP 使用 Remi 源装 7.4;
- MariaDB 使用官方 MariaDB 镜像源装 10.4,官方源里同时提供了
mariadb-server、mariadb-client等完整包。
配置 MariaDB 官方源时,需要先创建/etc/yum.repos.d/mariadb.repo。注意 MariaDB 官方源地址里带了版本号和系统标识,系统标识必须写rhel7而不是centos7,写错了会 404:
[mariadb] name = MariaDB baseurl = https://mirror.mariadb.org/yum/10.4/rhel7-amd64/ gpgkey = https://mirror.mariadb.org/yum/RPM-GPG-KEY-MariaDB gpgcheck = 1CentOS 7 还有一个所有 CentOS 系脚本都必须面对的问题:SELinux。如果开着 enforcing,Apache 默认不能连数据库、不能写目录,光是一个"网站能打开但 PHP 连不上 MariaDB"的问题就能耗掉一晚上。我的做法是在 MariaDB 安全初始化脚本里,单独执行两条 SELinux 策略调整命令:
setsebool -P httpd_can_network_connect_db 1 setsebool -P httpd_can_network_connect 1这两条命令只影响 httpd 域的网络访问权限,不会关闭 SELinux 本身,安全上可接受,也容易向客户解释。
4. 安装后的初始配置与安全加固
一键安装完成后,脚本会留下一些基础配置模板,同时顺手做掉必要的安全初始化。这个环节不是可选项,尤其是 MariaDB 的空密码问题,这是很多新装环境被入侵的直接原因。
4.1 PHP-FPM 与 Apache 的整合细节
把 PHP-FPM 接入 Apache,除了上一节提到的 FilesMatch 代理配置,还要注意DirectoryIndex里要包含 index.php,否则访问根路径时不会自动去找 PHP 入口文件。Apache 的默认配置文件在三个系统里位置不同:Ubuntu 是/etc/apache2/apache2.conf,CentOS/麒麟是/etc/httpd/conf/httpd.conf。脚本里我直接写了一个函数,用 grep 检查是否已经有配置,没有再追加,避免重复安装时产生重复指令。
还有一个细节是 PHP 的listen地址。为了性能和权限隔离,我让 php-fpm 监听 Unix Socket 而不是 TCP 端口,这样外部网络无法直接探测到 PHP 服务。配置文件在/etc/php/7.4/fpm/pool.d/www.conf,关键参数如下:
listen = /run/php/php7.4-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660其中listen.mode = 0660保证 Socket 只允许属主和属组访问,Apache 进程用户如果是 www-data,就能正常转发请求。如果 Apache 进程用户是 apache,则需要把 owner/group 改成 apache,或者把 Apache 的 User 指令改成 www-data。
4.2 MariaDB 初始化与审计插件
新装的 MariaDB 默认 root 用户没有密码,而且只能通过 Unix socket 登录。我的安全初始化脚本做了这些事:
- 设置 root 密码(从环境变量读取,如果没有则生成随机密码并写入脚本同目录的密码文件);
- 删除匿名用户;
- 移除 test 数据库;
- 强制刷新权限表。
关于 audit plugin,我在 CentOS 7 和麒麟上额外启用了 MariaDB 的审计日志插件,因为这两个系统的安全合规要求比较高。MariaDB 从 10.1 开始自带了server_audit插件,启用方法很简单:
INSTALL SONAME 'server_audit'; SET GLOBAL server_audit_logging = ON; SET GLOBAL server_audit_file_path = '/var/log/mysql/audit.log'; SET GLOBAL server_audit_file_rotate_size = 104857600;这个插件会把所有连接、查询、权限变更事件写入审计日志,对等保测评很有帮助。Ubuntu 24 上的 MariaDB 版本更新,自带的插件兼容性也更好,直接安装即可。
4.3 服务自启与状态检查
脚本最后一步是设置开机自启并验证服务状态:
systemctl enable httpd php-fpm mariadb systemctl start httpd php-fpm mariadb systemctl --no-pager status httpd php-fpm mariadb这里要检查服务是否真的起来了,而不仅仅是设置 enable。我遇到过几次"enable 成功但服务实际没起来"的情况,典型的错误是 php-fpm 的 pid 目录不存在或者权限错误,导致启动失败但 systemctl 没有返回明显错误提示。所以脚本里在启动完成后会接着执行一次 curl 探测:
curl -I http://127.0.0.1/ 2>/dev/null | head -n 1如果返回 HTTP/1.1 200 或 403,说明 Apache 至少起来了;如果连接被拒绝,脚本会提示用户手动查看日志,而不是假装安装成功。
5. 常见问题与排查技巧实录
这里整理一下我在这三个系统上测试时遇到频率最高的几个问题,每个都配上排查思路和解决方案,后续抄作业时能少走很多弯路。
| 问题现象 | 系统范围 | 排查思路 | 解决方式 |
|---|---|---|---|
| 浏览器访问 PHP 文件显示源码 | 三个系统都有概率 | 说明 Apache 没有把 .php 交给 PHP-FPM 处理 | 确认 FilesMatch 代理配置已写入,重启 Apache 后再试 |
| php -v 显示版本正常,但页面报 502 Bad Gateway | 主要出现在 Ubuntu 24 | php-fpm 服务没起来或 socket 路径与配置不一致 | 检查 php-fpm 状态,确认代理配置里的 socket 路径与实际完全对应 |
| yum 安装 php 时得到的还是 5.4 | 麒麟 V10 / CentOS 7 | Remi 源未启用或未指定 php74 模块流 | 安装 remi-release 后,安装命令必须加--enablerepo=remi-php74 |
| PHP 页面连接数据库失败,报 permission denied | CentOS 7 / 麒麟 | SELinux 拦截了 Apache 到数据库的网络访问 | 执行setsebool -P httpd_can_network_connect_db 1 |
| MariaDB root 登录失败 | 三个系统都有概率 | 安装后未正确初始化 root 密码,socket 认证与实际密码不一致 | 使用mysql -uroot -p直接登录,或sudo mysql进 root 后再改密码 |
| Apache 启动报 Address already in use | 三个系统均有 | 已有 nginx 或者其他 Web 服务占用 80/443 端口 | 用ss -lntp查看占用进程,临时停掉旧服务后重启 Apache |
5.1 一个最容易忽略的权限问题
我在 Ubuntu 24 测试时遇到过一个问题:PHP-FPM 和 Apache 都能起来,但网站传文件到/var/www/html时提示没有写权限。排查了半天,发现是/var/www目录的属主是 root,HTML 子目录没有给 Apache 用户的写入权限。这个不是脚本的问题,是 Linux 权限的经典场景。我的建议是,如果站点需要上传文件,应该单独挂一个 upload 目录并设置好属主,而不是直接把整个 web 根目录开放写权限,避免被上传恶意文件。
5.2 关于一键脚本的自更新和幂等性
最后分享一个关于脚本设计的小心得。我在执行脚本前加了set -e,理论上只要某一步失败,脚本就会停下来。但为了处理"脚本跑了一半失败,修复后重新执行"的场景,脚本里所有关键步骤都设置了幂等特性:比如配置文件存在时会先备份再覆盖,用户已存在时不会重复创建,软件包已安装时跳过安装。这样一来,客户现场即使第一次执行因为网络原因失败了,第二次重跑也不会有副作用,这个体验对运维人员来说非常重要。
我个人的体会是,一键安装脚本这件事,难点从来不在"安装"本身,而在于怎么优雅地处理系统差异和异常情况。做这套资源前前后后花了几天时间,真正的时间都花在了版本检测、源配置、SELinux 这类"旁支"问题上。如果你只是在统一版本的系统上做部署,其实没必要写这么复杂;但如果你和我一样需要面对多个发行版的交付,那套检测分发、幂等处理、安全初始化的框架,就非常值得花时间搭建了。
本文还有配套的精品资源,点击获取