手把手在 CentOS 7 虚拟机上实现最简 CI/CD(无 Docker / 无云服务)
本文通过纯本地环境(CentOS 7 + Git + Apache)演示 CI/CD 核心流程,帮助初学者理解持续集成与持续部署的本质。
📚 目录(点击跳转)
为什么要在本地实现 CI/CD?
- 理解 CI/CD 的底层原理
- 不依赖 GitHub/GitLab/Jenkins 等外部平台
- 适合学习、实验和内网开发环境
完整流程图:
开发者本地
│
│ git add / commit / push
▼
Bare 仓库:/opt/git/myapp.git\] ← 模拟 GitHub
│
│ 触发 post-receive 钩子
▼
\[CI/CD 脚本执行
├─ 1. 临时目录检出最新代码(/tmp/...)
├─ 2. 运行测试(如检查 index.html 是否存在)← CI
└─ 3. 同步文件到 /var/www/html/myapp/ ← CD
│
▼
Apache Web 服务\] ← 用户访问的"生产环境"
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 一、实验环境准备
* 操作系统:CentOS 7 虚拟机
* 所需软件:git、python3(可选)、httpd(Apache)
* 网络要求:仅本地回环,无需外网
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 二、创建本地"远程" Git 仓库
* 使用 `git init --bare` 模拟远程仓库
* 仓库路径示例:`/opt/git/myapp.git`
#清理旧环境(确保干净起点)
rm -rf ~/myapp-dev /opt/git/myapp.git /var/www/html/myapp /tmp/myapp-deploy-*
systemctl restart httpd
#创建"内部 GitHub"(bare 仓库)
mkdir -p /opt/git/myapp.git
cd /opt/git/myapp.git
git init --bare
#配置 Git 用户(避免提交报错)
#这里可以不是真实的用户名和邮箱,只是为了区分git用户避免混杂
git config --global user.name "ci-cd"
git config --global user.email "ci-cd@example.com"
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 三、开发者工作目录初始化
* 克隆 bare 仓库到本地开发目录
* 创建简单应用(如 `index.html`)
* 首次提交并推送至"远程"
#创建开发目录并首次提交
cd ~
git clone /opt/git/myapp.git myapp-dev
cd myapp-dev
echo '
CI/CD Works!
' > index.html
git checkout -b main # 显式创建 main 分支
git add .
git commit -m "Initial commit"
git push origin main # 首次推送
安装 rsync(用于同步文件)---这个得安装好
yum install -y rsync
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 四、核心:配置 Git Hook 实现自动化(CI/CD 关键)
* 编写 `post-receive` 钩子脚本
* 实现两个阶段:
* **CI(持续集成)**:代码检出 + 基础校验(如文件存在性)
* **CD(持续部署)**:自动同步到 Web 目录
#### post-receive 钩子脚本
cat > /opt/git/myapp.git/hooks/post-receive << 'EOF'
#!/bin/bash
# 启用严格模式:任何命令失败立即退出
set -e
# 部署目标目录(Apache 会从此处提供网页)
DEPLOY_DIR="/var/www/html/myapp"
# 临时工作目录(每次部署都新建,避免残留)
TMP_DIR="/tmp/myapp-deploy-$$" # $$ 是当前进程 ID,保证唯一
# ================================
# 第一步:获取 Git 推送的新提交 ID
# ================================
# Git 在 post-receive 中会通过标准输入(stdin)传入一行:
#
# 例如:000... dcc19ac refs/heads/main
read oldrev newrev refname
# 如果是删除分支(newrev 全 0),直接退出
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
exit 0
fi
# ================================
# 第二步:清理并准备临时目录
# ================================
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
# ================================
# 第三步:从 bare 仓库提取新代码(关键!)
# ================================
# 使用 git archive + newrev 提取指定提交的内容
# 输出是 tar 流,直接 pipe 给 tar 解压到 TMP_DIR
git --git-dir=/opt/git/myapp.git archive "$newrev" | tar -x -C "$TMP_DIR"
# ================================
# 第四步:CI ------ 检查关键文件是否存在
# ================================
if [ ! -f "$TMP_DIR/index.html" ]; then
echo " CI FAILED: index.html not found!"
exit 1 # 部署中断
fi
echo " CI PASSED: index.html is present."
# ================================
# 第五步:CD ------ 部署到 Web 目录
# ================================
mkdir -p "$DEPLOY_DIR"
# 同步 TMP_DIR/ 下所有内容到 DEPLOY_DIR/
# 注意结尾的 "/":表示同步内容,而不是嵌套目录
rsync -a --delete "$TMP_DIR/" "$DEPLOY_DIR/"
# 设置 Apache 可读权限(CentOS 默认用户是 apache)
chown -R apache:apache "$DEPLOY_DIR"
chmod -R 755 "$DEPLOY_DIR"
# 成功提示
echo " Deployed to http://localhost/myapp/"
EOF
## 添加执行权限(必须!)
chmod +x /opt/git/myapp.git/hooks/post-receive
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 五、配置 Apache 支持部署访问
* 设置 Web 根目录权限
* 确保 Apache 可读取部署路径
* 访问测试:`http://localhost/myapp/`
#测试部署
cd ~/myapp-dev
echo 'Auto Deploy Success! v2
' > index.html
git add .
git commit -m "Test auto deploy"
git push origin main
##你应该看到输出:
remote: CI PASSED: index.html is present.
remote: Deployed to http://localhost/myapp/
验证:
cat /var/www/html/myapp/index.html
# 输出:Auto Deploy Success! v2
浏览器访问:http://虚拟机IP/myapp/
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 六、完整流程测试
* 修改代码 → `git add/commit/push`
* 观察钩子自动执行输出
* 验证网页内容是否自动更新
为了完成本地提交--》自动部署 --》网页更新,同时需要展示当前时间,但在html文件中,$(date)只是一个普通的字符串,它是bash的命令语法,所以需要在部署之前将当前时间替换上去(对post-receive脚本进行修改),具体修改如下
cat > /opt/git/myapp.git/hooks/post-receive << 'EOF'
#!/bin/bash
set -e
DEPLOY_DIR="/var/www/html/myapp"
TMP_DIR="/tmp/myapp-deploy-$$"
read oldrev newrev refname
if [ "$newrev" = "0000000000000000000000000000000000000000" ]; then
exit 0
fi
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
# 提取新提交的内容
git --git-dir=/opt/git/myapp.git archive "$newrev" | tar -x -C "$TMP_DIR"
# 检查 index.html 是否存在
if [ ! -f "$TMP_DIR/index.html" ]; then
echo "CI FAILED: index.html not found!"
exit 1
fi
echo "CI PASSED: index.html is present."
#关键:在部署前替换 $(date) 为真实时间
current_time=$(date '+%Y-%m-%d %H:%M:%S')
sed -i "s/\$(date)/$current_time/" "$TMP_DIR/index.html"
# 部署到 Web 目录
mkdir -p "$DEPLOY_DIR"
rsync -a --delete "$TMP_DIR/" "$DEPLOY_DIR/"
chown -R apache:apache "$DEPLOY_DIR"
chmod -R 755 "$DEPLOY_DIR"
echo "Deployed to http://localhost/myapp/"
EOF
chmod +x /opt/git/myapp.git/hooks/post-receive
然后进行如下操作:

测试
cd ~/myapp-dev
echo ' CI/CD FULL TEST PASSED! $(date)
' > index.html
git add .
git commit -m "Test dynamic date"
git push origin main
http://192.168.126.50/myapp/

[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 七、总结:我们实现了什么?
| 步骤 | 对应 CI/CD 概念 |
|----------------|-------------|
| `git push` | 代码提交触发 |
| `post-receive` | 自动化流水线 |
| 文件校验 | 持续集成(CI) |
| 同步到 Web 目录 | 持续部署(CD) |
* 无需 Docker、无需云服务、100% 本地闭环
* 真正理解"提交即部署"的背后机制
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
### 八、后续可扩展方向(进阶)
* 加入单元测试脚本(如 Python unittest)
* 部署动态应用(如 Flask + systemd)
* 使用 rsync 或 scp 实现多节点部署
* 添加邮件/日志通知机制
[↑ 回到顶部](#↑ 回到顶部)
*** ** * ** ***
> 提示:本文所有操作均在 CentOS 7 虚拟机中完成,适合零基础读者动手实践。
>
> 如果你觉得有帮助,欢迎点赞、收藏、评论交流!